From b80dd58dfe560c2091346dd6f2140a174c531614 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 17 Sep 2026 23:11:59 +0000 Subject: [PATCH 001/182] fix(code-index): seat text through retryable graph activation A retryable graph activation used to erase the prepared serving candidate, and an unfinished clone-fingerprint successor withheld the same seat after exact and lexical owners were ready. Keep the candidate in both cases so search can move off the predecessor while graph retries. Co-authored-by: Zack Jackson --- .../src/code_index_scheduler/registry.rs | 38 ++++++++++++++++ .../code_index_scheduler/registry/mount.rs | 26 ++++++++++- .../registry/seat_swap_tests.rs | 45 +++++++++++++++++++ 3 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/seat_swap_tests.rs diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry.rs index fc9c8db791..1b5fdc207a 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry.rs @@ -56,6 +56,8 @@ mod query_authority; mod reconcile_failure_isolation_tests; mod scope_identity; #[cfg(test)] +mod seat_swap_tests; +#[cfg(test)] mod serving_readiness_tests; mod serving_reads; @@ -352,6 +354,42 @@ impl ServingSwapOutcomeV1 { } } +/// The prepared generation id after a graph-activation failure. +/// +/// Retryable activation used to replace the prepared triple with +/// `Ok((Err, None, None))`, so the swap never ran and search kept the +/// predecessor for the whole backoff. Both retryable and terminal failures +/// now leave the sealed text generation in place; only graph readiness +/// retries or becomes unavailable. +pub(super) fn serving_generation_after_activation_failure<'a>( + prepared_generation: Option<&'a str>, + retryable: bool, + repeated_conflict: bool, +) -> Option<&'a str> { + if activation_failure_keeps_serving_candidate(retryable, repeated_conflict) { + prepared_generation + } else { + None + } +} + +fn activation_failure_keeps_serving_candidate(retryable: bool, repeated_conflict: bool) -> bool { + // `retryable && !repeated_conflict` used to wipe the candidate. Terminal + // failures already kept it. Both now keep it; the flags stay so a later + // change cannot drop only the retryable arm without this predicate. + let _ = (retryable, repeated_conflict); + true +} + +/// An unfinished text projection withholds the serving seat only when exact +/// or lexical owners are still missing. +/// +/// A clone-fingerprint successor keeps `text_projection_needs_work` after +/// those owners are ready. That is not `published_text_owner_unfinished`. +pub(super) fn text_projection_unfinished_withholds_seat(exact_and_lexical_ready: bool) -> bool { + !exact_and_lexical_ready +} + #[cfg(any(test, feature = "test-helpers"))] struct ColdMountFinalCommitGateV1 { project_root: PathBuf, diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/mount.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/mount.rs index 3429b111e1..4bdf2be067 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/mount.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/mount.rs @@ -1638,6 +1638,11 @@ impl CodeIndexSchedulerRegistryV1 { // A conflict verdict identical to the previous // attempt's for this same generation is deterministic // and falls through to the terminal arm instead. + // + // The prepared text candidate stays. Wiping it to + // `Ok((Err, None, None))` skipped the serving swap, + // so search kept the predecessor while graph backoff + // ran. if error.is_retryable_activation() && !repeated_conflict { last_seat_conflict = error .activation_conflict_context() @@ -1654,7 +1659,7 @@ impl CodeIndexSchedulerRegistryV1 { retry_delay_micros = retry_delay.as_micros() as u64, error = %error, "graph activation failed retryably; the sealed generation \ - stays unseated until the scheduled retry" + still seats and the next pass retries native graph" ); hotpath::gauge!("daemon.code_index.graph_seat.retry_total") .inc(1_u64); @@ -1668,7 +1673,12 @@ impl CodeIndexSchedulerRegistryV1 { // The scheduled retry is the seat attempt, so it // must not be turned away as already attempted. graph_seat_attempted = None; - result = Ok((Err(error), None, None)); + if !super::activation_failure_keeps_serving_candidate( + error.is_retryable_activation(), + repeated_conflict, + ) { + result = Ok((Err(error), None, None)); + } } else { next_seat_attempt_at = None; seat_retry_backoff = ACTIVATION_RETRY_BACKOFF_FLOOR; @@ -1699,6 +1709,18 @@ impl CodeIndexSchedulerRegistryV1 { // and serving-swap boundary. Graph work above ran only when // the outcome was ready. if let Some(outcome) = published_text_projection_outcome.take() { + // A clone-fingerprint successor is still `Unfinished` work + // after exact and lexical owners are ready. That must not + // clear the prepared generation the way a missing owner does. + let owners_ready = exact_and_lexical_ready_for_graph(graph_text.as_ref()); + let outcome = match outcome { + PublishedTextProjectionOutcomeV1::Unfinished + if !super::text_projection_unfinished_withholds_seat(owners_ready) => + { + PublishedTextProjectionOutcomeV1::Finished + } + other => other, + }; match outcome { PublishedTextProjectionOutcomeV1::Finished => { // The seat needs only the ready exact/lexical diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/seat_swap_tests.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/seat_swap_tests.rs new file mode 100644 index 0000000000..150a741eac --- /dev/null +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/seat_swap_tests.rs @@ -0,0 +1,45 @@ +use crate::code_index_scheduler::CodeIndexSchedulerErrorV1; + +use super::ServingSwapOutcomeV1; + +/// A retryable native-graph failure must not drop the generation search will +/// serve. The swap installs that same id; graph activation retries beside it. +#[test] +fn retryable_activation_keeps_the_serving_generation_matched() { + let error = CodeIndexSchedulerErrorV1::GraphActivation( + "graph runtime unavailable during activation".to_owned(), + ); + assert!( + error.is_retryable_activation(), + "GraphActivation is the retryable class that used to erase the seat candidate" + ); + let prepared = Some("generation.head"); + let seated = super::serving_generation_after_activation_failure( + prepared, + error.is_retryable_activation(), + false, + ); + assert_eq!( + seated, prepared, + "retryable graph activation must leave the prepared generation on the seat" + ); + let outcome = ServingSwapOutcomeV1::decide(true, true, seated.is_some()); + assert!( + outcome.installs(), + "the serving swap still writes the slot when the candidate survives: {outcome:?}" + ); +} + +/// Clone-fingerprint backfill is still unfinished after exact and lexical +/// owners are ready. That successor is not `published_text_owner_unfinished`. +#[test] +fn unfinished_clone_fingerprint_successor_is_not_text_projection_unfinished() { + assert!( + !super::text_projection_unfinished_withholds_seat(true), + "ready exact and lexical owners must still seat while the clone successor runs" + ); + assert!( + super::text_projection_unfinished_withholds_seat(false), + "missing exact or lexical owners still withhold the seat" + ); +} From fd6f24c7ae056b758f4c28cab718bacfe2f2893f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 17 Sep 2026 23:51:26 +0000 Subject: [PATCH 002/182] test(code-index): prove no-call probe seals zero edges The graph-rebuild refresh fixture emits file Contains edges that abstain and primitive u32 refs that never bind. Census edge_count 0 is that shape, not a stalled projector. Co-authored-by: Zack Jackson --- crates/tracedecay-code-index/src/chunks.rs | 59 ++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/crates/tracedecay-code-index/src/chunks.rs b/crates/tracedecay-code-index/src/chunks.rs index 5eee31f0e9..004594272d 100644 --- a/crates/tracedecay-code-index/src/chunks.rs +++ b/crates/tracedecay-code-index/src/chunks.rs @@ -4273,6 +4273,65 @@ pub fn real_symbol() {} assert_eq!(identities(&reformatted), identities(&artifacts)); } + /// The graph-rebuild refresh batch is one no-call function per file. + /// File-rooted `Contains` edges abstain because the file node is not a + /// symbol row, and primitive `u32` refs have no import or glob, so the + /// sealed relation census is empty. A same-file call still binds, so an + /// empty edge list is that fixture's shape rather than a dead emitter. + #[test] + fn no_call_refresh_probe_seals_zero_relation_edges() { + let index = |source: &str| { + let file = validated_file("src/refresh_batch/file_0000.rs", source.as_bytes()); + let batch = batch_for(&file, ParseOutcomeV1::Complete); + chunker() + .index_file(&file, &batch, &rust_descriptor(), &NeverCancelled) + .expect("indexing succeeds") + }; + + let probe = "pub fn refresh_probe_0000_000(input: u32) -> u32 { input + 0 }\n"; + let artifacts = index(probe); + assert_eq!(artifacts.symbols.len(), 1, "{:?}", artifacts.symbols); + assert!( + artifacts.edges.is_empty(), + "no-call probe must not seal relation edges: {:?}", + artifacts.edges + ); + assert!( + artifacts.edge_abstentions.iter().all(|abstention| { + abstention.reason == CodeIndexEdgeAbstentionReasonV1::MissingSymbolEndpoint + && abstention.legacy_kind == EdgeKind::Contains.as_str() + && abstention.source_node_id.starts_with("file:") + }), + "file Contains must abstain, not vanish: {:?}", + artifacts.edge_abstentions + ); + assert!( + !artifacts.edge_abstentions.is_empty(), + "the file node still emits a Contains edge that the census drops" + ); + assert!( + artifacts + .unresolved_references + .iter() + .all(|reference| reference.reference_name == "u32" + && matches!( + reference.kind, + RelationEdgeKindV1::TypeOf | RelationEdgeKindV1::Returns + )), + "primitive type refs stay unresolved, not edges: {:?}", + artifacts.unresolved_references + ); + + let calling = "pub fn caller() -> u32 { refresh_probe_0000_000(1) }\n\ + pub fn refresh_probe_0000_000(input: u32) -> u32 { input + 0 }\n"; + let calling = index(calling); + assert!( + calling.edges.iter().any(|edge| edge.kind == RelationEdgeKindV1::Calls), + "a same-file call must still seal, so the empty probe is not a dead path: {:?}", + calling.edges + ); + } + /// A body larger than the extractor's traversal budget reaches this path /// as an incomplete analysis: the lineage record carries the state and /// offers no exact counters, while ordinary bodies stay exact. From a96143955a754e3d83d9d92b71988818247c8aee Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 18 Sep 2026 00:03:06 +0000 Subject: [PATCH 003/182] 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. From 681defca04f32eeff8a368d51c964ce89ce5a51d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 18 Sep 2026 00:46:05 +0000 Subject: [PATCH 004/182] test(transport): read rebuild readiness from an untruncated page The graph-rebuild receipt timed out for 90s against a generation that was already current: status reported `current` on the expected revision with the advertised generation matching what search served, and the seat never moved for the whole wait. `limit: 3` still rendered 18084 characters against the 15000-character response frame, so MCP replaced the body with a retrieval handle and moved `results` and `code_generation` inside `preview`. Every predicate read them as absent and the wait spun to its deadline. One candidate carries several KiB of ranking provenance, so the page has to be smaller than a guess at how many results fit. Ask for one result, and refuse a truncated envelope outright rather than reading it as a warming generation, so the next frame overflow reports itself instead of presenting as a deadline. Pace both waits as well: each `tracedecay_status` call runs the census ready-probe, a freshness read, and branch diagnostics, and a `yield_now` spin re-entered that path thousands of times a second on the runtime running the reconcile it waits for. Co-authored-by: Zack Jackson --- .../graph_rebuild_status_test.rs | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/crates/tracedecay/tests/transport_acceptance_suite/graph_rebuild_status_test.rs b/crates/tracedecay/tests/transport_acceptance_suite/graph_rebuild_status_test.rs index 5b02449a74..6f52e11104 100644 --- a/crates/tracedecay/tests/transport_acceptance_suite/graph_rebuild_status_test.rs +++ b/crates/tracedecay/tests/transport_acceptance_suite/graph_rebuild_status_test.rs @@ -25,6 +25,15 @@ use tracedecay_mcp::JsonRpcResponse; const RECEIPT_TIMEOUT: Duration = Duration::from_secs(90); +/// How often the waits below re-ask the public MCP surface. +/// +/// Each `tracedecay_status` call runs the generation census ready-probe, a +/// scheduler freshness read, and branch diagnostics — Git opens and +/// blocking-pool work on the runtime that is also running the reconcile these +/// waits are waiting for. A `yield_now` spin re-entered that path thousands of +/// times a second, so the observer competed with the publication it observes. +const READINESS_POLL_INTERVAL: Duration = Duration::from_millis(25); + fn git(project: &Path, args: &[&str]) { let output = Command::new("git") .args(["-c", "core.hooksPath=.git/no-hooks"]) @@ -116,16 +125,26 @@ async fn search( project: &Path, query: &str, ) -> Value { - // Keep the page tiny: a generation-scale refresh batch otherwise returns - // multi-dozen-KiB candidate bodies that MCP truncates into a handle, and - // the wait helpers never see top-level `results` / `code_generation`. - tool( + // One ranked candidate is all these journeys read, and the frame budget is + // why the page has to stay that small: every candidate carries several KiB + // of ranking provenance, so a three-result page rendered 18 084 characters + // against the 15 000-character response frame. + let payload = tool( harness, project, "tracedecay_search", - json!({"query": query, "limit": 3, "format": "json"}), + json!({"query": query, "limit": 1, "format": "json"}), ) - .await + .await; + // A truncated envelope moves `results` and `code_generation` inside + // `preview`, where every predicate below reads them as absent. That is a + // malformed observation, not a warming generation: the waits below would + // spin to their deadline against a generation that is already current. + assert!( + payload.get("truncated").is_none(), + "search exceeded the MCP response frame and was replaced by a retrieval handle: {payload}" + ); + payload } fn result_paths(search: &Value) -> Vec<&str> { @@ -168,7 +187,7 @@ async fn wait_for_current_generation( return current_generation; } } - tokio::task::yield_now().await; + tokio::time::sleep(READINESS_POLL_INTERVAL).await; } }) .await @@ -219,7 +238,7 @@ async fn wait_for_background_refresh( } return; } - tokio::task::yield_now().await; + tokio::time::sleep(READINESS_POLL_INTERVAL).await; } }) .await From e4290b05e8b7ccfeaa463011af7ff1a69fb4ae9f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 18 Sep 2026 00:46:14 +0000 Subject: [PATCH 005/182] perf(code-index): sample git metadata through retained topology `GitMetadataFingerprintV1::capture` is the tier-1 staleness signal sampled on every query admission, and its own contract calls that cost fixed and cheap. It was neither: resolving the git-dir and common-dir through a fresh `gix::open` cost 72.7us of the 75.2us per capture, and runtime-core already owns a revalidating topology memo that answers the same question in 2.0us. Search runs two to three captures per call. Measured on a one-ref fixture repository, perf profile, 2000 warm iterations: capture 75.2us -> 10.0us. The memo is asked only for a checkout carrying `/.git`, which is both where an open at exactly this root resolves through and where a discovery started at this root stops, so it returns the same two paths. A bare control directory or a path that is not a checkout root still opens directly, because discovery would walk past it to an ancestor whose git metadata does not describe this project. The memo canonicalizes both paths; the fingerprint samples file metadata and contents, so its value and its persisted signature are unchanged. Co-authored-by: Zack Jackson --- .../src/code_index_scheduler/identity.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/identity.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/identity.rs index cf27b8f086..588c4dcbf3 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/identity.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/identity.rs @@ -269,7 +269,23 @@ fn refs_heads_signature(dir: &Path) -> Option { /// Resolve the git-dir (worktree-local) and common-dir (repository-shared) /// paths, falling back to `/.git` when gix cannot open the checkout so a /// non-repository path still yields a stable, if empty, fingerprint. +/// +/// These two paths are structural, but the fingerprint above is sampled on +/// every query admission, and re-deriving them through a fresh repository open +/// was 97% of its cost — 73 µs of 75 µs per capture, against 2 µs for the +/// retained topology this now asks first. A checkout that carries `/.git` +/// is one an open at exactly this root resolves through, which is also where a +/// discovery started at this root stops, so the retained answer is the same +/// answer. Anything else — a bare repository's control directory, a path that +/// is not a checkout root — still opens directly, because discovery would walk +/// past it to an ancestor whose git metadata does not describe this project. fn git_metadata_dirs(project_root: &Path) -> (PathBuf, PathBuf) { + if project_root.join(".git").exists() + && let Ok(topology) = + tracedecay_runtime_core::git_repository::repository_topology(project_root) + { + return (topology.git_dir.clone(), topology.common_dir.clone()); + } if let Ok(repository) = gix::open(project_root) { let git_dir = repository.git_dir().to_path_buf(); let common_dir = { From 7da32a15ae697aa9a6bb9cbfb4795a06a8e8d2ee Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 18 Sep 2026 00:46:22 +0000 Subject: [PATCH 006/182] perf(daemon): wake the composition code-index wait on seat changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The production composition's publication wait re-ran its readiness probe every 10ms for up to 20 seconds. That probe canonicalizes the root, takes the scheduler registry's mounted mutex several times, offloads a Git-metadata freshness capture to the blocking pool, and emits a decline event — spent on the same cores as the reconcile it waits for, and leaving a pending arrival the worker yields its graph prepare to. The registry already publishes the edge this wants: the serving watch signals every seat install and every source revalidation that keeps an unchanged generation seated. Drive the wait from it, keeping a 100ms floor for the terminal answers that install no seat — a route that has not mounted yet, and a verified source that publishes no generation at all. Measured on the graph-rebuild transport journey, perf profile: publication waits 55.1s -> 50.1s and 15.4s -> 14.5s, suite 164.7s -> 155.3s. Co-authored-by: Zack Jackson --- .../src/daemon/production_harness.rs | 55 ++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/crates/tracedecay/src/daemon/production_harness.rs b/crates/tracedecay/src/daemon/production_harness.rs index bd00b7baa2..f379445d3e 100644 --- a/crates/tracedecay/src/daemon/production_harness.rs +++ b/crates/tracedecay/src/daemon/production_harness.rs @@ -21,6 +21,7 @@ use super::project_server_lifecycle::{detach_project_servers, shutdown_detached_ use super::*; #[cfg(unix)] use tracedecay_application::pr_tracking::try_acquire_manual_branch_lifecycle; +use tracedecay_code_index_runtime::CodeIndexSchedulerRegistryV1; #[cfg(all(unix, feature = "test-transport"))] use tracedecay_code_index_runtime::git_transactions; use tracedecay_daemon_identity::profile_identity; @@ -957,6 +958,47 @@ impl ProductionProjectCompositionHarnessV1 { } } +/// Liveness floor for the readiness probe above. +/// +/// Every seat install and every source revalidation that keeps an unchanged +/// generation seated signals the serving watch, so the common case wakes on +/// the publication itself. This bound only covers the terminal answers that +/// install no seat — a route that has not mounted yet, and a verified source +/// that publishes no generation at all. +const CODE_INDEX_READINESS_BACKSTOP: Duration = Duration::from_millis(100); + +/// Park until the mounted route seats a generation, or until the backstop. +/// +/// The probe this paces canonicalizes the root, takes the scheduler registry's +/// mounted mutex several times, offloads a Git-metadata freshness capture to +/// the blocking pool, and emits a decline event. Re-running it on a fixed +/// millisecond cadence spends that on the same cores as the reconcile it is +/// waiting for, so the wait is driven by the serving watch instead. +async fn await_serving_generation_change( + schedulers: &CodeIndexSchedulerRegistryV1, + project_root: &Path, + serving_changed: &mut Option>, +) { + if serving_changed.is_none() { + *serving_changed = schedulers + .subscribe_serving_generation_changes(project_root) + .await; + } + let Some(changed) = serving_changed.as_mut() else { + tokio::time::sleep(CODE_INDEX_READINESS_BACKSTOP).await; + return; + }; + match timeout(CODE_INDEX_READINESS_BACKSTOP, changed.changed()).await { + Ok(Ok(())) | Err(_) => {} + // The route retired its watch. Drop it and let the next probe report + // whatever typed state replaced the mount. + Ok(Err(_)) => { + *serving_changed = None; + tokio::time::sleep(CODE_INDEX_READINESS_BACKSTOP).await; + } + } +} + #[hotpath::measure(label = "daemon.harness.wait_code_index", future = true)] async fn wait_for_production_composition_code_index( invocation: &DaemonInvocationState, @@ -975,6 +1017,12 @@ async fn wait_for_production_composition_code_index( return Ok(()); } let wait_started = Instant::now(); + // Subscribe before the first probe so a seat installed between the probe + // and the wait still wakes this loop. + let mut serving_changed = invocation + .code_index_schedulers + .subscribe_serving_generation_changes(project_root) + .await; let publication = timeout(Duration::from_secs(20), async { loop { // Scope-aware readiness is the authenticated demand boundary that @@ -1025,7 +1073,12 @@ async fn wait_for_production_composition_code_index( { return; } - tokio::time::sleep(Duration::from_millis(10)).await; + await_serving_generation_change( + &invocation.code_index_schedulers, + project_root, + &mut serving_changed, + ) + .await; } }) .await; From 5a1cc6faa54290cc12698df7fc5d63b125bbaacf Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 18 Sep 2026 01:03:27 +0000 Subject: [PATCH 007/182] test(tracedecay): read a handle-truncated tool answer in one place MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two suites reached the same wall — an MCP answer over the response frame arrives as `{"truncated": true, "handle": …, "preview": …}`, where `preview` is a string and `results` / `code_generation` are absent at the top level — and only one of them handled it. The graph-rebuild receipt read that envelope as a warming generation and spent its whole 90s deadline on a generation that was already current; `limit: 20 -> 3` was a guess at how many candidates fit, and 3 still rendered 18084 characters against 15000. Lift the daemon suite's retrieve-paging resolver into the shared test surface and read every tool answer through it, so the page size stops being load-bearing: it is a property of how much ranking provenance a candidate carries, not something a journey should track. Both suites' duplicate `tool` / `tool_payload` helpers go with it. Co-authored-by: Zack Jackson --- .../tracedecay/tests/common/mcp_response.rs | 99 +++++++++++++++++++ crates/tracedecay/tests/common/mod.rs | 2 + .../tests/daemon_suite/git_watch_test.rs | 78 ++------------- .../graph_rebuild_status_test.rs | 49 ++------- 4 files changed, 115 insertions(+), 113 deletions(-) create mode 100644 crates/tracedecay/tests/common/mcp_response.rs diff --git a/crates/tracedecay/tests/common/mcp_response.rs b/crates/tracedecay/tests/common/mcp_response.rs new file mode 100644 index 0000000000..4cb87ba816 --- /dev/null +++ b/crates/tracedecay/tests/common/mcp_response.rs @@ -0,0 +1,99 @@ +//! Reading an MCP tool answer that outgrew the response frame. +//! +//! A body over the frame cap does not arrive as the JSON a journey reads. MCP +//! stores the original, answers with `{"truncated": true, "handle": …, +//! "preview": …}`, and `preview` is a *string* holding a prefix of that JSON. +//! Every field a predicate looks for — `results`, `code_generation` — is then +//! absent at the top level, and a wait that reads them as missing cannot tell +//! a truncated answer from a generation that is still warming. +//! +//! So read the stored original through `tracedecay_retrieve` the way an agent +//! does, and keep that one authority: the page size that fits the frame is a +//! property of how much ranking provenance a candidate carries, not something +//! a journey should be guessing at. + +use std::path::Path; + +use serde_json::{Value, json}; +use tracedecay::daemon::ProductionProjectCompositionHarnessV1; +use tracedecay_mcp::JsonRpcResponse; + +/// Call one MCP tool on an admitted project and read its JSON answer. +pub async fn tool_json( + harness: &ProductionProjectCompositionHarnessV1, + project: &Path, + name: &str, + arguments: Value, +) -> Value { + let payload = called_tool_payload(harness, project, name, arguments).await; + resolved_tool_payload(harness, project, payload).await +} + +/// Reassemble a payload the response frame replaced with a retrieval handle. +/// +/// An untruncated payload is returned as it arrived. +pub async fn resolved_tool_payload( + harness: &ProductionProjectCompositionHarnessV1, + project: &Path, + payload: Value, +) -> Value { + if payload.get("truncated") != Some(&json!(true)) { + return payload; + } + let handle = payload["handle"] + .as_str() + .unwrap_or_else(|| panic!("truncated response omitted its retrieve handle: {payload}")); + let mut content = String::new(); + let mut offset = 0_u64; + loop { + let retrieved = called_tool_payload( + harness, + project, + "tracedecay_retrieve", + json!({"handle": handle, "format": "json", "offset": offset}), + ) + .await; + content.push_str(retrieved["content"].as_str().unwrap_or_else(|| { + panic!("truncated response handle carried no content page: {retrieved}") + })); + if retrieved["has_more"] != json!(true) { + break; + } + let next_offset = retrieved["next_offset"].as_u64().unwrap_or_else(|| { + panic!("retrieve reported more pages without a next offset: {retrieved}") + }); + assert!( + next_offset > offset, + "retrieve did not advance past offset {offset}: {retrieved}" + ); + offset = next_offset; + } + serde_json::from_str(&content).unwrap_or_else(|error| { + panic!("truncated response handle did not retrieve JSON: {error}; content={content}") + }) +} + +/// One tool call, decoded but not reassembled. `tracedecay_retrieve` pages +/// answer within the frame by construction, so the paging loop above reads +/// them through this rather than through [`tool_json`]. +async fn called_tool_payload( + harness: &ProductionProjectCompositionHarnessV1, + project: &Path, + name: &str, + arguments: Value, +) -> Value { + let response = harness + .call_tool(project, name, arguments) + .await + .unwrap_or_else(|error| panic!("{name} failed: {error}")); + decoded_tool_payload(&response) +} + +fn decoded_tool_payload(response: &JsonRpcResponse) -> Value { + assert!(response.error.is_none(), "{response:?}"); + let result = response.result.as_ref().expect("tool result"); + assert_ne!(result["isError"], true, "tool effect failed: {result}"); + let text = result["content"][0]["text"].as_str().expect("tool text"); + serde_json::from_str(text) + .unwrap_or_else(|error| panic!("tool returned invalid JSON: {error}; text={text}")) +} diff --git a/crates/tracedecay/tests/common/mod.rs b/crates/tracedecay/tests/common/mod.rs index b460681656..5d8e698900 100644 --- a/crates/tracedecay/tests/common/mod.rs +++ b/crates/tracedecay/tests/common/mod.rs @@ -1,6 +1,8 @@ #![allow(dead_code)] // shared test support: each suite binary compiles this module and uses a subset pub mod fixture; +#[cfg(feature = "test-transport")] +pub mod mcp_response; pub mod repository_layout; use std::ffi::{OsStr, OsString}; diff --git a/crates/tracedecay/tests/daemon_suite/git_watch_test.rs b/crates/tracedecay/tests/daemon_suite/git_watch_test.rs index 07e8291aa5..4f9efc4f5c 100644 --- a/crates/tracedecay/tests/daemon_suite/git_watch_test.rs +++ b/crates/tracedecay/tests/daemon_suite/git_watch_test.rs @@ -15,7 +15,8 @@ use tracedecay_code_index_retention::code_index_generations::{ DurablePublicationPointerV1, scoped_code_index_store_root, }; use tracedecay_domain::configuration::SYNC_WATCH_LINKED_WORKTREES_SETTING_KEY; -use tracedecay_mcp::JsonRpcResponse; + +use crate::common::mcp_response::tool_json; fn git(project: &Path, args: &[&str]) { let output = Command::new("git") .args(["-c", "core.hooksPath=.git/no-hooks"]) @@ -66,29 +67,8 @@ async fn indexed_repo() -> (TempDir, PathBuf, ProductionProjectCompositionHarnes .unwrap(); (root, project, harness) } -fn tool_payload(response: &JsonRpcResponse) -> Value { - assert!(response.error.is_none(), "{response:?}"); - let result = response.result.as_ref().expect("tool result"); - assert_ne!(result["isError"], true, "tool effect failed: {result}"); - let text = result["content"][0]["text"].as_str().expect("tool text"); - serde_json::from_str(text) - .unwrap_or_else(|error| panic!("tool returned invalid JSON: {error}; text={text}")) -} -async fn tool( - harness: &ProductionProjectCompositionHarnessV1, - project: &Path, - name: &str, - arguments: Value, -) -> Value { - tool_payload( - &harness - .call_tool(project, name, arguments) - .await - .unwrap_or_else(|error| panic!("{name} failed: {error}")), - ) -} async fn status(harness: &ProductionProjectCompositionHarnessV1, project: &Path) -> Value { - tool( + tool_json( harness, project, "tracedecay_status", @@ -116,7 +96,7 @@ async fn search( project: &Path, query: &str, ) -> Value { - let payload = tool( + let payload = tool_json( harness, project, "tracedecay_search", @@ -127,53 +107,9 @@ async fn search( payload["reason"], "search_capacity_unavailable", "search {query:?} lost the execution permit race and was refused instead of queued: {payload}" ); - resolve_truncated_tool_payload(harness, project, payload).await + payload } -/// A `tracedecay_search` body over the MCP response cap arrives as a handle -/// envelope whose preview is not the JSON the journey reads. Reassemble the -/// stored original through `tracedecay_retrieve` pages exactly as an agent -/// does, so `code_generation` and `results` come from the full answer. -async fn resolve_truncated_tool_payload( - harness: &ProductionProjectCompositionHarnessV1, - project: &Path, - payload: Value, -) -> Value { - if payload.get("truncated") != Some(&json!(true)) { - return payload; - } - let handle = payload["handle"] - .as_str() - .unwrap_or_else(|| panic!("truncated search omitted retrieve handle: {payload}")); - let mut content = String::new(); - let mut offset = 0_u64; - loop { - let retrieved = tool( - harness, - project, - "tracedecay_retrieve", - json!({"handle": handle, "format": "json", "offset": offset}), - ) - .await; - content.push_str(retrieved["content"].as_str().unwrap_or_else(|| { - panic!("truncated search handle carried no content page: {retrieved}") - })); - if retrieved["has_more"] != json!(true) { - break; - } - let next_offset = retrieved["next_offset"].as_u64().unwrap_or_else(|| { - panic!("retrieve reported more pages without a next offset: {retrieved}") - }); - assert!( - next_offset > offset, - "retrieve did not advance past offset {offset}: {retrieved}" - ); - offset = next_offset; - } - serde_json::from_str(&content).unwrap_or_else(|error| { - panic!("truncated search handle did not retrieve JSON: {error}; content={content}") - }) -} fn symbol_count(payload: &Value, name: &str) -> usize { payload["results"] .as_array() @@ -191,7 +127,7 @@ fn generation_index_len(data_root: &Path, project: &Path) -> usize { pointer.generation_index.len() } async fn request_refresh(harness: &ProductionProjectCompositionHarnessV1, project: &Path) { - let receipt = tool( + let receipt = tool_json( harness, project, "tracedecay_admin_sync", @@ -356,7 +292,7 @@ async fn linked_worktree_requires_mount_then_serves_only_its_exact_generation() // `linked_worktree_disabled` state and never publishes. The opt-in is a // project-layer setting decided at route open, so write it through the // production configuration tool before the worktree route opens. - let receipt = tool( + let receipt = tool_json( &harness, &project, "tracedecay_configuration_set", diff --git a/crates/tracedecay/tests/transport_acceptance_suite/graph_rebuild_status_test.rs b/crates/tracedecay/tests/transport_acceptance_suite/graph_rebuild_status_test.rs index 6f52e11104..3375161f6b 100644 --- a/crates/tracedecay/tests/transport_acceptance_suite/graph_rebuild_status_test.rs +++ b/crates/tracedecay/tests/transport_acceptance_suite/graph_rebuild_status_test.rs @@ -21,7 +21,8 @@ use std::time::Duration; use serde_json::{Value, json}; use tracedecay::daemon::ProductionProjectCompositionHarnessV1; -use tracedecay_mcp::JsonRpcResponse; + +use crate::common::mcp_response::tool_json; const RECEIPT_TIMEOUT: Duration = Duration::from_secs(90); @@ -81,31 +82,8 @@ fn head(project: &Path) -> String { .to_owned() } -fn tool_payload(response: &JsonRpcResponse) -> Value { - assert!(response.error.is_none(), "{response:?}"); - let result = response.result.as_ref().expect("tool result"); - assert_ne!(result["isError"], true, "tool effect failed: {result}"); - let text = result["content"][0]["text"].as_str().expect("tool text"); - serde_json::from_str(text) - .unwrap_or_else(|error| panic!("tool returned invalid JSON: {error}; text={text}")) -} - -async fn tool( - harness: &ProductionProjectCompositionHarnessV1, - project: &Path, - name: &str, - arguments: Value, -) -> Value { - tool_payload( - &harness - .call_tool(project, name, arguments) - .await - .unwrap_or_else(|error| panic!("{name} failed: {error}")), - ) -} - async fn status(harness: &ProductionProjectCompositionHarnessV1, project: &Path) -> Value { - tool( + tool_json( harness, project, "tracedecay_status", @@ -125,26 +103,13 @@ async fn search( project: &Path, query: &str, ) -> Value { - // One ranked candidate is all these journeys read, and the frame budget is - // why the page has to stay that small: every candidate carries several KiB - // of ranking provenance, so a three-result page rendered 18 084 characters - // against the 15 000-character response frame. - let payload = tool( + tool_json( harness, project, "tracedecay_search", - json!({"query": query, "limit": 1, "format": "json"}), + json!({"query": query, "limit": 3, "format": "json"}), ) - .await; - // A truncated envelope moves `results` and `code_generation` inside - // `preview`, where every predicate below reads them as absent. That is a - // malformed observation, not a warming generation: the waits below would - // spin to their deadline against a generation that is already current. - assert!( - payload.get("truncated").is_none(), - "search exceeded the MCP response frame and was replaced by a retrieval handle: {payload}" - ); - payload + .await } fn result_paths(search: &Value) -> Vec<&str> { @@ -291,7 +256,7 @@ async fn background_refresh_and_reopen_report_only_servable_generations_inner() install_background_batch(isolation.path(), &project); commit_all(&project, "install background refresh batch"); let refreshed_revision = head(&project); - let receipt = tool( + let receipt = tool_json( &harness, &project, "tracedecay_admin_sync", From 9c1b3f158baf5318d988ee9c52aaa79a8d1ef56a Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sun, 20 Sep 2026 03:37:33 +0000 Subject: [PATCH 008/182] test(dist): reproduce the crate-local staging collision The product crate owns `crates/tracedecay/tests/fixtures`, the same path the root `tests/fixtures` asset is staged onto. The snapshot regression fixture never carried crate-local content there, so it never exercised that overlap and the gate's first real run failed instead. Seed the collision and assert the staged asset carries the root entries and nothing else. Co-Authored-By: Claude Fable 5.1 --- scripts/test-check-distribution-snapshot.sh | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/scripts/test-check-distribution-snapshot.sh b/scripts/test-check-distribution-snapshot.sh index 01a3af91ff..30d1c00ec2 100644 --- a/scripts/test-check-distribution-snapshot.sh +++ b/scripts/test-check-distribution-snapshot.sh @@ -75,6 +75,11 @@ printf 'wrapper\n' >"$repo/dashboard/hermes-wrapper/fixture" printf 'bundle\n' >"$repo/dashboard/app-dist/fixture" printf '#!/usr/bin/env bash\n' >"$repo/scripts/run-session-temporal-benchmark.sh" +# The product crate carries its own `tests/fixtures`, which occupies the path +# the root asset of the same name is staged onto. +mkdir -p -- "$repo/crates/tracedecay/tests/fixtures/crate_local" +printf 'crate local\n' >"$repo/crates/tracedecay/tests/fixtures/crate_local/fixture" + git -C "$repo" init -q git -C "$repo" config user.name "TraceDecay test" git -C "$repo" config user.email "test@tracedecay.local" @@ -157,4 +162,14 @@ grep -Fxq "original readme" "$staged/crates/tracedecay/README.md" || { } grep -Fxq "mutated live readme" "$repo/README.md" +product_fixtures="$staged/crates/tracedecay/tests/fixtures" +[[ ! -e "$product_fixtures/crate_local" ]] || { + echo "crate-local content survived beside the staged root asset" >&2 + exit 1 +} +[[ -f "$product_fixtures/packaged_host_events/claude.json" ]] || { + echo "staged root asset is missing from the product package" >&2 + exit 1 +} + printf 'distribution staged-snapshot regression passed\n' From 1d7dee1a68c5e339f1aa2a12e7de68112850c3c9 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sun, 20 Sep 2026 03:37:42 +0000 Subject: [PATCH 009/182] fix(dist): stage package assets onto a cleared destination `cp -a` merges a directory into an existing directory of the same name, so staging the root `tests/fixtures` asset beside the product manifest left the package-local copy a superset of the root tree: the crate's own `tests/fixtures/impls_behavior` survived alongside it. The snapshot assertion then reported the staged asset differing from its snapshot and the gate exited before packaging. Clear each destination path before copying, in both the product and CLI asset loops, so a staged asset is exactly the validated root snapshot no matter what the package directory already holds. Co-Authored-By: Claude Fable 5.1 --- scripts/check-distribution-acceptance.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scripts/check-distribution-acceptance.sh b/scripts/check-distribution-acceptance.sh index 72cb5820d3..763e4e8922 100755 --- a/scripts/check-distribution-acceptance.sh +++ b/scripts/check-distribution-acceptance.sh @@ -384,10 +384,16 @@ declare -a staged_root_assets=( "tests/fixtures" "scripts/run-session-temporal-benchmark.sh" ) +# A package directory may already carry its own entry at the destination path, +# as `crates/tracedecay/tests/fixtures` does. `cp -a` merges a directory into +# an existing directory of the same name, which would leave the package-local +# asset a superset of the root one. Clear the destination so the staged asset +# is exactly the root snapshot the assertion below demands. for asset in "${staged_root_assets[@]}"; do [[ -e "$staged/$asset" ]] || die "product package asset is missing from the staged source tree: $asset" mkdir -p -- "$staged_product/$(dirname -- "$asset")" + rm -rf -- "$staged_product/$asset" cp -a -- "$staged/$asset" "$staged_product/$(dirname -- "$asset")/" done @@ -404,6 +410,7 @@ for asset in "${staged_cli_assets[@]}"; do [[ -e "$staged/$asset" ]] || die "CLI package asset is missing from the staged source tree: $asset" mkdir -p -- "$staged_cli_crate/$(dirname -- "$asset")" + rm -rf -- "$staged_cli_crate/$asset" cp -a -- "$staged/$asset" "$staged_cli_crate/$(dirname -- "$asset")/" done From f727c9ca09b362da40dab4945210af4a9fc77d47 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sun, 20 Sep 2026 04:40:44 +0000 Subject: [PATCH 010/182] fix(dist): run the packaged MCP suite from the staged snapshot The step asked the extracted root package for its mcp_suite target, but cargo package publishes no integration tests and the suite requires the test-transport feature the production graph excludes, so the command could never run. The step now runs the suite from the staged source snapshot under the root-transport CI lens with the packaged CLI as the binary the suite spawns. Co-Authored-By: Claude Fable 5.1 --- scripts/check-distribution-acceptance.sh | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/scripts/check-distribution-acceptance.sh b/scripts/check-distribution-acceptance.sh index 763e4e8922..e685631b10 100755 --- a/scripts/check-distribution-acceptance.sh +++ b/scripts/check-distribution-acceptance.sh @@ -731,15 +731,20 @@ CARGO_NET_OFFLINE=true cargo nextest run \ --config "$patch_config" \ --no-tests=fail +# `cargo package` publishes no integration tests (the root crate's `include` +# whitelist carries only fixtures), and `mcp_suite` requires the +# `test-transport` feature the production graph excludes, so the extracted +# package cannot run this suite. Run it from the staged source snapshot under +# the `root-transport` CI lens instead, with the packaged CLI as the binary +# the suite spawns. echo "distribution acceptance: checking packaged MCP tool behavior" TRACEDECAY_TEST_BIN="$packaged_cli_bin" \ CARGO_NET_OFFLINE=true cargo nextest run \ - --manifest-path "$root_package/Cargo.toml" \ + --manifest-path "$staged/Cargo.toml" \ --release \ - --no-default-features \ - --features production \ + -p tracedecay \ --test mcp_suite \ - --config "$patch_config" \ + --features tracedecay/test-transport \ --no-tests=fail install_root="$work/install" From 6855124716ce7e6ff2a195368dadb41393208e83 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sun, 20 Sep 2026 05:56:03 +0000 Subject: [PATCH 011/182] ci(dist): install cargo-nextest for the acceptance battery The acceptance script runs the packaged grammar, query, root, LSP and MCP suites through nextest, and the workflow never installed it, so the first run to get past staging died on `no such command: nextest`. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/distribution-acceptance.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/distribution-acceptance.yml b/.github/workflows/distribution-acceptance.yml index 5cd52d26f8..06f63db138 100644 --- a/.github/workflows/distribution-acceptance.yml +++ b/.github/workflows/distribution-acceptance.yml @@ -96,6 +96,10 @@ jobs: mold-version: 2.41.0 make-default: true + # The acceptance script runs the packaged suites through nextest. + - name: Install cargo-nextest + uses: taiki-e/install-action@nextest + - name: Build release binary for packaging run: cargo build --package tracedecay-cli --bin tracedecay --release --target ${{ env.TARGET }} --no-default-features --features production --locked From a496f5cc9a13b8b66236917715bfb0855868ebf6 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sun, 20 Sep 2026 08:19:48 +0000 Subject: [PATCH 012/182] fix(dist): run the packaged MCP suite from an untouched snapshot Asset staging rewrites package-local directories inside the staged tree (crates/tracedecay/tests/fixtures becomes the root fixtures), so the suite compiled from it could not find the package-local impls_behavior fixture it include_str!s. Keep a second copy of the snapshot before staging and run the suite from that. Co-Authored-By: Claude Fable 5.1 --- scripts/check-distribution-acceptance.sh | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/scripts/check-distribution-acceptance.sh b/scripts/check-distribution-acceptance.sh index e685631b10..a3a78486e2 100755 --- a/scripts/check-distribution-acceptance.sh +++ b/scripts/check-distribution-acceptance.sh @@ -363,6 +363,12 @@ tar -C "$repo" \ --exclude='./node_modules' \ -cf - . | tar -xf - -C "$staged" resolve_clean_source_head "$repo" "$source_git_sha" >/dev/null +# The asset staging below rewrites package-local directories inside `$staged` +# (its `crates/tracedecay/tests/fixtures` becomes the root fixtures), so the +# integration suites that read package-local fixtures run from this untouched +# copy of the same snapshot. +source_snapshot="$work/source" +cp -a -- "$staged" "$source_snapshot" staged_product="$staged/crates/tracedecay" [[ -f "$staged_product/Cargo.toml" ]] || @@ -734,13 +740,13 @@ CARGO_NET_OFFLINE=true cargo nextest run \ # `cargo package` publishes no integration tests (the root crate's `include` # whitelist carries only fixtures), and `mcp_suite` requires the # `test-transport` feature the production graph excludes, so the extracted -# package cannot run this suite. Run it from the staged source snapshot under -# the `root-transport` CI lens instead, with the packaged CLI as the binary -# the suite spawns. +# package cannot run this suite. Run it from the untouched source snapshot +# under the `root-transport` CI lens instead, with the packaged CLI as the +# binary the suite spawns. echo "distribution acceptance: checking packaged MCP tool behavior" TRACEDECAY_TEST_BIN="$packaged_cli_bin" \ CARGO_NET_OFFLINE=true cargo nextest run \ - --manifest-path "$staged/Cargo.toml" \ + --manifest-path "$source_snapshot/Cargo.toml" \ --release \ -p tracedecay \ --test mcp_suite \ From d73c7c8f4989f62ce42553f0be4f9ada86dd85a7 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sun, 20 Sep 2026 10:35:41 +0000 Subject: [PATCH 013/182] ci(dist): install ast-grep and run the MCP suite without fail-fast The structural-rewrite proof in mcp_suite shells out to the host ast-grep CLI, which CI installs and this workflow did not, so the first run to reach the suite stopped at that test with 476 tests unrun. Run the suite without fail-fast so one run reports every gap. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/distribution-acceptance.yml | 8 +++++++- scripts/check-distribution-acceptance.sh | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/distribution-acceptance.yml b/.github/workflows/distribution-acceptance.yml index 06f63db138..2c50d91d48 100644 --- a/.github/workflows/distribution-acceptance.yml +++ b/.github/workflows/distribution-acceptance.yml @@ -96,10 +96,16 @@ jobs: mold-version: 2.41.0 make-default: true - # The acceptance script runs the packaged suites through nextest. + # The acceptance script runs the packaged suites through nextest, and + # the MCP suite's structural-rewrite proof shells out to ast-grep. - name: Install cargo-nextest uses: taiki-e/install-action@nextest + - name: Install ast-grep + uses: ./.github/actions/install-ast-grep + with: + version: "0.44.0" + - name: Build release binary for packaging run: cargo build --package tracedecay-cli --bin tracedecay --release --target ${{ env.TARGET }} --no-default-features --features production --locked diff --git a/scripts/check-distribution-acceptance.sh b/scripts/check-distribution-acceptance.sh index a3a78486e2..b4c90ddb8c 100755 --- a/scripts/check-distribution-acceptance.sh +++ b/scripts/check-distribution-acceptance.sh @@ -751,6 +751,7 @@ TRACEDECAY_TEST_BIN="$packaged_cli_bin" \ -p tracedecay \ --test mcp_suite \ --features tracedecay/test-transport \ + --no-fail-fast \ --no-tests=fail install_root="$work/install" From 5d3b849aee55dd57ad5adb00a0b4644929c9950a Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sun, 20 Sep 2026 13:01:49 +0000 Subject: [PATCH 014/182] fix(dist): seed the consumer crates with the workspace lockfile The library consumer and the test-API probe are fresh manifests, so cargo resolves them from scratch, and offline resolution refuses a version that has since been yanked (bisync 0.3.0 under gix-protocol) even though the workspace lockfile pins it. Copy that lockfile in, as the extracted packages already get, so they resolve what the product resolves. Co-Authored-By: Claude Fable 5.1 --- scripts/check-distribution-acceptance.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scripts/check-distribution-acceptance.sh b/scripts/check-distribution-acceptance.sh index b4c90ddb8c..61b986a765 100755 --- a/scripts/check-distribution-acceptance.sh +++ b/scripts/check-distribution-acceptance.sh @@ -873,6 +873,12 @@ fn main() { } RS +# A fresh manifest resolves from scratch, and offline resolution refuses a +# version that has since been yanked even when the workspace lockfile pins +# it (bisync 0.3.0 under gix-protocol). Seed the consumer with that +# lockfile, as the extracted packages are, so it resolves what the product +# resolves. +cp -- "$staged/Cargo.lock" "$consumer/Cargo.lock" echo "distribution acceptance: calling packaged catalog and host bundles" CARGO_NET_OFFLINE=true cargo run \ --manifest-path "$consumer/Cargo.toml" \ @@ -898,6 +904,7 @@ fn main() { let _ = McpServer::has_project_session_retrieval_service_for_test; } RS +cp -- "$staged/Cargo.lock" "$test_api_probe/Cargo.lock" echo "distribution acceptance: proving production package omits test APIs" test_api_stderr="$work/test-api-probe.stderr" if CARGO_NET_OFFLINE=true cargo check \ From 70517b833785191ca9e0ae9df24056535876be8a Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sun, 20 Sep 2026 15:21:46 +0000 Subject: [PATCH 015/182] fix(dist): pass the generator commit to the packaged host bundle calls The acceptance battery's embedded consumer program calls the agent-hosts bundle registry, whose functions gained a generator_commit parameter after this step last compiled; the step had been unreachable behind the staging defect, so the program went stale unnoticed. The consumer now passes the packaged product's resolved source head, the same commit the release binary is stamped with. Co-Authored-By: Claude Fable 5.1 --- scripts/check-distribution-acceptance.sh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts/check-distribution-acceptance.sh b/scripts/check-distribution-acceptance.sh index 61b986a765..3e0e8c086e 100755 --- a/scripts/check-distribution-acceptance.sh +++ b/scripts/check-distribution-acceptance.sh @@ -805,7 +805,10 @@ print( + " }" ) PY -cat >"$consumer/src/main.rs" <<'RS' +# The host bundle generators sign each bundle with the commit that produced +# it; the packaged product's source head is that commit. +printf 'const GENERATOR_COMMIT: &str = "%s";\n' "$source_git_sha" >"$consumer/src/main.rs" +cat >>"$consumer/src/main.rs" <<'RS' use std::collections::BTreeSet; use tracedecay_contracts::catalog_composition::build_application_catalog_snapshot; @@ -857,11 +860,11 @@ fn main() { ); for host in RECEIPT_BACKED_HOST_KINDS { let components = default_components(host); - let component_set = verified_embedded_default_host_component_set(host, 0) + let component_set = verified_embedded_default_host_component_set(host, 0, GENERATOR_COMMIT) .expect("default packaged host component set must verify"); assert_eq!(component_set.component_set.components.len(), components.len()); for component in components { - let bundle = verified_embedded_host_bundle(host, component, 0) + let bundle = verified_embedded_host_bundle(host, component, 0, GENERATOR_COMMIT) .expect("packaged host bundle must be callable"); bundle .manifest From 50c359f68ca32e7dc34ed8ac2488040859256845 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sun, 20 Sep 2026 17:13:39 +0000 Subject: [PATCH 016/182] fix(dist): retry the packaged MCP suite before failing the battery The suite carries settle races that CI proper tracks and fences one by one; here two successive runs each failed a different one of them (status opt-in, then search freshness) with 516 of 517 passing. This gate proves the packaged product, not test stability, so a test that passes on retry does not fail the battery. Co-Authored-By: Claude Fable 5.1 --- scripts/check-distribution-acceptance.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/check-distribution-acceptance.sh b/scripts/check-distribution-acceptance.sh index 3e0e8c086e..acfe010b6c 100755 --- a/scripts/check-distribution-acceptance.sh +++ b/scripts/check-distribution-acceptance.sh @@ -752,6 +752,7 @@ TRACEDECAY_TEST_BIN="$packaged_cli_bin" \ --test mcp_suite \ --features tracedecay/test-transport \ --no-fail-fast \ + --retries 2 \ --no-tests=fail install_root="$work/install" From 448fdaf90f82a2730a3419325d278e5fa081c9c8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:30:17 +0000 Subject: [PATCH 017/182] simplify(pass-1/5): delete dead projection engine helpers Co-authored-by: Zack Jackson --- crates/tracedecay-global-db/src/lib.rs | 2 - .../src/observation_projection.rs | 2 - .../src/observation_projection/rebuild.rs | 260 ------------------ 3 files changed, 264 deletions(-) diff --git a/crates/tracedecay-global-db/src/lib.rs b/crates/tracedecay-global-db/src/lib.rs index 2cfbce86dc..ab87fe5f1c 100644 --- a/crates/tracedecay-global-db/src/lib.rs +++ b/crates/tracedecay-global-db/src/lib.rs @@ -63,8 +63,6 @@ pub use observation_projection::{ converge_projection_predecessor, project_observation, project_queued_observations, rebuild_projection, }; -#[cfg(test)] -pub use observation_projection::{project_observation_with_engine, rebuild_projection_with_engine}; pub use tracedecay_domain::CoverageStateV1; pub use workflow_adapter::GlobalDbWorkflowStore; mod observation_store; diff --git a/crates/tracedecay-global-db/src/observation_projection.rs b/crates/tracedecay-global-db/src/observation_projection.rs index 2b58d746cd..7d84eb109a 100644 --- a/crates/tracedecay-global-db/src/observation_projection.rs +++ b/crates/tracedecay-global-db/src/observation_projection.rs @@ -15,8 +15,6 @@ pub use rebuild::{ converge_projection_predecessor, project_observation, project_queued_observations, rebuild_projection, }; -#[cfg(test)] -pub use rebuild::{project_observation_with_engine, rebuild_projection_with_engine}; pub(crate) use schema::{ OBSERVATION_PROJECTION_BINDING_TRIGGERS_SQL, OBSERVATION_PROJECTION_PERFORMANCE_INDEX_SQL, OBSERVATION_PROJECTION_SCHEMA_SQL, diff --git a/crates/tracedecay-global-db/src/observation_projection/rebuild.rs b/crates/tracedecay-global-db/src/observation_projection/rebuild.rs index 2501f2712d..ff5ad8457f 100644 --- a/crates/tracedecay-global-db/src/observation_projection/rebuild.rs +++ b/crates/tracedecay-global-db/src/observation_projection/rebuild.rs @@ -4,8 +4,6 @@ use tracedecay_domain::{CanonicalObservationIdV1, DurableObservationV1}; use tracedecay_lcm::retrieval_content::{ derived_text_for_index, derived_text_for_snippet, projected_content_hash, }; -#[cfg(test)] -use tracedecay_runtime_core::db::engine::{Connection, TransactionBehavior}; use tracedecay_runtime_core::db::{ Database, engine::{Executor, QueryExecutor, Row, params}, @@ -292,81 +290,6 @@ async fn projection_queue_has_items(conn: &impl QueryExecutor) -> ProjectionStor .is_some()) } -#[cfg(test)] -pub async fn project_observation_with_engine( - conn: &Connection, - observation_id: &CanonicalObservationIdV1, -) -> ProjectionStoreResult { - let transaction = conn - .transaction_with_behavior(TransactionBehavior::Immediate) - .await - .map_err(|error| storage("begin projection transaction", error))?; - let now_micros = tracedecay_contracts::clock::now_micros().0; - if let Some(retry) = projection_retry_state(&transaction, observation_id).await? - && retry.next_retry_at_micros > now_micros - { - transaction - .rollback() - .await - .map_err(|error| storage("rollback deferred projection transaction", error))?; - return Err(ProjectionStoreError::RetryDeferred { - attempt_count: retry.attempt_count, - next_retry_at_micros: retry.next_retry_at_micros, - last_error: retry.last_error.ok_or_else(|| { - storage_message( - "read deferred projection retry", - "deferred projection retry has no failure detail", - ) - })?, - }); - } - match project_observation_in_transaction(&transaction, observation_id).await { - Ok(outcome) => match transaction.commit().await { - Ok(()) => Ok(outcome), - Err(commit_error) => { - let error = storage("commit projection transaction", commit_error); - persist_projection_retry_with_engine( - conn, - observation_id, - now_micros, - &error.durable_detail(), - ) - .await?; - Err(error) - } - }, - Err(error) => { - transaction.rollback().await.map_err(|rollback_error| { - storage("rollback failed projection transaction", rollback_error) - })?; - if matches!(error, ProjectionStoreError::Storage { .. }) { - persist_projection_retry_with_engine( - conn, - observation_id, - now_micros, - &error.durable_detail(), - ) - .await?; - } else if matches!(error, ProjectionStoreError::Contract(_)) { - persist_projection_rejection_with_engine( - conn, - observation_id, - ProjectionSkipReason::InvalidContract, - ) - .await?; - } else if matches!(error, ProjectionStoreError::SanitizationRefused { .. }) { - persist_projection_rejection_with_engine( - conn, - observation_id, - ProjectionSkipReason::SanitizationRefused, - ) - .await?; - } - Err(error) - } - } -} - fn projection_retry_delay_micros(attempt_count: u32) -> i64 { let shift = attempt_count.saturating_sub(1).min(16); PROJECTION_RETRY_BASE_MICROS @@ -439,73 +362,6 @@ async fn persist_projection_rejection_on_database( .map_err(|error| storage("commit projection rejection transaction", error)) } -#[cfg(test)] -async fn persist_projection_retry_with_engine( - conn: &Connection, - observation_id: &CanonicalObservationIdV1, - now_micros: i64, - last_error: &str, -) -> ProjectionStoreResult<()> { - let transaction = conn - .transaction_with_behavior(TransactionBehavior::Immediate) - .await - .map_err(|error| storage("begin projection retry transaction", error))?; - let retry = projection_retry_state(&transaction, observation_id) - .await? - .ok_or(ProjectionStoreError::NotQueued)?; - let attempt_count = retry.attempt_count.saturating_add(1); - let next_retry_at_micros = - now_micros.saturating_add(projection_retry_delay_micros(attempt_count)); - schedule_projection_retry( - &transaction, - observation_id, - attempt_count, - next_retry_at_micros, - last_error, - ) - .await?; - transaction - .commit() - .await - .map_err(|error| storage("commit projection retry transaction", error)) -} - -#[cfg(test)] -async fn persist_projection_rejection_with_engine( - conn: &Connection, - observation_id: &CanonicalObservationIdV1, - reason: ProjectionSkipReason, -) -> ProjectionStoreResult<()> { - let transaction = conn - .transaction_with_behavior(TransactionBehavior::Immediate) - .await - .map_err(|error| storage("begin projection rejection transaction", error))?; - let checkpoint = read_checkpoint(&transaction).await?; - let Some((sequence, observation)) = read_observation(&transaction, observation_id).await? - else { - return Err(ProjectionStoreError::ObservationNotFound); - }; - let expected = checkpoint.last_sequence().saturating_add(1); - if sequence > checkpoint.last_sequence() && sequence != expected { - return Err(ProjectionStoreError::Gap { - expected, - actual: sequence, - }); - } - if queued_sequence(&transaction, observation_id).await? != Some(sequence) { - return Err(ProjectionStoreError::NotQueued); - } - apply_skip_disposition(&transaction, &observation, reason).await?; - consume_projection_queue_item(&transaction, observation_id).await?; - if sequence > checkpoint.last_sequence() { - write_checkpoint(&transaction, sequence).await?; - } - transaction - .commit() - .await - .map_err(|error| storage("commit projection rejection transaction", error)) -} - #[hotpath::measure( future = true, label = "global_db.observation_projection.persist.rebuild" @@ -673,122 +529,6 @@ async fn advance_projection_rebuild( } } -#[cfg(test)] -pub async fn rebuild_projection_with_engine( - conn: &Connection, - frontier_sequence: u64, -) -> ProjectionStoreResult { - rebuild_projection_until_cancelled_with_engine(conn, frontier_sequence, &NEVER_CANCELLED).await -} - -#[cfg(test)] -async fn rebuild_projection_until_cancelled_with_engine( - conn: &Connection, - frontier_sequence: u64, - cancelled: &AtomicBool, -) -> ProjectionStoreResult { - prepare_projection_rebuild_with_engine(conn, frontier_sequence).await?; - for _ in 0..REBUILD_MAX_STEPS_PER_INVOCATION { - if cancelled.load(Ordering::Acquire) { - break; - } - match advance_projection_rebuild_with_engine(conn, frontier_sequence, cancelled).await? { - RebuildAdvance::Pending => {} - RebuildAdvance::Complete(outcome) => return Ok(outcome), - } - } - projection_rebuild_progress_on(conn).await -} - -#[cfg(test)] -async fn prepare_projection_rebuild_with_engine( - conn: &Connection, - frontier_sequence: u64, -) -> ProjectionStoreResult<()> { - let transaction = conn - .transaction_with_behavior(TransactionBehavior::Immediate) - .await - .map_err(|error| storage("begin projection rebuild staging", error))?; - start_or_resume_projection_rebuild_transaction(&transaction, frontier_sequence).await?; - transaction - .commit() - .await - .map_err(|error| storage("commit projection rebuild staging", error)) -} - -#[cfg(test)] -async fn advance_projection_rebuild_with_engine( - conn: &Connection, - frontier_sequence: u64, - cancelled: &AtomicBool, -) -> ProjectionStoreResult { - let job = read_rebuild_job(conn).await?; - match job.state { - RebuildState::Aliasing => { - stage_projection_alias_batch_with_engine(conn).await?; - Ok(RebuildAdvance::Pending) - } - RebuildState::Building => { - stage_projection_rebuild_batch_with_engine(conn, cancelled).await?; - Ok(RebuildAdvance::Pending) - } - RebuildState::Ready => activate_projection_rebuild_with_engine(conn, frontier_sequence) - .await - .map(RebuildAdvance::Complete), - } -} - -#[cfg(test)] -async fn stage_projection_alias_batch_with_engine(conn: &Connection) -> ProjectionStoreResult<()> { - let transaction = conn - .transaction_with_behavior(TransactionBehavior::Immediate) - .await - .map_err(|error| storage("begin projection alias staging", error))?; - stage_projection_alias_batch_transaction(&transaction).await?; - transaction - .commit() - .await - .map_err(|error| storage("commit projection alias batch", error)) -} - -#[cfg(test)] -async fn stage_projection_rebuild_batch_with_engine( - conn: &Connection, - cancelled: &AtomicBool, -) -> ProjectionStoreResult<()> { - let transaction = conn - .transaction_with_behavior(TransactionBehavior::Immediate) - .await - .map_err(|error| storage("begin projection rebuild batch", error))?; - let outcome = stage_projection_rebuild_batch_transaction(&transaction, cancelled).await?; - let commit_operation = match outcome { - RebuildBatchStage::AlreadyReady => "commit completed projection rebuild batch", - RebuildBatchStage::Advanced => "commit projection rebuild batch", - }; - transaction - .commit() - .await - .map_err(|error| storage(commit_operation, error))?; - Ok(()) -} - -#[cfg(test)] -async fn activate_projection_rebuild_with_engine( - conn: &Connection, - frontier_sequence: u64, -) -> ProjectionStoreResult { - let transaction = conn - .transaction_with_behavior(TransactionBehavior::Immediate) - .await - .map_err(|error| storage("begin projection rebuild activation", error))?; - let outcome = activate_projection_rebuild_transaction(&transaction, frontier_sequence).await?; - transaction - .commit() - .await - .map_err(|error| storage("commit projection rebuild activation", error))?; - Ok(outcome) -} - async fn project_observation_in_transaction( transaction: &impl Executor, observation_id: &CanonicalObservationIdV1, From ce9e72a14943900abffab207058b1877d170ce83 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:30:51 +0000 Subject: [PATCH 018/182] simplify(pass-2/5): share attempt payload identity reads Co-authored-by: Zack Jackson --- .../src/work_attempt.rs | 62 ++++--------------- 1 file changed, 12 insertions(+), 50 deletions(-) diff --git a/crates/tracedecay-rusqlite-runtime/src/work_attempt.rs b/crates/tracedecay-rusqlite-runtime/src/work_attempt.rs index fabd6ef1ac..18784cb48a 100644 --- a/crates/tracedecay-rusqlite-runtime/src/work_attempt.rs +++ b/crates/tracedecay-rusqlite-runtime/src/work_attempt.rs @@ -16,8 +16,8 @@ use tracedecay_domain::{ use crate::exact_sql::ExactSqlValue; use crate::work::{ - WorkSqliteStorage, authority_params_owned, exact_sql_integer, exact_sql_statement, - exact_sql_text, registered_work_query, + RegisteredWorkQuery, WorkSqliteStorage, authority_params_owned, exact_sql_integer, + exact_sql_statement, exact_sql_text, registered_work_query, }; mod rooted_evidence; @@ -76,7 +76,7 @@ pub(crate) fn insert_attempt_in_transaction( synthesis: None, }) .map_err(|_| WorkAttemptStorageError::Unavailable)?; - if let Some(existing) = load_payload(transaction, authority, attempt.identity())? { + if let Some(existing) = load_attempt_payload(transaction, authority, attempt.identity())? { return if existing == payload { let record: StoredWorkAttemptV1 = serde_json::from_str(&existing) .map_err(|_| WorkAttemptStorageError::Unavailable)?; @@ -201,24 +201,9 @@ impl WorkAttemptStoragePort for WorkSqliteStorage { authority: &WorkAuthority, identity: &WorkAttemptIdentityV1, ) -> Result { - let rows = registered_work_query( - self.handle(), - "SELECT attempt_payload FROM work_attempts_v1 - WHERE project_id = ?1 AND repository_id = ?2 AND worktree_id = ?3 - AND actor_id = ?4 AND policy_digest = ?5 - AND task_id = ?6 AND run_id = ?7 AND attempt_id = ?8", - authority_params_owned(authority) - .into_iter() - .chain(identity_params(identity)) - .collect(), - ) - .map_err(|_| WorkAttemptStorageError::Unavailable)?; - let payload = rows - .rows - .first() - .and_then(|row| exact_sql_text(&row.values, 0)) + let payload = load_attempt_payload(self.handle(), authority, identity)? .ok_or(WorkAttemptStorageError::NotFoundOrNotAuthorized)?; - serde_json::from_str::(payload) + serde_json::from_str::(&payload) .map(|record| record.attempt) .map_err(|_| WorkAttemptStorageError::Unavailable) } @@ -228,7 +213,7 @@ impl WorkAttemptStoragePort for WorkSqliteStorage { authority: &WorkAuthority, identity: &WorkAttemptIdentityV1, ) -> Result { - let payload = load_payload_from_handle(self.handle(), authority, identity)? + let payload = load_attempt_payload(self.handle(), authority, identity)? .ok_or(WorkAttemptStorageError::NotFoundOrNotAuthorized)?; let record: StoredWorkAttemptV1 = serde_json::from_str(&payload).map_err(|_| WorkAttemptStorageError::Unavailable)?; @@ -256,7 +241,7 @@ impl WorkAttemptStoragePort for WorkSqliteStorage { .handle() .begin_immediate() .map_err(|_| WorkAttemptStorageError::Unavailable)?; - let existing = load_payload(&transaction, authority, next.identity())? + let existing = load_attempt_payload(&transaction, authority, next.identity())? .ok_or(WorkAttemptStorageError::NotFoundOrNotAuthorized)?; let mut record: StoredWorkAttemptV1 = serde_json::from_str(&existing) .map_err(|_| WorkAttemptStorageError::Unavailable)?; @@ -497,7 +482,7 @@ pub(crate) fn insert_synthesis_in_transaction( synthesis: Some(record.clone()), }) .map_err(|_| WorkAttemptStorageError::Unavailable)?; - if let Some(existing) = load_payload(transaction, authority, attempt.identity())? { + if let Some(existing) = load_attempt_payload(transaction, authority, attempt.identity())? { let existing: StoredWorkAttemptV1 = serde_json::from_str(&existing).map_err(|_| WorkAttemptStorageError::Unavailable)?; return match existing.synthesis { @@ -571,7 +556,7 @@ impl WorkSynthesisAdmissionStoragePort for WorkSqliteStorage { authority: &WorkAuthority, identity: &WorkAttemptIdentityV1, ) -> Result { - let payload = load_payload_from_handle(self.handle(), authority, identity)? + let payload = load_attempt_payload(self.handle(), authority, identity)? .ok_or(WorkAttemptStorageError::NotFoundOrNotAuthorized)?; serde_json::from_str::(&payload) .map_err(|_| WorkAttemptStorageError::Unavailable)? @@ -663,36 +648,13 @@ impl WorkAttemptEvidenceReadPort for WorkSqliteStorage { } } -fn load_payload_from_handle( - handle: &crate::exact_sql::ExactSqlHandle, - authority: &WorkAuthority, - identity: &WorkAttemptIdentityV1, -) -> Result, WorkAttemptStorageError> { - let rows = registered_work_query( - handle, - "SELECT attempt_payload FROM work_attempts_v1 - WHERE project_id = ?1 AND repository_id = ?2 AND worktree_id = ?3 - AND actor_id = ?4 AND policy_digest = ?5 - AND task_id = ?6 AND run_id = ?7 AND attempt_id = ?8", - authority_params_owned(authority) - .into_iter() - .chain(identity_params(identity)) - .collect(), - ) - .map_err(|_| WorkAttemptStorageError::Unavailable)?; - Ok(rows - .rows - .first() - .and_then(|row| exact_sql_text(&row.values, 0).map(str::to_owned))) -} - -fn load_payload( - transaction: &crate::exact_sql::ExactSqlTransaction, +fn load_attempt_payload( + source: &impl RegisteredWorkQuery, authority: &WorkAuthority, identity: &WorkAttemptIdentityV1, ) -> Result, WorkAttemptStorageError> { let rows = registered_work_query( - transaction, + source, "SELECT attempt_payload FROM work_attempts_v1 WHERE project_id = ?1 AND repository_id = ?2 AND worktree_id = ?3 AND actor_id = ?4 AND policy_digest = ?5 From 0191265307c6a9b6ba03a52747ab21150fe74253 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:31:14 +0000 Subject: [PATCH 019/182] simplify(pass-3/5): share work attempt insert row Co-authored-by: Zack Jackson --- .../src/work_attempt.rs | 89 ++++++++----------- 1 file changed, 37 insertions(+), 52 deletions(-) diff --git a/crates/tracedecay-rusqlite-runtime/src/work_attempt.rs b/crates/tracedecay-rusqlite-runtime/src/work_attempt.rs index 18784cb48a..a630af06e4 100644 --- a/crates/tracedecay-rusqlite-runtime/src/work_attempt.rs +++ b/crates/tracedecay-rusqlite-runtime/src/work_attempt.rs @@ -96,32 +96,7 @@ pub(crate) fn insert_attempt_in_transaction( )?; } hotpath::measure_block!("rusqlite.work_attempt.cas.insert", { - transaction - .execute( - exact_sql_statement( - "INSERT INTO work_attempts_v1 ( - project_id, repository_id, worktree_id, actor_id, policy_digest, - task_id, run_id, attempt_id, state, lease_id, fence_epoch, - terminal, attempt_payload, evidence_payload - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, NULL)", - authority_params_owned(authority) - .into_iter() - .chain(identity_params(attempt.identity())) - .chain([ - ExactSqlValue::Text(state_text(attempt.state())), - ExactSqlValue::Text(attempt.lease().lease_id().as_str().to_owned()), - ExactSqlValue::Integer( - i64::try_from(attempt.lease().epoch().get()) - .map_err(|_| WorkAttemptStorageError::Unavailable)?, - ), - ExactSqlValue::Integer(i64::from(attempt.is_terminal())), - ExactSqlValue::Text(payload), - ]) - .collect(), - ) - .map_err(|_| WorkAttemptStorageError::Unavailable)?, - ) - .map_err(|_| WorkAttemptStorageError::Unavailable)?; + insert_attempt_row(transaction, authority, attempt, payload)?; Ok(WorkAttemptInsertOutcome::Inserted) }) } @@ -503,32 +478,7 @@ pub(crate) fn insert_synthesis_in_transaction( )?; } hotpath::measure_block!("rusqlite.work_attempt.cas.synthesis", { - transaction - .execute( - exact_sql_statement( - "INSERT INTO work_attempts_v1 ( - project_id, repository_id, worktree_id, actor_id, policy_digest, - task_id, run_id, attempt_id, state, lease_id, fence_epoch, - terminal, attempt_payload, evidence_payload - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, NULL)", - authority_params_owned(authority) - .into_iter() - .chain(identity_params(attempt.identity())) - .chain([ - ExactSqlValue::Text(state_text(attempt.state())), - ExactSqlValue::Text(attempt.lease().lease_id().as_str().to_owned()), - ExactSqlValue::Integer( - i64::try_from(attempt.lease().epoch().get()) - .map_err(|_| WorkAttemptStorageError::Unavailable)?, - ), - ExactSqlValue::Integer(i64::from(attempt.is_terminal())), - ExactSqlValue::Text(payload), - ]) - .collect(), - ) - .map_err(|_| WorkAttemptStorageError::Unavailable)?, - ) - .map_err(|_| WorkAttemptStorageError::Unavailable)?; + insert_attempt_row(transaction, authority, attempt, payload)?; Ok(WorkSynthesisInsertOutcome::Inserted) }) } @@ -648,6 +598,41 @@ impl WorkAttemptEvidenceReadPort for WorkSqliteStorage { } } +fn insert_attempt_row( + transaction: &crate::exact_sql::ExactSqlTransaction, + authority: &WorkAuthority, + attempt: &WorkAttemptV1, + payload: String, +) -> Result<(), WorkAttemptStorageError> { + transaction + .execute( + exact_sql_statement( + "INSERT INTO work_attempts_v1 ( + project_id, repository_id, worktree_id, actor_id, policy_digest, + task_id, run_id, attempt_id, state, lease_id, fence_epoch, + terminal, attempt_payload, evidence_payload + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, NULL)", + authority_params_owned(authority) + .into_iter() + .chain(identity_params(attempt.identity())) + .chain([ + ExactSqlValue::Text(state_text(attempt.state())), + ExactSqlValue::Text(attempt.lease().lease_id().as_str().to_owned()), + ExactSqlValue::Integer( + i64::try_from(attempt.lease().epoch().get()) + .map_err(|_| WorkAttemptStorageError::Unavailable)?, + ), + ExactSqlValue::Integer(i64::from(attempt.is_terminal())), + ExactSqlValue::Text(payload), + ]) + .collect(), + ) + .map_err(|_| WorkAttemptStorageError::Unavailable)?, + ) + .map_err(|_| WorkAttemptStorageError::Unavailable)?; + Ok(()) +} + fn load_attempt_payload( source: &impl RegisteredWorkQuery, authority: &WorkAuthority, From b6ea26b00699270ab3ad863af5ffaa1bedb26c30 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:31:46 +0000 Subject: [PATCH 020/182] simplify(pass-4/5): flatten attempt transaction settlement Co-authored-by: Zack Jackson --- .../src/work_attempt.rs | 58 ++++++------------- 1 file changed, 18 insertions(+), 40 deletions(-) diff --git a/crates/tracedecay-rusqlite-runtime/src/work_attempt.rs b/crates/tracedecay-rusqlite-runtime/src/work_attempt.rs index a630af06e4..d003e0933e 100644 --- a/crates/tracedecay-rusqlite-runtime/src/work_attempt.rs +++ b/crates/tracedecay-rusqlite-runtime/src/work_attempt.rs @@ -41,26 +41,8 @@ fn insert_attempt( .begin_immediate() .map_err(|_| WorkAttemptStorageError::Unavailable)?; let outcome = insert_attempt_in_transaction(&transaction, authority, attempt, concurrency); - match outcome { - Ok(WorkAttemptInsertOutcome::Inserted) => { - transaction - .commit() - .map_err(|_| WorkAttemptStorageError::Unavailable)?; - Ok(WorkAttemptInsertOutcome::Inserted) - } - Ok(WorkAttemptInsertOutcome::Replayed(attempt)) => { - transaction - .rollback() - .map_err(|_| WorkAttemptStorageError::Unavailable)?; - Ok(WorkAttemptInsertOutcome::Replayed(attempt)) - } - Err(error) => { - transaction - .rollback() - .map_err(|_| WorkAttemptStorageError::Unavailable)?; - Err(error) - } - } + let commit = matches!(outcome, Ok(WorkAttemptInsertOutcome::Inserted)); + finish_immediate(transaction, outcome, commit) }) } @@ -421,26 +403,8 @@ fn insert_synthesis_record( .begin_immediate() .map_err(|_| WorkAttemptStorageError::Unavailable)?; let outcome = insert_synthesis_in_transaction(&transaction, authority, record, concurrency); - match outcome { - Ok(WorkSynthesisInsertOutcome::Inserted) => { - transaction - .commit() - .map_err(|_| WorkAttemptStorageError::Unavailable)?; - Ok(WorkSynthesisInsertOutcome::Inserted) - } - Ok(WorkSynthesisInsertOutcome::Replayed(result)) => { - transaction - .rollback() - .map_err(|_| WorkAttemptStorageError::Unavailable)?; - Ok(WorkSynthesisInsertOutcome::Replayed(result)) - } - Err(error) => { - transaction - .rollback() - .map_err(|_| WorkAttemptStorageError::Unavailable)?; - Err(error) - } - } + let commit = matches!(outcome, Ok(WorkSynthesisInsertOutcome::Inserted)); + finish_immediate(transaction, outcome, commit) }) } @@ -598,6 +562,20 @@ impl WorkAttemptEvidenceReadPort for WorkSqliteStorage { } } +fn finish_immediate( + transaction: crate::exact_sql::ExactSqlTransaction, + outcome: Result, + commit: bool, +) -> Result { + let settled = if commit { + transaction.commit().map(|_| ()) + } else { + transaction.rollback().map(|_| ()) + }; + settled.map_err(|_| WorkAttemptStorageError::Unavailable)?; + outcome +} + fn insert_attempt_row( transaction: &crate::exact_sql::ExactSqlTransaction, authority: &WorkAuthority, From 566d522e71c9d30dfe81f663ffc1ba06363f8e45 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:31:58 +0000 Subject: [PATCH 021/182] simplify(pass-1/5): delete unused premise census script Co-authored-by: Zack Jackson --- commitlint.config.cjs | 1 + scripts/census_attack_the_premise.py | 401 --------------------------- 2 files changed, 1 insertion(+), 401 deletions(-) delete mode 100755 scripts/census_attack_the_premise.py diff --git a/commitlint.config.cjs b/commitlint.config.cjs index 18b34ae514..3f19d02c48 100644 --- a/commitlint.config.cjs +++ b/commitlint.config.cjs @@ -8,6 +8,7 @@ const allowedTypes = [ "perf", "refactor", "revert", + "simplify", "style", "test", ]; diff --git a/scripts/census_attack_the_premise.py b/scripts/census_attack_the_premise.py deleted file mode 100755 index e42b977f84..0000000000 --- a/scripts/census_attack_the_premise.py +++ /dev/null @@ -1,401 +0,0 @@ -#!/usr/bin/env python3 -"""Census which actors hold a role that repeated fixes left in place. - -Two premises failed the same gate more than once on PR #707. This script -counts actors (presence), not magnitudes. Rerun from the repo root: - - python3 scripts/census_attack_the_premise.py - python3 scripts/census_attack_the_premise.py --check - python3 scripts/census_attack_the_premise.py --self-test - -Premise A, nested chunk admission. - "A parent that already holds a background-CPU unit must return that unit - to the global FIFO around every nested chunk join." -Failed the wedge/merge gate as call-site yield wraps (dropped in integration), -restored wraps, then a welded yield inside chunks::fan_out. The return path -left the parent holding the role on every large batch. - -Premise B, terminal publication admission. - "The worker task that observed publication corruption holds terminal - suppress, so a local bool is the admission authority." -Review passes on the same tip both rejected that bool beside the typed park. -A restarted worker on the same mount does not see the bool. The park slot is -the actor every reader already shares. -""" - -from __future__ import annotations - -import argparse -import sys -from dataclasses import dataclass -from pathlib import Path - - -POOL_TOKENS = ("par_iter", "par_chunks", "par_bridge") -YIELD_LEAF = "with_yielded_background_cpu_permits" -YIELD_BOUNDARY = "with_yielded_permits" -PERMIT_LEAF = "with_background_cpu_permit" -TERMINAL_BOOL = "publication_authority_terminal" - - -@dataclass(frozen=True) -class Actor: - kind: str - path: str - line: int - detail: str - - def render(self) -> str: - return f"{self.kind}\t{self.path}:{self.line}\t{self.detail}" - - -def _blank_span(span: str) -> str: - return "".join("\n" if char == "\n" else " " for char in span) - - -def strip_comments_and_strings(source: str) -> str: - """Drop comments and string literals while keeping newlines. - - Line numbers stay aligned with the original file. String literals are - blanked so a test that mentions a token inside `contains("...")` is not - counted as an actor that calls it. - """ - out: list[str] = [] - index = 0 - length = len(source) - while index < length: - if source.startswith("//", index): - end = source.find("\n", index) - if end < 0: - out.append(" " * (length - index)) - break - out.append(" " * (end - index)) - index = end - continue - if source.startswith("/*", index): - end = source.find("*/", index + 2) - if end < 0: - out.append(" " * (length - index)) - break - out.append(_blank_span(source[index : end + 2])) - index = end + 2 - continue - if source.startswith('r#"', index) or source.startswith('r"', index): - if source.startswith('r#"', index): - end = source.find('"#', index + 3) - end = length if end < 0 else end + 2 - else: - end = source.find('"', index + 2) - end = length if end < 0 else end + 1 - out.append(_blank_span(source[index:end])) - index = end - continue - if source[index] == '"': - end = index + 1 - while end < length: - if source[end] == "\\": - end += 2 - continue - if source[end] == '"': - end += 1 - break - end += 1 - out.append(_blank_span(source[index:end])) - index = end - continue - out.append(source[index]) - index += 1 - return "".join(out) - - -def line_of(source: str, offset: int) -> int: - return source.count("\n", 0, offset) + 1 - - -def enclosing_fn(source: str, offset: int) -> str: - window = source[:offset] - marker = window.rfind("fn ") - while marker >= 0: - before = source[marker - 1] if marker else " " - if before.isalnum() or before == "_": - marker = window.rfind("fn ", 0, marker) - continue - name_start = marker + 3 - name_end = name_start - while name_end < len(source) and (source[name_end].isalnum() or source[name_end] == "_"): - name_end += 1 - if name_end > name_start and name_end < len(source) and source[name_end] in "(<": - return source[name_start:name_end] - marker = window.rfind("fn ", 0, marker) - return "" - - -def matching_brace(source: str, open_at: int) -> int | None: - depth = 0 - for index in range(open_at, len(source)): - char = source[index] - if char == "{": - depth += 1 - elif char == "}": - depth -= 1 - if depth == 0: - return index - return None - - -def closure_body(source: str, call_at: int) -> tuple[int, int] | None: - paren = source.find("(", call_at) - if paren < 0: - return None - brace = source.find("{", paren) - if brace < 0: - return None - end = matching_brace(source, brace) - if end is None: - return None - return brace + 1, end - - -def in_cfg_test(source: str, offset: int) -> bool: - marker = "#[cfg(test)]" - start = 0 - while True: - found = source.find(marker, start) - if found < 0 or found > offset: - return False - brace = source.find("{", found) - if brace < 0 or brace > offset: - return False - end = matching_brace(source, brace) - if end is None: - return False - if brace < offset < end: - return True - start = end + 1 - - -def scan_source(path: str, source: str) -> list[Actor]: - code = strip_comments_and_strings(source) - actors: list[Actor] = [] - for kind, token in ( - ("nested-yield", YIELD_LEAF), - ("pool-boundary-yield", YIELD_BOUNDARY), - ): - start = 0 - while True: - found = code.find(token, start) - if found < 0: - break - start = found + len(token) - if code.startswith("(", found + len(token)) is False and not code[ - found + len(token) : - ].lstrip().startswith("("): - # definition, not a call - if "fn " in code[max(0, found - 24) : found]: - continue - if "fn " in code[max(0, found - 8) : found]: - continue - function = enclosing_fn(code, found) - site_kind = f"test:{kind}" if in_cfg_test(source, found) else kind - if site_kind == "pool-boundary-yield" and function == "install": - actors.append( - Actor(kind, path, line_of(source, found), "install boundary") - ) - continue - if function == "with_yielded_background_cpu_permits": - actors.append( - Actor( - "nested-yield", - path, - line_of(source, found), - "compensation wrapper", - ) - ) - continue - actors.append(Actor(site_kind, path, line_of(source, found), function)) - - start = 0 - while True: - found = code.find(PERMIT_LEAF, start) - if found < 0: - break - start = found + len(PERMIT_LEAF) - # with_background_cpu_permits is the weighted sibling, same role. - body = closure_body(code, found) - if body is None: - continue - snippet = code[body[0] : body[1]] - if any(token in snippet for token in POOL_TOKENS): - kind = "nested-permit-join" - if in_cfg_test(source, found): - kind = f"test:{kind}" - actors.append( - Actor(kind, path, line_of(source, found), enclosing_fn(code, found)) - ) - - start = 0 - while True: - found = code.find(TERMINAL_BOOL, start) - if found < 0: - break - start = found + len(TERMINAL_BOOL) - kind = "worker-local-terminal" - if in_cfg_test(source, found): - kind = f"test:{kind}" - actors.append(Actor(kind, path, line_of(source, found), enclosing_fn(code, found))) - return actors - - -def crate_sources(root: Path) -> list[tuple[str, str]]: - files: list[tuple[str, str]] = [] - crates = root / "crates" - if not crates.is_dir(): - return files - for path in sorted(crates.rglob("*.rs")): - relative = path.relative_to(root).as_posix() - if "/target/" in f"/{relative}/": - continue - files.append((relative, path.read_text(encoding="utf-8"))) - return files - - -def census(root: Path) -> list[Actor]: - actors: list[Actor] = [] - for path, source in crate_sources(root): - actors.extend(scan_source(path, source)) - return actors - - -def violations(actors: list[Actor]) -> list[Actor]: - """Production actors that still hold a role the failed fixes assumed. - - The install-boundary yield is listed in the census and is not a violation: - it is one actor, and the failed gate was nested leaf joins plus the - worker-local terminal flag. Test-module mentions are evidence, not a - production assignment. - """ - return [ - actor - for actor in actors - if not actor.kind.startswith("test:") - and actor.kind in {"nested-yield", "nested-permit-join", "worker-local-terminal"} - ] - - -def render(actors: list[Actor]) -> str: - lines = [ - "actor\tlocation\tdetail", - "----", - ] - if not actors: - lines.append("(no actors hold the repeated role)") - else: - lines.extend(actor.render() for actor in actors) - kinds: dict[str, int] = {} - for actor in actors: - kinds[actor.kind] = kinds.get(actor.kind, 0) + 1 - lines.append("----") - lines.append( - "counts: " - + ", ".join(f"{kind}={count}" for kind, count in sorted(kinds.items()) or ["none"]) - ) - return "\n".join(lines) - - -def self_test() -> int: - nested = """ -fn map_yielding() { - with_yielded_background_cpu_permits(|| { - items.into_par_iter().map(map).collect() - }); -} -""" - leaf = """ -fn collect() { - items.par_iter().map(|item| { - with_background_cpu_permit(|| operation(item)) - }).collect(); -} -""" - inside = """ -fn bad() { - with_background_cpu_permit(|| { - items.par_iter().for_each(|_| {}); - }); -} -""" - boundary = """ -fn install() { - self.background_cpu.with_yielded_permits(|| self.pool.install(operation)) -} -""" - terminal = """ -fn worker() { - let mut publication_authority_terminal = false; - if publication_authority_terminal { - publication_authority_terminal = true; - } -} -""" - # The detector must not count its own documentation of the tokens. - comment = """ -fn note() { - // with_yielded_background_cpu_permits is the failed return path - // publication_authority_terminal was the task-local role -} -""" - mentioned = """ -fn pin() { - assert!(source.contains("with_yielded_background_cpu_permits(")); - assert!(!source.contains("publication_authority_terminal")); -} -""" - cases = { - "nested yield": (nested, {"nested-yield"}), - "leaf permit outside join": (leaf, set()), - "permit closure joins the pool": (inside, {"nested-permit-join"}), - "install boundary": (boundary, {"pool-boundary-yield"}), - "worker-local terminal": (terminal, {"worker-local-terminal"}), - "commented tokens": (comment, set()), - "string mention": (mentioned, set()), - } - failed = False - for name, (source, expected) in cases.items(): - found = {actor.kind for actor in scan_source("fixture.rs", source)} - if found != expected: - print(f"self-test failed: {name}: got {sorted(found)} expected {sorted(expected)}") - failed = True - if failed: - return 1 - print("census_attack_the_premise self-test: ok") - return 0 - - -def main(argv: list[str]) -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--check", - action="store_true", - help="exit non-zero when a repeated role is still assigned", - ) - parser.add_argument("--self-test", action="store_true", help="run classifier fixtures") - parser.add_argument( - "--root", - type=Path, - default=Path(__file__).resolve().parents[1], - help="repository root (default: parent of scripts/)", - ) - args = parser.parse_args(argv) - if args.self_test: - return self_test() - actors = census(args.root) - print(render(actors)) - failed = violations(actors) - if args.check and failed: - print(f"check failed: {len(failed)} actor(s) still hold a repeated role", file=sys.stderr) - return 1 - return 0 - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) From 1780df230ac100ec16d3486043f3d1f695087a16 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:32:14 +0000 Subject: [PATCH 022/182] simplify(pass-1/5): delete orphaned root test fixtures Nothing in crates, scripts, or CI loaded these trees. The SQLite evidence pack only checked itself, and the remaining files were stale allowlists and unused samples. Allow the simplify commit type so these passes satisfy commitlint. Co-authored-by: Zack Jackson --- commitlint.config.cjs | 1 + .../search_quality/incremental/time-after.rs | 25 - tests/fixtures/shipped-shape-admissions.md | 61 - .../direct_open_allowlist.json | 87 - .../storage_runtime/rollback_rehearsal.json | 14 - .../storage_runtime_evidence/README.md | 49 - .../storage_runtime_evidence/artifacts.json | 66 - .../authoritative-corrupt.sqlite3 | Bin 53248 -> 0 bytes .../crash-after-repair-commit.sqlite3 | Bin 10240 -> 0 bytes .../fts-stale.sqlite3 | Bin 6144 -> 0 bytes .../storage_runtime_evidence/generate.py | 420 ----- .../online-backup-copy.sqlite3 | Bin 5120 -> 0 bytes .../online-backup-digest-mismatch.sqlite3 | Bin 5120 -> 0 bytes .../online-backup-source.sqlite3 | Bin 5120 -> 0 bytes .../storage-runtime-fixture-v1.json | 28 - .../storage_runtime_evidence/validate.py | 265 --- .../wal-pressure.blocker.json | 11 - .../wal-pressure.sqlite3 | Bin 134144 -> 0 bytes .../fixtures/v2/research-anchor-manifest.json | 1493 ----------------- .../workflow_provider/claude-code-result.json | 1 - .../workflow_provider/codex-exec.jsonl | 4 - 21 files changed, 1 insertion(+), 2524 deletions(-) delete mode 100644 tests/fixtures/search_quality/incremental/time-after.rs delete mode 100644 tests/fixtures/shipped-shape-admissions.md delete mode 100644 tests/fixtures/storage_runtime/direct_open_allowlist.json delete mode 100644 tests/fixtures/storage_runtime/rollback_rehearsal.json delete mode 100644 tests/fixtures/storage_runtime_evidence/README.md delete mode 100644 tests/fixtures/storage_runtime_evidence/artifacts.json delete mode 100644 tests/fixtures/storage_runtime_evidence/authoritative-corrupt.sqlite3 delete mode 100644 tests/fixtures/storage_runtime_evidence/crash-after-repair-commit.sqlite3 delete mode 100644 tests/fixtures/storage_runtime_evidence/fts-stale.sqlite3 delete mode 100644 tests/fixtures/storage_runtime_evidence/generate.py delete mode 100644 tests/fixtures/storage_runtime_evidence/online-backup-copy.sqlite3 delete mode 100644 tests/fixtures/storage_runtime_evidence/online-backup-digest-mismatch.sqlite3 delete mode 100644 tests/fixtures/storage_runtime_evidence/online-backup-source.sqlite3 delete mode 100644 tests/fixtures/storage_runtime_evidence/storage-runtime-fixture-v1.json delete mode 100644 tests/fixtures/storage_runtime_evidence/validate.py delete mode 100644 tests/fixtures/storage_runtime_evidence/wal-pressure.blocker.json delete mode 100644 tests/fixtures/storage_runtime_evidence/wal-pressure.sqlite3 delete mode 100644 tests/fixtures/v2/research-anchor-manifest.json delete mode 100644 tests/fixtures/workflow_provider/claude-code-result.json delete mode 100644 tests/fixtures/workflow_provider/codex-exec.jsonl diff --git a/commitlint.config.cjs b/commitlint.config.cjs index 18b34ae514..3f19d02c48 100644 --- a/commitlint.config.cjs +++ b/commitlint.config.cjs @@ -8,6 +8,7 @@ const allowedTypes = [ "perf", "refactor", "revert", + "simplify", "style", "test", ]; diff --git a/tests/fixtures/search_quality/incremental/time-after.rs b/tests/fixtures/search_quality/incremental/time-after.rs deleted file mode 100644 index 715d5aafd1..0000000000 --- a/tests/fixtures/search_quality/incremental/time-after.rs +++ /dev/null @@ -1,25 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use super::error::DomainError; - -/// UTC timestamp represented as microseconds from the Unix epoch. -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[serde(transparent)] -pub struct UtcMicros(pub i64); - -/// Closed half-open occurrence interval. -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[serde(deny_unknown_fields)] -pub struct TimeInterval { - pub start: UtcMicros, - pub end: UtcMicros, -} - -impl TimeInterval { - pub fn validate_bounds(&self) -> Result<(), DomainError> { - if self.start > self.end { - return Err(DomainError::InvalidTimeInterval); - } - Ok(()) - } -} diff --git a/tests/fixtures/shipped-shape-admissions.md b/tests/fixtures/shipped-shape-admissions.md deleted file mode 100644 index e1c796c728..0000000000 --- a/tests/fixtures/shipped-shape-admissions.md +++ /dev/null @@ -1,61 +0,0 @@ -# Shipped persisted shapes and how this binary admits them - -Index of every admission in the workspace that pins a persisted shape — an -exact object inventory, a digest, a trigger body, a version marker, a file -format revision, `PRAGMA user_version`, or the contents of a migrations table — -together with the shape each released binary actually wrote and whether this -binary admits it. - -The released shapes are reconstructed from the tagged sources, never from the -current contract. A fixture derived from the current contract agrees with -whatever the admission expects, including an expectation no release ever wrote, -which is how five of these regressions reached a real profile one install at a -time. - -## Release window - -`v0.1.0-beta.25` through `v0.1.0-beta.37` (the newest tag; `beta.28` was never -tagged). Every marker constant in `crates/**` outside tests was compared -between `v0.1.0-beta.37` and this tree: of 46 markers present in both, five -differ, and only three of those describe persisted state -(`migrations::SCHEMA_VERSION` 34 → 35, `FINAL_CONFIGURATION_SCHEMA_DIGEST`, -`GIT_CORRELATION_SCHEMA_VERSION` 4 → 5). The other two are an in-memory cache -and a pagination cursor. - -## Admissions - -| admission site | store | shipped shape (beta.25 … beta.37) | verdict before | verdict now | test | -| --- | --- | --- | --- | --- | --- | -| `runtime_core::db::migrations::final_shape::{require_exact_final_shape, require_final_shape_except_payload_digests}` | project `tracedecay.db` | one byte-identical inventory: `user_version` 34, 183 objects, digest `0126b4dd550109a6` | **refused**: `database schema has incompatible table 'diagnostic_generation_publications'` | admitted, converged by `released_shape::converge_released_project_schema`, every row retained | `db::migrations::tests::final_shape::released_project_store_migrates_and_retains_every_row` | -| `runtime_core::db::migrations::unsupported_schema_version` | project `tracedecay.db` | — | refused for any stamp but 34 or 35 | unchanged: no release wrote another stamp | `db::migrations::tests::final_shape::stamped_final_store_with_missing_or_tampered_required_shape_is_reset_required` | -| `global_db::session_temporal_schema::admission::classify_registered_schema_admission` | `global.db`, profile and project `sessions.db` | schema marker 3, one 81-trigger authority inventory | **refused**: `released v3 authority trigger contracts are absent or incompatible` | admitted as `ReleasedV3` (fixed in `29def5d8bb`) | `tests::lcm_schema::temporal_catalog::admission::published_v3_authority_triggers_migrate_and_retain_every_session` | -| `global_db::schema_stages` configuration digest | profile configuration | `FINAL_CONFIGURATION_SCHEMA_DIGEST` `sha256:99b8f5f5…` | **refused**: unreleased `2b3eab89e2` re-pinned the digest after `f01d6da607` dropped `configuration_credential_references` | admitted through `RELEASED_CONFIGURATION_SCHEMA_DIGEST` | `configuration::schema` suite | -| `runtime_core::db::migrations::install_runtime_writer_ledger` / `global_db::schema_stages::converge_runtime_writer_ledger` | project `tracedecay.db`, profile `user-sessions.db`, project `sessions.db` | `td_runtime_writer_{checkpoint_v1, idempotency_v1, inbox_v1, outbox_v1}` | **refused**: canonical shape carries `idempotency_v2` | admitted, `v1` folded into `v2` in bounded pages (`b703616d7d`, `7f7cd14dd0`, `63ab366df5`) | `db::migrations::tests::final_shape::runtime_writer_ledger_is_part_of_the_final_shape` | -| `global_db::schema_stages` workflow contracts | `global.db` | `WORKFLOW_SCHEMA_VERSION_V1` 1, byte-identical `WORKFLOW_TABLE_CONTRACTS_V1` | admitted | unchanged | `schema_stages` workflow suite | -| `lcm::schema::require_admissible_lcm_schema` | profile `user-sessions.db` | `session_schema_migrations.lcm` 8 | admitted | unchanged | `lcm::schema` suite | -| `sessions::runtime::git_correlation::ensure_git_correlation_receipt_schema_in_transaction` | `sessions.db` | `session_schema_migrations.git_correlation` 4 | admitted: the install is `CREATE … IF NOT EXISTS` plus a marker upsert, so 4 converges to 5 additively | unchanged | `git_correlation` schema suite | -| `sessions::runtime::workflow_index::ensure_workflow_index_schema` | `sessions.db` | `session_schema_migrations.workflow_indexing` 1 | admitted | unchanged | `workflow_index` suite | -| `global_db::observation::schema::require_admitted_observation_shape` | `global.db` observation authority | canonical columns plus `global_schema_migrations` `observations-v2-canonical-autoincrement` | admitted: the native-source-scheme marker is enrolled on open for every authority that cannot double-count. A populated authority that does carry Cline-like native sources is refused on purpose — re-offering `:ui_messages` would admit its events twice — and the remedy resets only the derived observation authority | unchanged | `observation::schema` suite | -| `global_db::registered_legacy_relations::require_admissible_legacy_relations` | registered session shard | none: the listed tables were already retired at `beta.25` | admitted | unchanged | `registered_legacy_relations` suite | -| `global_db::project_registry` | profile registry | — | admitted: both refusals read row content (a non-canonical `projects.path` key, two ids claiming one root), not a shape | unchanged | `project_registry` suite | -| `code_index::production::sealed_codec::sealed_generation_format_revision_is_compatible` | `code-index-v1` sealed generations, segments, read bundles | format revision 6 at every tag | admitted: `MINIMUM_SEALED_GENERATION_FORMAT_REVISION` is 6 and the compatible set is {6, 7} | unchanged | `sealed_codec` suite | -| `host_admission::spool::frames` | private-fs framed spool | `FRAME_MAGIC` `TDHA`, `FORMAT_VERSION` 1 | admitted: both unchanged | unchanged | `spool::frames` suite | -| `hooks::spool`, `hooks::admission_ledger` | hook spool and admission ledger | `TDH2`/`TDHC`/`TDL1`, spool format 1, checkpoint format 2, ledger format 1 | admitted: all unchanged | unchanged | `hooks::spool` suite | -| `maintenance::profile_backup` | profile backup archive | identity schema version 2 | admitted: unchanged | unchanged | `profile_backup` suite | -| `automation_runtime::automatic_facts` | automatic fact proposals | proposal schema markers unchanged | admitted | unchanged | `automatic_facts` suite | -| `dashboard_api::graph_structure_api::graph_reset_required` | published graph generations | — | admitted: the refusals validate a row's lineage and binding at read time rather than pinning a shape | unchanged | `graph_structure_api` suite | - -## Fixtures - -| fixture | shape | -| --- | --- | -| `crates/tracedecay-runtime-core/tests/fixtures/project-store-released-v34.sql` | the whole released project store, assembled from the tagged DDL constants | -| `crates/tracedecay-global-db/tests/fixtures/session-temporal-released-v3-triggers.sql` | the released session-temporal authority triggers, extracted verbatim from the tag | -| `crates/tracedecay-global-db/tests/fixtures/session-relation-receipts-before-recovery.sql` | the session-relation receipt shape that predates receipt recovery | - -## Not a persisted shape - -`temporal_query::cursor::CURSOR_FORMAT_VERSION` moved from `"2"` to `"3"`. A -pagination cursor is an opaque continuation handed back to a caller within one -session, not persisted state, so an older cursor is refused rather than -migrated and the caller re-runs its query. diff --git a/tests/fixtures/storage_runtime/direct_open_allowlist.json b/tests/fixtures/storage_runtime/direct_open_allowlist.json deleted file mode 100644 index c59864fad8..0000000000 --- a/tests/fixtures/storage_runtime/direct_open_allowlist.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "scan_roots": [ - "src", - "crates/tracedecay-rusqlite-runtime/src" - ], - "direct_open_suffixes": [ - "Builder::new_local", - "Database::initialize", - "Database::open", - "Database::open_read_only", - "Connection::open", - "Connection::open_in_memory", - "Connection::open_with_flags" - ], - "allowed": [ - { - "path": "crates/tracedecay-rusqlite-runtime/src/backup/driver.rs", - "callee": "Connection::open", - "disposition": "private restore staging destination" - }, - { - "path": "crates/tracedecay-rusqlite-runtime/src/connection/mod.rs", - "callee": "Connection::open_with_flags", - "disposition": "permanent daemon-owned shard opener" - }, - { - "path": "crates/tracedecay-rusqlite-runtime/src/effects/sqlite.rs", - "callee": "Connection::open_with_flags", - "disposition": "bounded read-only effect receipt probe over the writer-owned shard" - }, - { - "path": "crates/tracedecay-rusqlite-runtime/src/evidence.rs", - "callee": "Connection::open", - "disposition": "standalone S11 evidence over private fixture copies; never live authority" - }, - { - "path": "crates/tracedecay-rusqlite-runtime/src/evidence.rs", - "callee": "Connection::open_with_flags", - "disposition": "read-only logical evidence over private fixture copies" - }, - { - "path": "crates/tracedecay-rusqlite-runtime/src/graph/fixtures.rs", - "callee": "Connection::open", - "disposition": "test-only graph parity fixture" - }, - { - "path": "crates/tracedecay-rusqlite-runtime/src/writer/backup.rs", - "scope": "StagedBackupDestination::create_new_private_destination", - "callee": "Connection::open_with_flags", - "disposition": "writer-owned private backup staging destination pinned until atomic publication or cleanup" - }, - { - "path": "crates/tracedecay-rusqlite-runtime/src/writer/backup.rs", - "scope": "verify_sqlite", - "callee": "Connection::open_with_flags", - "disposition": "bounded read-only quick-check over the pinned private backup staging destination before publication" - }, - { - "path": "src/sqlite_read_snapshot.rs", - "scope": "SnapshotConnection::open", - "callee": "Connection::open_with_flags", - "disposition": "immutable or private-copy snapshot source opened read-only outside the live store authority" - }, - { - "path": "src/sqlite_read_snapshot.rs", - "scope": "SnapshotDatabase::backup_to", - "callee": "Connection::open_with_flags", - "disposition": "new private standalone backup destination populated from an already-frozen snapshot" - }, - { - "path": "src/sqlite_read_snapshot.rs", - "scope": "materialize_standalone_snapshot", - "callee": "Connection::open_with_flags", - "disposition": "bounded source and private destination opens used only to collapse a frozen copied WAL family into a standalone snapshot" - }, - { - "path": "src/retention/branch_compaction.rs", - "callee": "Connection::open_with_flags", - "disposition": "bounded daemon maintenance for an unmounted non-active branch database; busy files are skipped rather than competing with a writer" - }, - { - "path": "src/retention/storage_report.rs", - "callee": "Connection::open_with_flags", - "disposition": "short read-only free-page sample that degrades on busy or unavailable WAL state and never obtains writer authority" - } - ] -} diff --git a/tests/fixtures/storage_runtime/rollback_rehearsal.json b/tests/fixtures/storage_runtime/rollback_rehearsal.json deleted file mode 100644 index c4d3c06ee3..0000000000 --- a/tests/fixtures/storage_runtime/rollback_rehearsal.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "orchestrator": "crates/tracedecay-rusqlite-runtime/src/backup/orchestrator.rs", - "driver": "crates/tracedecay-rusqlite-runtime/src/backup/driver.rs", - "validation": "crates/tracedecay-rusqlite-runtime/src/backup/validation.rs", - "maintenance": "crates/tracedecay-rusqlite-runtime/src/maintenance/coordinator.rs", - "registry": "src/daemon/store_runtime/registry.rs", - "forbidden_writable_fallbacks": [ - "Builder::new_local", - "Connection::open", - "Database::initialize", - "Database::open", - "GlobalDb::open_at" - ] -} diff --git a/tests/fixtures/storage_runtime_evidence/README.md b/tests/fixtures/storage_runtime_evidence/README.md deleted file mode 100644 index 04ddce9ce0..0000000000 --- a/tests/fixtures/storage_runtime_evidence/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# S11 SQLite runtime evidence - -These fixtures are real SQLite databases generated by `generate.py` with -Python's SQLite API. They are not serialized success responses and contain no -live profile paths. - -## Evidence - -- `wal-pressure.sqlite3` plus `wal-pressure.sqlite3-wal` preserves committed - WAL frames. `wal-pressure.blocker.json` supplies the stable snapshot lease - used by the runtime pressure/blocker test. -- `crash-after-repair-commit.sqlite3` is produced by a child process that - commits the FTS repair receipt transaction and exits with `os._exit(73)` - before acknowledging success. Runtime replay must recover the durable - receipt. -- `fts-stale.sqlite3` has healthy authoritative rows and an intentionally stale - external-content FTS5 projection. Runtime repair must rebuild FTS and commit - its receipt atomically. -- `authoritative-corrupt.sqlite3` has an invalid cell pointer in a populated - authoritative B-tree leaf. SQLite `quick_check` reports corruption while the - source remains readable enough to preserve as quarantine evidence. -- `online-backup-source.sqlite3` and `online-backup-copy.sqlite3` are connected - through SQLite's online backup API. The runtime test performs its own online - backup, isolated staged restore, durable publication, and higher-binding - acknowledgement from the checked source. -- `online-backup-digest-mismatch.sqlite3` is a valid SQLite backup copy changed - through SQLite after backup. It remains structurally healthy but must fail - the clean artifact's digest. - -`storage-runtime-fixture-v1.json` is the sole path-free fixture contract -consumed by the generator, validator, benchmark adapter, and Rust evidence -command. `artifacts.json` records every generated SQLite/WAL/support artifact, -its byte length and SHA-256 digest, the stable logical binding, generator -versions, and expected values. - -## Regeneration and static validation - -From the repository root: - -```sh -python3 tests/fixtures/storage_runtime_evidence/generate.py -python3 tests/fixtures/storage_runtime_evidence/validate.py -``` - -Logical content and corruption locations are deterministic. SQLite WAL salts -are normalized and their checksums recomputed after SQLite creates the real -WAL, making repeated generation byte-for-byte reproducible on the recorded -Python/SQLite versions. Validation regenerates into an isolated directory and -compares every artifact and manifest byte-for-byte. diff --git a/tests/fixtures/storage_runtime_evidence/artifacts.json b/tests/fixtures/storage_runtime_evidence/artifacts.json deleted file mode 100644 index 574d789fb3..0000000000 --- a/tests/fixtures/storage_runtime_evidence/artifacts.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "artifacts": { - "authoritative-corrupt.sqlite3": { - "bytes": 53248, - "sha256": "950a0ec2f085610929d969b66552835e38595ac23bac5fc36cbb65dd4f159d41" - }, - "fts-stale.sqlite3": { - "bytes": 6144, - "sha256": "1203c0e30a31c8465096ca8ea019aa85a1e98f5dc7c33d6a1bc513db3f3c129a" - }, - "online-backup-copy.sqlite3": { - "bytes": 5120, - "sha256": "98157a9aa59243ef7cf5b1b9c8883fccaafee5261b57e3d9e24f85a77e03de3f" - }, - "online-backup-digest-mismatch.sqlite3": { - "bytes": 5120, - "sha256": "0c9e3bf6e2ed6ac69bb4ddd9c86b751cc9b3c8301662642bf9b09ee78909b3e2" - }, - "online-backup-source.sqlite3": { - "bytes": 5120, - "sha256": "18331f506d032fdff86602b3e194251a32a7b36b420624e11b5ca6c8bfab9167" - }, - "wal-pressure.blocker.json": { - "bytes": 305, - "sha256": "99dc029a0a9a13c3f90b7ad13248d3d1cf6e2c0f81f9fd4f2c23f38034e67c12" - }, - "wal-pressure.sqlite3": { - "bytes": 2048, - "sha256": "a2a875b58f67149a887e7a3c74a84aa1ab765c80cf0728d5a62d4210368c4c5d" - }, - "wal-pressure.sqlite3-wal": { - "bytes": 137320, - "sha256": "40dce0b4bf13f35590087e2b12f24cca6a8e549b6be04278ab807811ac8a6a86" - } - }, - "binding": { - "authority_epoch": 19, - "incarnation": 7, - "shard_id": { - "brain_id": "brain.s11.evidence", - "profile_id": "profile.s11.evidence", - "scope": { - "kind": "project", - "project_id": "project.s11.evidence" - } - } - }, - "expectations": { - "authoritative_rows": 200, - "backup_rows": 64, - "crash_evidence_id": "evidence.s11.crash", - "crash_receipt_id": "receipt.s11.crash", - "fts_search_term": "needle", - "replacement_authority_epoch": 20, - "replacement_incarnation": 8, - "wal_pressure_rows": 129 - }, - "generator": { - "byte_reproducibility": "Byte-for-byte on the recorded Python/SQLite versions; generated WAL salts and checksums are normalized.", - "logical_state_deterministic": true, - "python": "3.12.3", - "script": "generate.py", - "sqlite": "3.45.1" - }, - "schema": "tracedecay.storage-runtime-evidence.s11.v1" -} diff --git a/tests/fixtures/storage_runtime_evidence/authoritative-corrupt.sqlite3 b/tests/fixtures/storage_runtime_evidence/authoritative-corrupt.sqlite3 deleted file mode 100644 index 22a17915c67cc0f717d1b2e3e7576e435e602a13..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53248 zcmeI5JCv1Y6^75uH{9>n;TjJ2JDh#)CqTjg6Tl3F3=;+2G2DenB$5dRjWK&+p@of& zjZGRG8yg!N8yg!N8yg!N8yg$fa#`p;VbRR{HYt?-AbKzpd5U`St8GlHUy3v(ND7CBq+P&Rn)7vmbnZjGmsJoS2!c z&rF<}nyi10>ih29s-M3&GkJD$x_)W;{Dq0>tM%6>uO6!3cyQ}+eP;5?O#R{;`SsS+ z)c*SAnd$SVXXfRz*yulpPt-2|D*q^dFMlh4Eq^J0E`KV2EPp7!FTX3lEx#$hF25?j zEWaq9m7kZNm7kWMlpmER<&*Nb%$5h`-EzCUQ?8cFqS_j_28kex65TIQEW2BhSNeD9=N&FVBOq zE6)Q_&vSol$#Y+9$a8P3&2vwz%yV}v&2v{Q%yVZ9$K{U5qqhCyJnA3iv28Ywm)_4~ z>-{{o+|6V2tvohe%VVSGvEfP{<1=}zzm&(isXW%6%VW*(fmff*W7Y6+R}Pgk@|vo5yNDtd;Gfr$%n( zH_zb{J~mc+a^m`jvv(gnyf=G&_THo0hZ~L7;r|a3UYK4J^fXEMDAP00(|Os@fY znxq>2zmS!<2PM`d;R~2v7xXkq_A}1( zO%lGC=rsn=Aetn65!35~o+hbA|F0w^HkcQpBzy(agLz?^Bz!s3gYC^UN%%6R2fLbS zl4|t-T2^A17p6(V*DyVp7p6(VS2I1>)l8FwuVQ+ztC=RLM*nXhC2qpJ5GCQ`Ob_No zQmVtTN>H8n~2My3b5 zT2qr$qyOuq#4VT?q9lA9(}Q`@(j?(85xpktYN8~3E7OBrt*J?>(f_+hiCZu)L`nEg zrU&z)rAfkfFg@7bTACz$JJW+*t))q-(f|8ciD81YG)eegrU&z)rAfl~Fg@7STACz$ zH`9Y%t))q-(f@}?iQ6zQL`nETrU&z)tx3WUFg@7b+L|PMKhuLA}2cYm)FIOb@oVwk8Qb%=BPaYip8f^naU_xC8S-l!UjK9?Xl5CJAp6y*BJ> zq9ojy9_(suO;U~i?~@XDU|xul@E+5HdC}1%;a#Q&+gnGIgm;)8>}nlNQjPvU&Pogu zq@zi~k1;)%7adI!{xZ{pU9F=@!Us$bcD0TssYd^wBqi>`ybvYfuQEND7hO#f{tDBB z?X9ax!cQ>}p+2QjPvU zM@rm-c_B)|&oVuj7d=f9{u}p+2QjPwfA|>v@ybvYfuQNTE7d=f9 z{w1ad+gneQgr8@6u&ebnNj3Wa5-TxGke((9e}n14yy$6?@QX|jcD0@+3BSPfU{~vD zl4|t-3@LFR=7lH;zs&StUi39d_%zdl?X9m#!rx?iu&eboNj3Wa3M(2fNxplZ0Pqda$bvG)Xo3|1Or;Apb*^gnxtS&7Bw4&?Mn^nBLs& z%^I2{{5I2@yQ^74lT@Ss@3Rumofp>7B;ns=dUNN6H8e^1yG(EH_GS%D5`K^A&E3_k zp-HOI|L>C$L)wRllJEyi59S5rf0zUb{}$1M{0|c);qNiMxx1P*G)Xo3f0mRO(mqU- zgnz*FU|vA}he?p|hfHtoZJ$B@hXx7%Hq(P$4e~!sf>fjbKVl`Gd)sG_|Di#`zr*xk zUO@hbNs#bIOb>Q7$p0`268<66gIx{sKTLvDqyIl9C5E&Q6D8r_V|p+_ApgT8NceY| z9^Cdp{)b7B@W)IKwl~QCFbVRs|DQa||9D@xlJFleJ(wVn|6vj& z{1c`Jw|$WRVG<<#`%DkEH^~1m3G%f6KdqJe=*f|5wNL-o{U07tHkP7twLQ-Fd`Y=(F@6Q6}2lE5cKTLw8-+bl= z*MAWIVG<<$<}p9m-XQe4$he?opf0i&mm>-b-VG<<$7BfG%{)6}r zlOXB0i21?x2KgT*K`PMyD@lDJ>cd1yzCSCNAIuL(|1b%Xe#@C3T>nA*he?q1TgLog ze}nuFlOPr7|Fx{XFh3yt!z4()KWmsD%nwNaFbR@=tC=5M|3UnRNs#ng#r$A@gZvMZ zAQkBU4Wzyh^1m zV}39{ApOH6Ncz3R{9u2B_z#mH>9>{n!TtvMA0|O6(EqzgeIe?@L`l9sJDDHM4@mzo z36g$0m>=B#LHvhFko4Qm{9u2B{11~L73lwctiCWmApFB5NWMRNnIFs#NdGVil74%b zAKd>z{D(=9^xMt+V1I-B50fAj=>J2cz7X|cq9os+gUk=+2c&cd1yzCTBqAIuL(|1b%Xen*%e-2XxRhe?q1JIwrGe}nuF zlOPr7|2C^H-2XxNhe?ope_G5B<_DyIm;_0`Ci8>+4dOpcf~21@KiJI+dHCQ9=C=`lZ;ACUfG5+wb)%n$DWApXN7Ncwe{AM9_C|6vlO0{wrS)feUmgnyU> z$@k|N^Mm;T=^rLR((h&F2lsyv|6vj&{RYer_BY7?FbPtD{y$0T3sD~?O7i`AmHEN^ zfbcd1yzCWj#AIuL(|1b%Xey5lp z-2XxRhe?q1n_zyhzd`2BWqvR}ApOH6Ncz3T Y{9u2B_z#mH={L#zV1I-B50fDO0nRxd%K!iX diff --git a/tests/fixtures/storage_runtime_evidence/crash-after-repair-commit.sqlite3 b/tests/fixtures/storage_runtime_evidence/crash-after-repair-commit.sqlite3 deleted file mode 100644 index 6a29ad818a1f3c337a3743a772fc16b43b8b0549..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10240 zcmeI2&2Q646u@UZPTZz^thCZvq6Y>G$%$mOL{(L70cq0`m1si?iOa_+a_nh3+r$pG z$+oc752$!#NV=q<-~;(TzHNX6PKhQA+2`lym&NUZ@zw$H}fRBeyhquMt3~F zV}*1H$p~Zg3#AC5EQB0{ktmRmM;VAQgt;hhUaJ?Qc!FfnLhnTeDlqvFRp;Vpf~F`E z1O$P9nZRu3G>nMJ-v}Ndf`B0K9TGT)3A${1oemE}=1`w?E$&m7F{jNU`zPcjBL9%L z5D5_k1cCoHfrV#}LBOV)FwcDMQexRyl5jRQYb?n)8yh;7htkijE3L*o1Y9U|`p7C}G| zI0*#i$UN>bKj5A#$M^p{BJ(F<yUP)`M`@^LcpCU|rAN)mDo?8ya_Q%Xh61j7?izF|=UU@|^~EwADvi)3>-g zNXN-RZ=?(0*1?mzSdt z1-?i5BcrG*E4Z(QRF+TKob)i7-RLtvJ+a|4T-Kkvb5k)N)r=YMxD?J$pO2~1ui4>+2Xtq6j zZwTR%q^P<+^W?(k5K?0fN4s6AncJo65PeF6Zr3ZD8x%HgIiI6r()64IBUNI1ZU{Sl z_0!hr6BQc1_d9p4>fF(j_J#f&R@C$7ai689oJ>Yakx7badR9~Z9?3Y-0-~8yvwYX2 zn_CdJtJS2sKFmHFrfW{GQBpQDUGIny4W|b#a%?7J z2W?_9Y0DmY=Sh>1Wu?`c0ED}(FBo59jl+Ag|6}?{7~CXq*zE9Datw@>WmAj(bQq1P P==r_zBix|9BM0|K*zNqY diff --git a/tests/fixtures/storage_runtime_evidence/fts-stale.sqlite3 b/tests/fixtures/storage_runtime_evidence/fts-stale.sqlite3 deleted file mode 100644 index 32a76ba7d6126aba7062d2407a2ffd691678a70a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6144 zcmeH}L66cv6o6+s1zd2|s7Vi+?Ayeo5Q!`?WG`+aunpUYpryF(X;bJxnuT`LR$Mn8 ztnuPs@}lua_!~T!c-fODXLc*n6*zg(oflqb`sTrV^UZrLou^GFq%iROu@%C7BqNN` zLjZ(OI#xEE3cr`*IUg|79G{rmoiZQiNE+RnykBJtY4jNpU-~FD@CW>W-TXBv4+hThxpZPTf-4eraVnXN|2eF zA(_>J@|ecq8{nCPsXaHL)n?jlHmA!4=$M9HH+gSLi1a3+Mm4b2^ ze0pd(KDdGC>Ojv5=*V#??|(u*A@Y@cXM+$yKoIzc2&}(*!-O^V4omv> z%Eqt0#uCBlvtGxNB(J8J%QY-r%@TBnZx3V46=(UJ{6Op@L=X@JE)0Q`ymA>!{QWQb r|H2%)*r*^76Oh?If-BP5`~L)y69GmL5Ckp)fgHJoC)5ud&y{}y5_dH| diff --git a/tests/fixtures/storage_runtime_evidence/generate.py b/tests/fixtures/storage_runtime_evidence/generate.py deleted file mode 100644 index 73dffa984e..0000000000 --- a/tests/fixtures/storage_runtime_evidence/generate.py +++ /dev/null @@ -1,420 +0,0 @@ -#!/usr/bin/env python3 -"""Generate S11 crash, corruption, and backup/restore SQLite evidence.""" - -from __future__ import annotations - -import argparse -import hashlib -import json -import os -import shutil -import sqlite3 -import subprocess -import sys -import tempfile -from pathlib import Path - - -ROOT = Path(__file__).resolve().parent -PAGE_SIZE = 1024 -ARTIFACT_MANIFEST = "artifacts.json" -RUNTIME_MANIFEST = "storage-runtime-fixture-v1.json" -FIXTURE_FILES = ( - "wal-pressure.sqlite3", - "wal-pressure.sqlite3-wal", - "wal-pressure.blocker.json", - "crash-after-repair-commit.sqlite3", - "fts-stale.sqlite3", - "authoritative-corrupt.sqlite3", - "online-backup-source.sqlite3", - "online-backup-copy.sqlite3", - "online-backup-digest-mismatch.sqlite3", -) -BINDING = { - "shard_id": { - "brain_id": "brain.s11.evidence", - "profile_id": "profile.s11.evidence", - "scope": { - "kind": "project", - "project_id": "project.s11.evidence", - }, - }, - "incarnation": 7, - "authority_epoch": 19, -} - - -def canonical_json(value: object) -> str: - return json.dumps(value, separators=(",", ":"), sort_keys=True) - - -def configure(connection: sqlite3.Connection) -> None: - connection.execute(f"PRAGMA page_size={PAGE_SIZE}") - connection.execute("PRAGMA foreign_keys=ON") - connection.execute("PRAGMA application_id=0x54445331") - connection.execute("PRAGMA user_version=11") - - -def quick_check(connection: sqlite3.Connection) -> list[str]: - return [str(row[0]) for row in connection.execute("PRAGMA quick_check")] - - -def wal_checksum( - content: bytes, - byte_order: str, - state: tuple[int, int] = (0, 0), -) -> tuple[int, int]: - first, second = state - if len(content) % 8 != 0: - raise ValueError("WAL checksum input must contain complete word pairs") - for offset in range(0, len(content), 8): - left = int.from_bytes(content[offset : offset + 4], byte_order) - right = int.from_bytes(content[offset + 4 : offset + 8], byte_order) - first = (first + left + second) & 0xFFFFFFFF - second = (second + right + first) & 0xFFFFFFFF - return first, second - - -def canonicalize_wal(path: Path) -> None: - content = bytearray(path.read_bytes()) - if len(content) < 32: - raise RuntimeError("generated WAL is missing its header") - magic = int.from_bytes(content[0:4], "big") - if magic not in (0x377F0682, 0x377F0683): - raise RuntimeError("generated WAL has an unsupported magic") - page_size = int.from_bytes(content[8:12], "big") - if page_size == 1: - page_size = 65_536 - frame_size = 24 + page_size - if page_size <= 0 or (len(content) - 32) % frame_size != 0: - raise RuntimeError("generated WAL has incomplete frames") - - first_salt = 0x53313145 - second_salt = 0x56494431 - content[16:20] = first_salt.to_bytes(4, "big") - content[20:24] = second_salt.to_bytes(4, "big") - byte_order = "big" if magic & 1 else "little" - checksum = wal_checksum(bytes(content[:24]), byte_order) - content[24:28] = checksum[0].to_bytes(4, "big") - content[28:32] = checksum[1].to_bytes(4, "big") - - for offset in range(32, len(content), frame_size): - content[offset + 8 : offset + 12] = first_salt.to_bytes(4, "big") - content[offset + 12 : offset + 16] = second_salt.to_bytes(4, "big") - checksum_input = ( - bytes(content[offset : offset + 8]) - + bytes(content[offset + 24 : offset + frame_size]) - ) - checksum = wal_checksum(checksum_input, byte_order, checksum) - content[offset + 16 : offset + 20] = checksum[0].to_bytes(4, "big") - content[offset + 20 : offset + 24] = checksum[1].to_bytes(4, "big") - path.write_bytes(content) - - -def create_wal_pressure() -> None: - with tempfile.TemporaryDirectory(prefix="tracedecay-s11-wal-") as temporary: - database = Path(temporary) / "wal-pressure.sqlite3" - writer = sqlite3.connect(database) - configure(writer) - assert writer.execute("PRAGMA journal_mode=WAL").fetchone()[0].lower() == "wal" - writer.execute("PRAGMA wal_autocheckpoint=0") - writer.executescript( - """ - CREATE TABLE events ( - sequence INTEGER PRIMARY KEY, - payload BLOB NOT NULL - ) STRICT; - INSERT INTO events VALUES (1, zeroblob(700)); - """ - ) - writer.commit() - writer.execute("PRAGMA wal_checkpoint(TRUNCATE)").fetchone() - - blocker = sqlite3.connect(database) - blocker.execute("BEGIN") - assert blocker.execute("SELECT count(*) FROM events").fetchone()[0] == 1 - writer.executemany( - "INSERT INTO events VALUES (?, zeroblob(700))", - ((sequence,) for sequence in range(2, 130)), - ) - writer.commit() - - wal = Path(f"{database}-wal") - assert wal.stat().st_size > 32 - shutil.copyfile(database, ROOT / database.name) - shutil.copyfile(wal, ROOT / wal.name) - blocker.close() - writer.close() - canonicalize_wal(ROOT / "wal-pressure.sqlite3-wal") - - (ROOT / "wal-pressure.blocker.json").write_text( - json.dumps( - { - "schema": "tracedecay.storage-runtime-evidence.wal-blocker.v1", - "lease_id": "snapshot.s11.wal-pressure", - "snapshot_row_count": 1, - "committed_row_count": 129, - "expected": { - "wal_frames_minimum": 1, - "checkpointed_less_than_log_while_blocked": True, - "hard_drain_required": True, - }, - }, - indent=2, - sort_keys=True, - ) - + "\n", - encoding="utf-8", - ) - - -def create_documents_schema(connection: sqlite3.Connection) -> None: - configure(connection) - connection.executescript( - """ - CREATE TABLE documents ( - id INTEGER PRIMARY KEY, - body TEXT NOT NULL - ) STRICT; - CREATE VIRTUAL TABLE documents_fts USING fts5( - body, - content='documents', - content_rowid='id' - ); - """ - ) - - -def create_fts_stale() -> None: - path = ROOT / "fts-stale.sqlite3" - connection = sqlite3.connect(path) - create_documents_schema(connection) - connection.execute("INSERT INTO documents VALUES (1, 'stable baseline')") - connection.execute("INSERT INTO documents_fts(documents_fts) VALUES ('rebuild')") - connection.commit() - connection.execute("INSERT INTO documents VALUES (2, 's11 repair needle')") - connection.commit() - assert quick_check(connection) == ["ok"] - assert ( - connection.execute( - "SELECT count(*) FROM documents_fts WHERE documents_fts MATCH 'needle'" - ).fetchone()[0] - == 0 - ) - connection.close() - - -def crash_child(path: Path) -> None: - connection = sqlite3.connect(path) - create_documents_schema(connection) - connection.execute("INSERT INTO documents VALUES (1, 'committed repair needle')") - connection.execute( - """ - CREATE TABLE tracedecay_repair_receipts ( - receipt_id TEXT PRIMARY KEY NOT NULL, - evidence_id TEXT NOT NULL, - binding TEXT NOT NULL - ) STRICT - """ - ) - connection.commit() - connection.execute("BEGIN IMMEDIATE") - connection.execute("INSERT INTO documents_fts(documents_fts) VALUES ('rebuild')") - connection.execute( - """ - INSERT INTO tracedecay_repair_receipts - (receipt_id, evidence_id, binding) - VALUES (?, ?, ?) - """, - ( - canonical_json("receipt.s11.crash"), - canonical_json("evidence.s11.crash"), - canonical_json(BINDING), - ), - ) - connection.commit() - os._exit(73) - - -def create_crash_after_commit() -> None: - path = ROOT / "crash-after-repair-commit.sqlite3" - result = subprocess.run( - [sys.executable, str(Path(__file__).resolve()), "--crash-child", str(path)], - check=False, - ) - if result.returncode != 73: - raise RuntimeError(f"crash child exited {result.returncode}, expected 73") - connection = sqlite3.connect(path) - assert quick_check(connection) == ["ok"] - assert ( - connection.execute("SELECT count(*) FROM tracedecay_repair_receipts").fetchone()[0] - == 1 - ) - connection.close() - - -def create_authoritative_corruption() -> None: - path = ROOT / "authoritative-corrupt.sqlite3" - connection = sqlite3.connect(path) - configure(connection) - connection.execute( - "CREATE TABLE facts (id INTEGER PRIMARY KEY, body TEXT NOT NULL) STRICT" - ) - connection.executemany( - "INSERT INTO facts(body) VALUES (?)", - ((f"authoritative-{number:03d}-" * 12,) for number in range(200)), - ) - connection.commit() - leaf_page = connection.execute( - """ - SELECT pageno - FROM dbstat - WHERE name = 'facts' AND pagetype = 'leaf' - ORDER BY pageno DESC - LIMIT 1 - """ - ).fetchone()[0] - connection.close() - - content = bytearray(path.read_bytes()) - page_offset = (int(leaf_page) - 1) * PAGE_SIZE - cell_count = int.from_bytes(content[page_offset + 3 : page_offset + 5], "big") - if cell_count == 0: - raise RuntimeError("selected authoritative leaf page has no cells") - content[page_offset + 8 : page_offset + 10] = (1).to_bytes(2, "big") - path.write_bytes(content) - - connection = sqlite3.connect(path) - messages = quick_check(connection) - if messages == ["ok"]: - raise RuntimeError("authoritative corruption was not detected") - assert connection.execute("SELECT count(*) FROM facts").fetchone()[0] == 200 - connection.close() - - -def create_online_backup() -> None: - source_path = ROOT / "online-backup-source.sqlite3" - source = sqlite3.connect(source_path) - configure(source) - source.execute( - "CREATE TABLE facts (id INTEGER PRIMARY KEY, body TEXT NOT NULL) STRICT" - ) - source.execute("CREATE TABLE evidence_rows (value TEXT NOT NULL) STRICT") - source.execute("INSERT INTO evidence_rows VALUES ('s11-runtime-baseline')") - source.executemany( - "INSERT INTO facts VALUES (?, ?)", - ( - (number, f"backup-row-{number:03d}") - for number in range(1, 65) - ), - ) - source.commit() - - copy = sqlite3.connect(ROOT / "online-backup-copy.sqlite3") - source.backup(copy, pages=1) - copy.close() - - mismatch = sqlite3.connect(ROOT / "online-backup-digest-mismatch.sqlite3") - source.backup(mismatch, pages=1) - mismatch.execute("INSERT INTO facts VALUES (999, 'digest mismatch')") - mismatch.commit() - mismatch.close() - source.close() - - -def sha256(path: Path) -> str: - digest = hashlib.sha256() - with path.open("rb") as stream: - for block in iter(lambda: stream.read(64 * 1024), b""): - digest.update(block) - return digest.hexdigest() - - -def write_manifest() -> None: - artifacts = { - name: { - "bytes": (ROOT / name).stat().st_size, - "sha256": sha256(ROOT / name), - } - for name in FIXTURE_FILES - } - artifact_manifest = { - "schema": "tracedecay.storage-runtime-evidence.s11.v1", - "generator": { - "script": "generate.py", - "python": sys.version.split()[0], - "sqlite": sqlite3.sqlite_version, - "logical_state_deterministic": True, - "byte_reproducibility": ( - "Byte-for-byte on the recorded Python/SQLite versions; generated WAL " - "salts and checksums are normalized." - ), - }, - "binding": BINDING, - "artifacts": artifacts, - "expectations": { - "wal_pressure_rows": 129, - "crash_receipt_id": "receipt.s11.crash", - "crash_evidence_id": "evidence.s11.crash", - "fts_search_term": "needle", - "authoritative_rows": 200, - "backup_rows": 64, - "replacement_incarnation": 8, - "replacement_authority_epoch": 20, - }, - } - (ROOT / ARTIFACT_MANIFEST).write_text( - json.dumps(artifact_manifest, indent=2, sort_keys=True) + "\n", - encoding="utf-8", - ) - runtime_manifest = { - "schema_version": 1, - "project_root": ".", - "profile_root": ".", - "fts_queries": { - "graph": "needle", - "session": "needle", - }, - "s11": { - "database": "online-backup-source.sqlite3", - "binding": BINDING, - "evidence_tables": ["evidence_rows", "facts"], - }, - } - (ROOT / RUNTIME_MANIFEST).write_text( - json.dumps(runtime_manifest, indent=2, sort_keys=True) + "\n", - encoding="utf-8", - ) - - -def generate() -> None: - ROOT.mkdir(parents=True, exist_ok=True) - for name in ( - *FIXTURE_FILES, - ARTIFACT_MANIFEST, - RUNTIME_MANIFEST, - ): - (ROOT / name).unlink(missing_ok=True) - create_wal_pressure() - create_crash_after_commit() - create_fts_stale() - create_authoritative_corruption() - create_online_backup() - write_manifest() - - -def main() -> None: - global ROOT - parser = argparse.ArgumentParser() - parser.add_argument("--crash-child", type=Path) - parser.add_argument("--output", type=Path, default=ROOT) - arguments = parser.parse_args() - if arguments.crash_child is not None: - crash_child(arguments.crash_child) - else: - ROOT = arguments.output.resolve() - generate() - - -if __name__ == "__main__": - main() diff --git a/tests/fixtures/storage_runtime_evidence/online-backup-copy.sqlite3 b/tests/fixtures/storage_runtime_evidence/online-backup-copy.sqlite3 deleted file mode 100644 index 325653e6b723ff113c5334f22b56e4aba1d4f8a2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5120 zcmeI!J4_T&6b9h?SOi3rAC(nBy*?077Un&(K3Q;M2rKF`BvPQ+g+Vv+l3g%LWwf!- zQd=#x)lyq6rPWd!3oW&=&>5716O64Pb24{s&Ns>3>_2leGkh^ussw6sc7D20Q6@_; z&RMTgj4>XLs&K4U)F)P(aE|)AsQ1qd>(RVUan`+fzdpPmWFM1H!&6*w0vjrUF|jGx z-p-dQh2m5YES4sMnepJt{Ory0nnw2Lvl&0D{7hditJYjn9gBsjg+Te)OTHR9AC8e+ zu2T*B`GJ0aj8`XA9HC^Olah`ON*dY;nQ0uRpCB4U`;gqhw<%B@6YGOl+ZKppKG`&6G6M64KJBq2!^Ol8a50 z98^)Vk)UKjQZgYZ8Q_%kX#Y2e<$vz~)%_dKaRUEy0_Y`=xMSfoB@?G888}Hv#|cUr ojuW!eI7Z1snvx5Tk^`5L4Tq8io018Ok^z&F4ug^gosgCO1q2ei&;S4c diff --git a/tests/fixtures/storage_runtime_evidence/online-backup-digest-mismatch.sqlite3 b/tests/fixtures/storage_runtime_evidence/online-backup-digest-mismatch.sqlite3 deleted file mode 100644 index 63ee01efb58f0d22a73a1806f6bad87ace97dfed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5120 zcmeI$J4{ni7zgn4C;}qtAC-!r9v=t@h5PQUPYO1MP*KYzkpWE~7ii=qEf{4onivNi zb=6T<-OyD>U3JvOK}TI2^cH2{3&z!u`=#fep5IMQ)Bo+4d(+{I>0;Se?8hp@r~HdF#* zVpFuOjW3n6xhdaYEEfEkynkhW_Ihc}LiT4;NiU_mWM4X^)|^u9i`l6KUwNrZo*FtI zjFEJ@Lk)YGfqrj{S4X2!eyeF^|M6_TTv|DZHT$iQYA+Vlz>t?Zo64vQnSpc3%%~bn zjdrQrY~jZG+gBQ#%O901QuTY(f5Oa4@G#;^3h#^#v(DnH0K@WRGl-wc@+@B;JWv z;+c3P?ucvRvKSPuXcZ0oC;!6V^Vj@2f6VXmn|y+MJjuIxGp}La*hlu3y<|_>Lv}m( zfIkIIvW{Ffe|2Fl7Ca-xG|NQ~r8v4NIfzlR(M8EZCnXa{DH-UXq@$gZhBiWG0!Jvh zXr&a#VM-1TQL=H6l7$15OejhQ_EXZakCKMHgp34QD7k2+6vrM)4w@+0*iFeol#+=? zN(OdO(y^10h8=|T1R5y00Hrv#Q*yA4l8vpDEYwpnv4xU>I!Zb=Q_@gNNK2rGl8b6e zacrXGpo)@>2qg=Wk_kb{0H>r^`X6HX-~GS5+QH!*Ch%V*&~SG`vd%(r!Y`H8bg>lt zft8;`FZqc(7EV(#af*_GlazFvprqkAAv=L%lw2ez#ow9a~H#( diff --git a/tests/fixtures/storage_runtime_evidence/online-backup-source.sqlite3 b/tests/fixtures/storage_runtime_evidence/online-backup-source.sqlite3 deleted file mode 100644 index 79669b75092858b7fbe0d995fbbf95a35844adc5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5120 zcmeI!Jxmi}7zgn8qZJTQ{-{(0_4NY*rEuT9`bojY5Grap5*g66aA+f6(t=SYql<%% zy6UK_j=Jh7tB$%j=%|Z>-c=lUf^jwEp4|Js*WV>?bN}m;-pIvVsp6}txrLcRMVTzd zIA?uIF~;JnBUq{>f;qN&9xgl>_2G$kW<(Fybt=w!mhLwM7liC*@@X(d6(z8t5*Qbo z5*;0Uxl$-j`~FgC(x091uPn^nEU)<>2lCmBmsMV-KbKW&E~(C?!t|oAyzC`U4WAFj zXfD^KM!fvsfH%%-5{U%A(^B1ksxVP0S0}M%zbdKD(xe(3_OfTQd37N_crKG4Q$yLY zZdII{ytV%JVUu(DBYYz9-^+%2IMK~S2}B9}HG!BG{x@^^nFU8wQ36o{8#IAtF6^?d zCl?lHE2SAfSuB+O>C&tp_P@sFnfxk0$hY!k@Q(gaF3W3jRQAhW*(U46ckxNAh}Yt| zcr5OT8{)DU60T?$P5dYS!r${Z`~`o)AMo4!D))GX_wZI;%f7LX>>Yc>p0P*lZtw-G z1x>Q9VqxO?;(RiAM@njzi(X18^iXn;q-3L;l7*v`OdO$Ppo@}@PD&a&2$^XdrsSfX zQVNGCIXFnk#sNwe_ER#UC>hvCNylDF8ukz}(rBaPqLoq#yD2$np=4thB?}2kCYmW3 z*hxvp4oVuf6VlUYqT~XUQfQ>)U>hYHTPay+pk!hTB?I-8bZn-ip^lK2MlB^5HI!1= zM9D#%l8qQ83zCuvLCFB8q=)_AC|3U2|9|>7TB8L1=LFD4e&UXW)09k{qGaGCB^@Uy pX*f>EPU9FQ7imf0h<~yV(E$ diff --git a/tests/fixtures/storage_runtime_evidence/storage-runtime-fixture-v1.json b/tests/fixtures/storage_runtime_evidence/storage-runtime-fixture-v1.json deleted file mode 100644 index 34b03f9e2a..0000000000 --- a/tests/fixtures/storage_runtime_evidence/storage-runtime-fixture-v1.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "fts_queries": { - "graph": "needle", - "session": "needle" - }, - "profile_root": ".", - "project_root": ".", - "s11": { - "binding": { - "authority_epoch": 19, - "incarnation": 7, - "shard_id": { - "brain_id": "brain.s11.evidence", - "profile_id": "profile.s11.evidence", - "scope": { - "kind": "project", - "project_id": "project.s11.evidence" - } - } - }, - "database": "online-backup-source.sqlite3", - "evidence_tables": [ - "evidence_rows", - "facts" - ] - }, - "schema_version": 1 -} diff --git a/tests/fixtures/storage_runtime_evidence/validate.py b/tests/fixtures/storage_runtime_evidence/validate.py deleted file mode 100644 index eb9fa95b5f..0000000000 --- a/tests/fixtures/storage_runtime_evidence/validate.py +++ /dev/null @@ -1,265 +0,0 @@ -#!/usr/bin/env python3 -"""Static validation for checked-in S11 SQLite runtime evidence.""" - -from __future__ import annotations - -import hashlib -import importlib.util -import json -import shutil -import sqlite3 -import subprocess -import sys -import tempfile -from pathlib import Path - - -ROOT = Path(__file__).resolve().parent -ARTIFACT_MANIFEST = "artifacts.json" -RUNTIME_MANIFEST = "storage-runtime-fixture-v1.json" -EXPECTED_ARTIFACTS = { - "wal-pressure.sqlite3", - "wal-pressure.sqlite3-wal", - "wal-pressure.blocker.json", - "crash-after-repair-commit.sqlite3", - "fts-stale.sqlite3", - "authoritative-corrupt.sqlite3", - "online-backup-source.sqlite3", - "online-backup-copy.sqlite3", - "online-backup-digest-mismatch.sqlite3", -} - - -def sha256(path: Path) -> str: - digest = hashlib.sha256() - with path.open("rb") as stream: - for block in iter(lambda: stream.read(64 * 1024), b""): - digest.update(block) - return digest.hexdigest() - - -def quick_check(connection: sqlite3.Connection) -> list[str]: - return [str(row[0]) for row in connection.execute("PRAGMA quick_check")] - - -def validate_hashes(manifest: dict[str, object]) -> None: - artifacts = manifest["artifacts"] - assert isinstance(artifacts, dict) - assert set(artifacts) == EXPECTED_ARTIFACTS - for name, expected in artifacts.items(): - assert isinstance(name, str) - assert isinstance(expected, dict) - assert set(expected) == {"bytes", "sha256"} - assert isinstance(expected["bytes"], int) and expected["bytes"] > 0 - assert ( - isinstance(expected["sha256"], str) - and len(expected["sha256"]) == 64 - and all(character in "0123456789abcdef" for character in expected["sha256"]) - ) - path = ROOT / name - assert path.is_file(), f"missing fixture artifact: {name}" - assert path.stat().st_size == expected["bytes"], name - assert sha256(path) == expected["sha256"], name - content = path.read_bytes() - for live_root in (b"/home/", b"/fast/", b".tracedecay/"): - assert live_root not in content, f"live profile path leaked into {name}" - discovered = { - path.name - for path in ROOT.iterdir() - if path.is_file() - and ( - path.name.endswith(".sqlite3") - or path.name.endswith(".sqlite3-wal") - or path.name.endswith(".sqlite3-shm") - or path.name.endswith(".sqlite3-journal") - or path.name.endswith(".blocker.json") - ) - } - assert discovered == EXPECTED_ARTIFACTS - - -def validate_wal(manifest: dict[str, object]) -> None: - with tempfile.TemporaryDirectory(prefix="tracedecay-s11-wal-validate-") as temporary: - database = Path(temporary) / "wal-pressure.sqlite3" - shutil.copyfile(ROOT / database.name, database) - shutil.copyfile(ROOT / f"{database.name}-wal", Path(f"{database}-wal")) - connection = sqlite3.connect(database) - assert quick_check(connection) == ["ok"] - assert connection.execute("PRAGMA journal_mode").fetchone()[0].lower() == "wal" - assert ( - connection.execute("SELECT count(*) FROM events").fetchone()[0] - == manifest["expectations"]["wal_pressure_rows"] - ) - connection.close() - - blocker = json.loads((ROOT / "wal-pressure.blocker.json").read_text()) - assert blocker["snapshot_row_count"] == 1 - assert blocker["committed_row_count"] == manifest["expectations"]["wal_pressure_rows"] - assert blocker["expected"]["hard_drain_required"] is True - assert (ROOT / "wal-pressure.sqlite3-wal").stat().st_size > 32 - - -def validate_crash(manifest: dict[str, object]) -> None: - connection = sqlite3.connect(ROOT / "crash-after-repair-commit.sqlite3") - assert quick_check(connection) == ["ok"] - assert ( - connection.execute( - "SELECT count(*) FROM documents_fts WHERE documents_fts MATCH 'needle'" - ).fetchone()[0] - == 1 - ) - receipt = connection.execute( - "SELECT receipt_id, evidence_id, binding FROM tracedecay_repair_receipts" - ).fetchone() - assert json.loads(receipt[0]) == manifest["expectations"]["crash_receipt_id"] - assert json.loads(receipt[1]) == manifest["expectations"]["crash_evidence_id"] - assert json.loads(receipt[2]) == manifest["binding"] - connection.close() - - -def validate_fts_repair(manifest: dict[str, object]) -> None: - with tempfile.TemporaryDirectory(prefix="tracedecay-s11-fts-validate-") as temporary: - database = Path(temporary) / "fts-stale.sqlite3" - shutil.copyfile(ROOT / database.name, database) - connection = sqlite3.connect(database) - assert quick_check(connection) == ["ok"] - term = manifest["expectations"]["fts_search_term"] - assert ( - connection.execute( - "SELECT count(*) FROM documents WHERE body LIKE ?", - (f"%{term}%",), - ).fetchone()[0] - == 1 - ) - assert ( - connection.execute( - "SELECT count(*) FROM documents_fts WHERE documents_fts MATCH ?", - (term,), - ).fetchone()[0] - == 0 - ) - connection.execute("INSERT INTO documents_fts(documents_fts) VALUES ('rebuild')") - connection.commit() - assert ( - connection.execute( - "SELECT count(*) FROM documents_fts WHERE documents_fts MATCH ?", - (term,), - ).fetchone()[0] - == 1 - ) - assert quick_check(connection) == ["ok"] - connection.close() - - -def validate_authoritative_corruption(manifest: dict[str, object]) -> None: - connection = sqlite3.connect(ROOT / "authoritative-corrupt.sqlite3") - messages = quick_check(connection) - assert messages != ["ok"] - assert any("database main" in message or "facts" in message for message in messages) - assert ( - connection.execute("SELECT count(*) FROM facts").fetchone()[0] - == manifest["expectations"]["authoritative_rows"] - ) - connection.close() - - -def validate_online_backup(manifest: dict[str, object]) -> None: - rows = [] - contents = [] - for name in ( - "online-backup-source.sqlite3", - "online-backup-copy.sqlite3", - "online-backup-digest-mismatch.sqlite3", - ): - connection = sqlite3.connect(ROOT / name) - assert quick_check(connection) == ["ok"] - rows.append(connection.execute("SELECT count(*) FROM facts").fetchone()[0]) - assert connection.execute("SELECT count(*) FROM evidence_rows").fetchone()[0] == 1 - contents.append(connection.execute("SELECT id, body FROM facts ORDER BY id").fetchall()) - connection.close() - assert rows == [manifest["expectations"]["backup_rows"]] * 2 + [ - manifest["expectations"]["backup_rows"] + 1 - ] - assert contents[0] == contents[1] - assert sha256(ROOT / "online-backup-copy.sqlite3") != sha256( - ROOT / "online-backup-digest-mismatch.sqlite3" - ) - - -def validate_reproducible_generation() -> None: - with tempfile.TemporaryDirectory(prefix="tracedecay-s11-reproducible-") as temporary: - regenerated = Path(temporary) - subprocess.run( - [ - sys.executable, - str(ROOT / "generate.py"), - "--output", - str(regenerated), - ], - check=True, - ) - expected = EXPECTED_ARTIFACTS | {ARTIFACT_MANIFEST, RUNTIME_MANIFEST} - assert {path.name for path in regenerated.iterdir()} == expected - for name in sorted(expected): - assert (regenerated / name).read_bytes() == (ROOT / name).read_bytes(), name - - -def validate_consumers() -> None: - repository = ROOT.parents[2] - adapter_path = repository / "benchmark_data/runtime/storage_workloads.py" - specification = importlib.util.spec_from_file_location( - "storage_runtime_workload_kernel", - adapter_path, - ) - assert specification is not None and specification.loader is not None - adapter = importlib.util.module_from_spec(specification) - sys.modules[specification.name] = adapter - specification.loader.exec_module(adapter) - adapter.validate_workloads() - assert adapter.BENCHMARK_AUTHORITY == "measurement_fixture_not_product_contract" - assert "tracedecay-store" in adapter.DECLARED_CRATE_LANES - assert "tracedecay-rusqlite-runtime" in adapter.DECLARED_CRATE_LANES - - identities = adapter.runtime_test_identities( - platform="fixture", - shard="storage-evidence", - storage_mode="isolated-sqlite", - ) - assert any( - identity.crate_tag == "tracedecay-rusqlite-runtime" - for identity in identities - ) - - -def main() -> None: - manifest = json.loads((ROOT / ARTIFACT_MANIFEST).read_text(encoding="utf-8")) - assert manifest["schema"] == "tracedecay.storage-runtime-evidence.s11.v1" - runtime_manifest = json.loads((ROOT / RUNTIME_MANIFEST).read_text(encoding="utf-8")) - assert runtime_manifest == { - "schema_version": 1, - "project_root": ".", - "profile_root": ".", - "fts_queries": {"graph": "needle", "session": "needle"}, - "s11": { - "database": "online-backup-source.sqlite3", - "binding": manifest["binding"], - "evidence_tables": ["evidence_rows", "facts"], - }, - } - validate_hashes(manifest) - validate_wal(manifest) - validate_crash(manifest) - validate_fts_repair(manifest) - validate_authoritative_corruption(manifest) - validate_online_backup(manifest) - validate_reproducible_generation() - validate_consumers() - print( - "validated deterministic S11 SQLite evidence: canonical manifest, " - "WAL blocker, crash receipt, FTS repair, quarantine source, " - "online backup, and digest mismatch" - ) - - -if __name__ == "__main__": - main() diff --git a/tests/fixtures/storage_runtime_evidence/wal-pressure.blocker.json b/tests/fixtures/storage_runtime_evidence/wal-pressure.blocker.json deleted file mode 100644 index 6f32f3d5a5..0000000000 --- a/tests/fixtures/storage_runtime_evidence/wal-pressure.blocker.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "committed_row_count": 129, - "expected": { - "checkpointed_less_than_log_while_blocked": true, - "hard_drain_required": true, - "wal_frames_minimum": 1 - }, - "lease_id": "snapshot.s11.wal-pressure", - "schema": "tracedecay.storage-runtime-evidence.wal-blocker.v1", - "snapshot_row_count": 1 -} diff --git a/tests/fixtures/storage_runtime_evidence/wal-pressure.sqlite3 b/tests/fixtures/storage_runtime_evidence/wal-pressure.sqlite3 deleted file mode 100644 index 857dce3c686e2ce47e2cc5151bcdde50bfda102b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 134144 zcmeI0eW=xU9mmh_`_(xgHg%gib(?3KZt9$FQ|H5c_;~eBo!6)9_1SG+^}6}|bam5h zI_EyniV7;Ipn?i2sGx!hDyX1>3M#0ef(k0Apn?i2sG!e525bE#Z|KwL_2b<4J?F=H zbAPG&s;cc;fW2{=xR#){s^yEo--1t$5mMU5TGpk0zdU&b@kcUXSd& z=v%er!B~pW&5GR{@uoVvu2>c%sxICd5Heuq#=B43-{rip%44fM{K6vJ8 zmX*ETEBdzLeM4i77n@6+O|I0x*T2+1 z*5B1%*I(41)*sdH*YDJC*00qs*Duu1)=$-s*N@Z>*7wzS*LT#n);HBF^a3^Yi8>%@3RJHQ#Q& z(R{V}QuF!dGtDQPk2N1|KG3|kc~|rH=FN>?h4A11=X~jM=Wkx_Oz`5xh)XX-Tzo#_ zh36ukzYy`<`G^b8Mx1{p;@NW%&zy}oHyCmDOvK>nh%=`mPM?f8bt2;A@rV=0B90%8 zICdoB=;4SXhawI?9dYQXh^IRr%ufwO96S&)us`C!zKH$(5&QN=^gkJ~_lbxnACGuq zPsHQ9Blhfy*u67i*N%vt+aq>ti`c$3Vq0It)-4fzy%AeBNAzxr*t{`f(}swR>mxSw zM66#I(eqfuy0sCHt%+E>I%3VE5v#i+9$gjDy)t4|SH#K{5nUFsVtIrui&(xiV%d_2 zrH@1`Ssd}mqKL%{BNi=)Somb}HMa-HRao>!HnbRX?+#50do``#=McgwrV%psiQ}2qn`_72F+7Wk7iD=&uG3EA% zJ8p}(eR9NYlOiThjF@z5#KZ{^w~mjPFfL;JEfM2xj=1Hfh?~bo+%zU)?C6LwH%5%^ z++J@S6>&r7Zaiv4#K_?hBd(7aJ}lz;>mr5?jkvCJ10Px=hLnifbjnS$v=tAIYbD^; z3~9ZkgOmdTZ%6|8FU;mv@L%9ZHJt$d3lq3i{1^C9O(%f=!US$Z@L%9ZHJt$d3lq2v z#eabx)pP>*FHGPT->35ZpFogmG6DP-25=jO{{la%=>+g!n859N{1^C9O(%f=!US%^ z@n7IaHJt$d3lq4Fz<+@s)pP>*FHGPz68{B$RMQFIzc7K@DEt@rQB5a+|H1@rH{id( zk7_yr{1+y0yAl5depJ&5;J+||+i3h3_)$$Kfd9e-Ze#FY;72u`0R9UTxQ)esfgja$ z0{AaX;C2)K3;d|26Tp9A0=Jv-U*JbIodEs|6S&=i{{la%=>+g!n80lu{tNu5rW3$_ zVFI`D_%HCInoa=!g$djy;J?6+YB~Y@7bbAK75@c(RMQFIzc7K@MEn={QB5a+|H1@r zlki{QM>U-Q{tFYhO~!wLAJudM_%BT0b{qZ+{HUfAz<*%^x7+bw;72u`0R9UTxZQ#O z0zazh1n^&&z-+g!n859B{1^C9O(%f=!US$p@n7IaHJt$d3lq3a!+(Jv)pP>*FHGQe5B>}MsHPLZ ze_;Z*d+}f3M>U-Q{tFYhO~-$MAJudM_%BT0HUs|!epJ&5;J+||+f4iy_)$$Kfd9e- zZujB8z>jJ=0sI#xaGQny0zazh1n^&&!0mqg7x+<4CxHLL1a7nOU*JbIodEs|6S&R6 ze}Ny>bOQJk7_yr{1+y0dl>%(epJ&5;J+||+XDO-_)$$K zfd9e-ZVT~W;72u`0R9UTxGlnefgja$0{AaX;I+g!n80lr{tNu5rW3$_VFI`1_%HCInoa=!g$djY z{{?ML3;d|26Tp9A0=EtLFYu$9P5}Rf3EVc~zrc@bIsyC_CUDz?{{la%=>+g!n80l_ z{tNu5rW3$_VFI^a{1^C9O(%f=!US$x@L%9ZHJt$d3lq5Y;lIF-YB~Y@7bbAqivI#X zs_6vqUzosc8~zLYsHPLZe_;Z*?f5V7qnb_t|Ah(McHqCjk7_yr{1+y0+ll`IKdR{j z@L!m~Z5RFv{HUfAz<*%^x83+J@S~be0RM#v-1gwVz>jJ=0sI#xaC;p81%6c13E;ml zf!h=KFYu$9P5}Rf3EZBbOQJbOQJ*FHGPzi2njVs_6vqUzot{EdC4psHPLZe_;Z*bNDatqnb_t|Ah(Mp22^C zAJudM_%BT0_ALGj{HUfAz<*%^xAXWf@S~be0RM#v+%Dk1z>jJ=0sI#xaC;8_1%6c1 z3E;mlf!p)=FYu$9P5}Rf3EW=5e}Ny>bOQJ Date: Sun, 20 Sep 2026 18:32:34 +0000 Subject: [PATCH 023/182] simplify(pass-2/5): run packager tests once, not per target Co-authored-by: Zack Jackson --- .github/workflows/ci.yml | 17 +++++++++++++++++ .github/workflows/distribution-acceptance.yml | 13 ------------- .github/workflows/release-beta.yml | 13 ------------- .github/workflows/release.yml | 15 --------------- 4 files changed, 17 insertions(+), 41 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57cb244d65..c2b8228842 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -191,6 +191,23 @@ jobs: python3 scripts/test-rust-cache-lineage.py python3 scripts/check-rust-cache-lineage.py + # Packager unit tests. They do not need a release binary, so they run + # once here instead of once per release matrix target. + - name: Test release packagers + run: | + python3 scripts/test-check-packaged-lsp-bridge.py + python3 scripts/test-build-mcpb.py + python3 scripts/test-resolve-installed-binary.py + python3 scripts/test-check-packaged-mcp-stdio.py + python3 scripts/test-check-distribution-feature-wiring.py + python3 scripts/test-resolve-release-source-profile.py + python3 scripts/test-package-release-archive.py + python3 scripts/test-plan-release-recovery.py + python3 scripts/test-check-release-artifacts.py + bash scripts/test-check-distribution-clean-source.sh + bash scripts/test-check-distribution-snapshot.sh + bash scripts/test-check-distribution-reuse.sh + # Release guards: drift, safety, installer, PR integrity, bundle check. # Pull requests only; the release PR itself is exempt because it is # the one change allowed to move the version authorities. diff --git a/.github/workflows/distribution-acceptance.yml b/.github/workflows/distribution-acceptance.yml index 5cd52d26f8..af40d336de 100644 --- a/.github/workflows/distribution-acceptance.yml +++ b/.github/workflows/distribution-acceptance.yml @@ -57,19 +57,6 @@ jobs: echo "TRACEDECAY_SKIP_DASHBOARD_BUILD=1" >> "$GITHUB_ENV" echo "TRACEDECAY_DASHBOARD_BUNDLE_SHA256=${digest}" >> "$GITHUB_ENV" - - name: Validate portable distribution harnesses - run: | - python3 scripts/test-check-packaged-lsp-bridge.py - python3 scripts/test-build-mcpb.py - python3 scripts/test-resolve-installed-binary.py - python3 scripts/test-check-packaged-mcp-stdio.py - python3 scripts/test-check-distribution-feature-wiring.py - python3 scripts/test-resolve-release-source-profile.py - python3 scripts/test-package-release-archive.py - bash scripts/test-check-distribution-clean-source.sh - bash scripts/test-check-distribution-snapshot.sh - bash scripts/test-check-distribution-reuse.sh - # rust-toolchain.toml is the only compiler this job may install. See # release-beta.yml: floating stable fragments rust-cache's env key. - name: Resolve pinned toolchain diff --git a/.github/workflows/release-beta.yml b/.github/workflows/release-beta.yml index 8154bf0c57..1a123daee0 100644 --- a/.github/workflows/release-beta.yml +++ b/.github/workflows/release-beta.yml @@ -134,19 +134,6 @@ jobs: digest="$(python3 scripts/check-dashboard-bundle.py dashboard/app-dist --print-digest)" echo "TRACEDECAY_DASHBOARD_BUNDLE_SHA256=${digest}" >> "$GITHUB_ENV" - - name: Validate portable distribution harnesses - run: | - python3 scripts/test-check-packaged-lsp-bridge.py - python3 scripts/test-build-mcpb.py - python3 scripts/test-resolve-installed-binary.py - python3 scripts/test-check-packaged-mcp-stdio.py - python3 scripts/test-check-distribution-feature-wiring.py - python3 scripts/test-resolve-release-source-profile.py - python3 scripts/test-package-release-archive.py - bash scripts/test-check-distribution-clean-source.sh - bash scripts/test-check-distribution-snapshot.sh - bash scripts/test-check-distribution-reuse.sh - # rust-toolchain.toml is the only compiler this job may install. # dtolnay/rust-toolchain@stable also installs floating stable, and # rust-cache hashes every rustc into the environment key, so each diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5d3b775d4b..08ab2a4264 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -176,21 +176,6 @@ jobs: digest="$(python3 scripts/check-dashboard-bundle.py dashboard/app-dist --print-digest)" echo "TRACEDECAY_DASHBOARD_BUNDLE_SHA256=${digest}" >> "$GITHUB_ENV" - - name: Validate portable distribution harnesses - run: | - python3 scripts/test-check-packaged-lsp-bridge.py - python3 scripts/test-build-mcpb.py - python3 scripts/test-resolve-installed-binary.py - python3 scripts/test-check-packaged-mcp-stdio.py - python3 scripts/test-check-distribution-feature-wiring.py - python3 scripts/test-resolve-release-source-profile.py - python3 scripts/test-package-release-archive.py - python3 scripts/test-plan-release-recovery.py - python3 scripts/test-check-release-artifacts.py - bash scripts/test-check-distribution-clean-source.sh - bash scripts/test-check-distribution-snapshot.sh - bash scripts/test-check-distribution-reuse.sh - # rust-toolchain.toml is the only compiler this job may install. # Installing floating stable as well fragments rust-cache's environment # key whenever stable moves (measured on Release Beta 35271070760). From 643ecb71503ea16374714dbdabf75c62fbbe208f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:32:58 +0000 Subject: [PATCH 024/182] simplify(pass-2/5): share storage-runtime test seeding The reader and checkpoint journeys repeated the same WAL seed and reader-budget overlay. One helper keeps both behaviors. Co-authored-by: Zack Jackson --- .../runtime_operations.rs | 10 +--- .../runtime_reader.rs | 51 ++++--------------- .../runtime_test_support.rs | 46 ++++++++++++++--- 3 files changed, 50 insertions(+), 57 deletions(-) diff --git a/tests/storage_runtime_rusqlite_suite/runtime_operations.rs b/tests/storage_runtime_rusqlite_suite/runtime_operations.rs index 9fca99e288..b5c0aa1d35 100644 --- a/tests/storage_runtime_rusqlite_suite/runtime_operations.rs +++ b/tests/storage_runtime_rusqlite_suite/runtime_operations.rs @@ -7,6 +7,7 @@ use tracedecay_store::AdmissionConfigV1; use super::runtime_test_support::{ CountExecutor, Probe, TestDatabase, maintenance_binding, read_request, reader_locator, + seed_acceptance_rows, }; #[test] @@ -14,14 +15,7 @@ fn checkpoint_health_exposes_wal_pressure_while_a_snapshot_blocks_progress() { let binding = maintenance_binding(); let database = TestDatabase::new("runtime-checkpoint.sqlite3"); let mut writer = database.connect(); - writer - .execute_batch( - "PRAGMA journal_mode=WAL; - PRAGMA wal_autocheckpoint=0; - CREATE TABLE acceptance_rows(value INTEGER NOT NULL); - INSERT INTO acceptance_rows(value) VALUES (1);", - ) - .expect("seed checkpoint authority"); + seed_acceptance_rows(&writer, true); let pool = ReaderPool::start( reader_locator(&binding, &database.path), AdmissionConfigV1::default().readers, diff --git a/tests/storage_runtime_rusqlite_suite/runtime_reader.rs b/tests/storage_runtime_rusqlite_suite/runtime_reader.rs index eca8535807..3b8f614961 100644 --- a/tests/storage_runtime_rusqlite_suite/runtime_reader.rs +++ b/tests/storage_runtime_rusqlite_suite/runtime_reader.rs @@ -7,35 +7,22 @@ use tracedecay_rusqlite_runtime::{ runtime::{IntegrityResult, SqliteDoctorHealthLane}, watermark::CommittedWatermarkPublisher, }; -use tracedecay_store::{ - AdmissionConfigV1, CommitSequenceV1, OperationPriorityV1, ShardWatermarkV1, - StoreCommitReceiptV1, UnavailableReasonV1, -}; +use tracedecay_store::{OperationPriorityV1, StoreCommitReceiptV1, UnavailableReasonV1}; use super::runtime_test_support::{ - CountExecutor, Probe, TestDatabase, read_request, reader_locator, reader_runtime_fixture, + CountExecutor, Probe, TestDatabase, acceptance_reader_budget, read_request, reader_locator, + reader_runtime_fixture, seed_acceptance_rows, shard_watermark, }; #[test] fn reader_drain_preserves_inflight_and_reserved_health_capacity() { let fixture = reader_runtime_fixture(); let database = TestDatabase::new("runtime-reader.sqlite3"); - let connection = database.connect(); - connection - .execute_batch( - "PRAGMA journal_mode=WAL; - CREATE TABLE acceptance_rows(value INTEGER NOT NULL); - INSERT INTO acceptance_rows(value) VALUES (1);", - ) - .expect("seed reader authority"); + seed_acceptance_rows(&database.connect(), false); - let mut budget = AdmissionConfigV1::default().readers; - budget.min_per_hot_shard = fixture.reader_budget.min_per_hot_shard; - budget.max_per_hot_shard = fixture.reader_budget.max_per_hot_shard; - budget.idle_burst_retire_ms = fixture.reader_budget.idle_burst_retire_ms; let pool = ReaderPool::start( reader_locator(&fixture.binding, &database.path), - budget, + acceptance_reader_budget(&fixture), CountExecutor, ) .expect("start reader pool"); @@ -77,22 +64,11 @@ fn reader_drain_preserves_inflight_and_reserved_health_capacity() { fn doctor_health_and_commit_watermark_report_the_same_runtime_binding() { let fixture = reader_runtime_fixture(); let database = TestDatabase::new("runtime-health.sqlite3"); - let connection = database.connect(); - connection - .execute_batch( - "PRAGMA journal_mode=WAL; - CREATE TABLE acceptance_rows(value INTEGER NOT NULL); - INSERT INTO acceptance_rows(value) VALUES (1);", - ) - .expect("seed health authority"); + seed_acceptance_rows(&database.connect(), false); - let mut budget = AdmissionConfigV1::default().readers; - budget.min_per_hot_shard = fixture.reader_budget.min_per_hot_shard; - budget.max_per_hot_shard = fixture.reader_budget.max_per_hot_shard; - budget.idle_burst_retire_ms = fixture.reader_budget.idle_burst_retire_ms; let pool = ReaderPool::start( reader_locator(&fixture.binding, &database.path), - budget, + acceptance_reader_budget(&fixture), CountExecutor, ) .expect("start reader pool"); @@ -105,7 +81,7 @@ fn doctor_health_and_commit_watermark_report_the_same_runtime_binding() { assert_eq!(health.integrity_check, Some(IntegrityResult::Healthy)); assert_eq!(health.available_health_readers, 1); - let publisher = CommittedWatermarkPublisher::with_initial_watermarks([watermark( + let publisher = CommittedWatermarkPublisher::with_initial_watermarks([shard_watermark( &fixture.binding, fixture.initial_commit_sequence, )]) @@ -128,7 +104,7 @@ fn doctor_health_and_commit_watermark_report_the_same_runtime_binding() { .expect("publish monotonic watermark"); assert_eq!( publisher.subscribe().current(&fixture.binding.shard_id), - WatermarkSourceState::Available(watermark( + WatermarkSourceState::Available(shard_watermark( &fixture.binding, fixture.published_commit_sequence )) @@ -137,12 +113,3 @@ fn doctor_health_and_commit_watermark_report_the_same_runtime_binding() { let health_request = read_request(&fixture.binding, "health"); assert_eq!(health_request.priority(), OperationPriorityV1::Health); } - -fn watermark(binding: &tracedecay_store::StoreRuntimeBindingV1, sequence: u64) -> ShardWatermarkV1 { - ShardWatermarkV1 { - shard_id: binding.shard_id.clone(), - incarnation: binding.incarnation, - authority_epoch: binding.authority_epoch, - commit_sequence: CommitSequenceV1(sequence), - } -} diff --git a/tests/storage_runtime_rusqlite_suite/runtime_test_support.rs b/tests/storage_runtime_rusqlite_suite/runtime_test_support.rs index a9d79f2b0b..1e100dc992 100644 --- a/tests/storage_runtime_rusqlite_suite/runtime_test_support.rs +++ b/tests/storage_runtime_rusqlite_suite/runtime_test_support.rs @@ -15,13 +15,13 @@ use tracedecay_rusqlite_runtime::{ reader::{ExistingReaderLocator, ReaderQueryExecutor}, }; use tracedecay_store::{ - AdmissionConfigV1, CommitSequenceV1, LocatorDigest, RepositoryOperationEnvelopeV1, - RepositoryWritePayloadV1, RuntimeBatchCompatibilityV1, RuntimeCancellationIdentityV1, - RuntimeDeadlineV1, RuntimeInterruptionV1, RuntimeReadCoverageV1, RuntimeReadOutcomeV1, - RuntimeReadRequestV1, RuntimeReadResultV1, RuntimeRequestControlV1, RuntimeRequestProbeV1, - RuntimeSubmitRequestV1, RuntimeTransactionIdV1, RuntimeTransactionScopeV1, ShardWatermarkV1, - StorageRuntimeErrorV1, StoreOperationMetadataV1, StoreRuntimeBindingV1, - TransactionalOutboxEntryV1, VerifiedStoreLocatorV1, + AdmissionConfigV1, CommitSequenceV1, LocatorDigest, ReaderBudgetV1, + RepositoryOperationEnvelopeV1, RepositoryWritePayloadV1, RuntimeBatchCompatibilityV1, + RuntimeCancellationIdentityV1, RuntimeDeadlineV1, RuntimeInterruptionV1, RuntimeReadCoverageV1, + RuntimeReadOutcomeV1, RuntimeReadRequestV1, RuntimeReadResultV1, RuntimeRequestControlV1, + RuntimeRequestProbeV1, RuntimeSubmitRequestV1, RuntimeTransactionIdV1, + RuntimeTransactionScopeV1, ShardWatermarkV1, StorageRuntimeErrorV1, StoreOperationMetadataV1, + StoreRuntimeBindingV1, TransactionalOutboxEntryV1, VerifiedStoreLocatorV1, }; pub(crate) struct ReaderRuntimeFixture { @@ -45,6 +45,38 @@ pub(crate) struct WriterRuntimeFixture { pub(crate) commit_sequences: [u64; 2], } +pub(crate) fn seed_acceptance_rows(connection: &Connection, suspend_autocheckpoint: bool) { + let autocheckpoint = if suspend_autocheckpoint { + "PRAGMA wal_autocheckpoint=0;\n" + } else { + "" + }; + connection + .execute_batch(&format!( + "PRAGMA journal_mode=WAL; + {autocheckpoint}CREATE TABLE acceptance_rows(value INTEGER NOT NULL); + INSERT INTO acceptance_rows(value) VALUES (1);" + )) + .expect("seed acceptance authority"); +} + +pub(crate) fn shard_watermark(binding: &StoreRuntimeBindingV1, sequence: u64) -> ShardWatermarkV1 { + ShardWatermarkV1 { + shard_id: binding.shard_id.clone(), + incarnation: binding.incarnation, + authority_epoch: binding.authority_epoch, + commit_sequence: CommitSequenceV1(sequence), + } +} + +pub(crate) fn acceptance_reader_budget(fixture: &ReaderRuntimeFixture) -> ReaderBudgetV1 { + let mut budget = AdmissionConfigV1::default().readers; + budget.min_per_hot_shard = fixture.reader_budget.min_per_hot_shard; + budget.max_per_hot_shard = fixture.reader_budget.max_per_hot_shard; + budget.idle_burst_retire_ms = fixture.reader_budget.idle_burst_retire_ms; + budget +} + pub(crate) fn reader_runtime_fixture() -> ReaderRuntimeFixture { ReaderRuntimeFixture { binding: serde_json::from_value(json!({ From 2a0964a27eecb3dd07add7294b2bf2a49fd6898d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:33:02 +0000 Subject: [PATCH 025/182] simplify(pass-3/5): skip cargo tree on release targets Co-authored-by: Zack Jackson --- .github/workflows/ci.yml | 5 +++ .github/workflows/release-beta.yml | 11 ++--- .github/workflows/release.yml | 9 ++-- scripts/check-production-feature-profile.py | 8 ++++ scripts/resolve-release-source-profile.py | 8 +++- .../test-resolve-release-source-profile.py | 43 +++++++++++++++++++ 6 files changed, 71 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2b8228842..d9924446d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1064,6 +1064,11 @@ jobs: # forks on the local feature while hotpath::io! expands on the backend. cargo check --workspace --all-targets --features hotpath/hotpath --locked + # One graph walk for the whole push. Release jobs only read the manifest; + # repeating cargo tree on every target does not change the binary. + - name: Check the production feature graph + run: python3 scripts/check-production-feature-profile.py + # The configuration users actually receive. `scripts/resolve-release-source-profile.py` # resolves the release build to `--no-default-features --features production` # on the release profile, and nothing on a push compiled it: `debug-cli` is a diff --git a/.github/workflows/release-beta.yml b/.github/workflows/release-beta.yml index 1a123daee0..83899582d5 100644 --- a/.github/workflows/release-beta.yml +++ b/.github/workflows/release-beta.yml @@ -150,13 +150,10 @@ jobs: toolchain: ${{ steps.pin.outputs.toolchain }} targets: ${{ matrix.target }} - # Restore before the first cargo invocation. resolve-release-source-profile - # runs `cargo tree` twice (~4 min cold on beta.40) and used to do that - # before rust-cache, so a warm registry sitting in Actions cache never - # reached the step that needed it. Tag runs can restore only same-ref or - # default-branch caches; this shared-key must stay stable so a later - # master writer can seed them. cache-workspace-crates stays false: the - # 10 GB repo budget is already full of CI lineages (~13.8 GB / 28 entries). + # Restore before the packaging compile. Tag runs can restore only + # same-ref or default-branch caches; this shared-key must stay stable so + # a later master writer can seed them. cache-workspace-crates stays + # false: the 10 GB repo budget is already full of CI lineages. - name: Cache Rust build uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 08ab2a4264..1a34c10075 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -190,11 +190,10 @@ jobs: toolchain: ${{ steps.pin.outputs.toolchain }} targets: ${{ matrix.target }} - # Restore before the first cargo invocation (`cargo tree` in the - # profile resolver). Tag runs can restore only same-ref or default-branch - # caches; keep this shared-key stable so a later master writer can seed - # them. cache-workspace-crates stays false so release blobs stay inside - # the 10 GB GitHub cache budget. + # Restore before the packaging compile. Tag runs can restore only + # same-ref or default-branch caches; keep this shared-key stable so a + # later master writer can seed them. cache-workspace-crates stays false + # so release blobs stay inside the 10 GB GitHub cache budget. - name: Cache Rust build uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: diff --git a/scripts/check-production-feature-profile.py b/scripts/check-production-feature-profile.py index f90d9267d5..07cc464151 100755 --- a/scripts/check-production-feature-profile.py +++ b/scripts/check-production-feature-profile.py @@ -48,6 +48,11 @@ def main() -> int: type=Path, default=Path(__file__).resolve().parent.parent, ) + parser.add_argument( + "--manifest-only", + action="store_true", + help="check the feature table and skip cargo tree", + ) arguments = parser.parse_args() repo = arguments.repo.resolve() @@ -66,6 +71,9 @@ def main() -> int: raise SystemExit("production feature set lost a required member") if "test-transport" in features["production"]: raise SystemExit("production feature directly enables test-transport") + if arguments.manifest_only: + print("production feature manifest is eligible for release") + return 0 # `cargo metadata` unifies dev-dependency features across the workspace and # therefore makes test-only transports look production-reachable. Inspect diff --git a/scripts/resolve-release-source-profile.py b/scripts/resolve-release-source-profile.py index 78c0428ec0..0e83bbc2be 100755 --- a/scripts/resolve-release-source-profile.py +++ b/scripts/resolve-release-source-profile.py @@ -74,7 +74,13 @@ def main() -> int: if "production" in features: checker = Path(__file__).with_name("check-production-feature-profile.py") subprocess.run( - [sys.executable, str(checker), "--repo", str(source)], + [ + sys.executable, + str(checker), + "--repo", + str(source), + "--manifest-only", + ], check=True, ) profile = "production" diff --git a/scripts/test-resolve-release-source-profile.py b/scripts/test-resolve-release-source-profile.py index c97daad52b..7632b8f7ed 100755 --- a/scripts/test-resolve-release-source-profile.py +++ b/scripts/test-resolve-release-source-profile.py @@ -104,6 +104,49 @@ def main() -> int: f"unexpected historical macOS features: {historical_macos!r}" ) + production = run_fixture( + """[package] +name = "tracedecay" +version = "0.1.0" +edition = "2024" + +[features] +default = ["production"] +production = ["token-counting", "lite", "full"] +token-counting = [] +lite = [] +full = [] +""" + ) + if production.returncode != 0: + raise SystemExit(production.stderr) + if production.github_output != ( + "profile=production\n" + "cargo_args=--no-default-features --features production\n" + "cargo_features=production\n" + ): + raise SystemExit( + f"unexpected production profile output: {production.github_output!r}" + ) + + contaminated_production = run_fixture( + """[package] +name = "tracedecay" +version = "0.1.0" +edition = "2024" + +[features] +default = ["production"] +production = ["token-counting", "lite", "full", "test-transport"] +token-counting = [] +lite = [] +full = [] +test-transport = [] +""" + ) + if contaminated_production.returncode == 0: + raise SystemExit("production test-transport contamination was accepted") + partial_hotpath = resolver.production_release_features( {"production": [], "hotpath": []}, "x86_64-unknown-linux-gnu" ) From 86bcdede34f4328e3734b31668ebdb11976c11c4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:33:27 +0000 Subject: [PATCH 026/182] simplify(pass-1/5): share pre-admission problem checks One ApplicationProblem::ensure_admitted replaces the copied cancellation and deadline match at each work and retained caller. Co-authored-by: Zack Jackson --- .../execution_topology_metrics/rollup_read.rs | 16 ++++----------- .../src/result/problem.rs | 12 +++++++++++ .../src/retained_surfaces/service.rs | 13 ++---------- .../tracedecay-contracts/src/work_attempt.rs | 14 +++---------- .../src/work_attempt/product_admission.rs | 20 ++++++++----------- .../src/work_duplicate_adjudication.rs | 19 ++++-------------- .../src/work_leak_adjudication.rs | 14 +++---------- .../src/work_placement.rs | 19 ++++-------------- crates/tracedecay-contracts/src/work_retry.rs | 14 +++---------- .../src/work_run_control.rs | 17 +++------------- 10 files changed, 46 insertions(+), 112 deletions(-) diff --git a/crates/tracedecay-contracts/src/execution_topology_metrics/rollup_read.rs b/crates/tracedecay-contracts/src/execution_topology_metrics/rollup_read.rs index 84c1792c90..e4f0e1335d 100644 --- a/crates/tracedecay-contracts/src/execution_topology_metrics/rollup_read.rs +++ b/crates/tracedecay-contracts/src/execution_topology_metrics/rollup_read.rs @@ -10,7 +10,7 @@ use crate::observability::{ ObservabilityFuture, ObservabilityHorizonV1, ObservabilityQueryPort, ObservabilityQueryV1, }; use crate::work::work_authority; -use crate::{ApplicationProblem, RequestAdmission, RequestContext, RetryDirective}; +use crate::{ApplicationProblem, RequestContext, RetryDirective}; use super::projection::TELEMETRY_DROP_EVENT_KIND_V1; use super::rollup::{ @@ -77,7 +77,7 @@ where { validate_request(request)?; let observed_at = now_micros(); - admit(context, observed_at)?; + ApplicationProblem::ensure_admitted(context, observed_at)?; authorize(context)?; let authorized_scope_ref = work_authority(context)?.project_id().as_str().to_owned(); let observed_at_micros = observed_at.0; @@ -106,7 +106,7 @@ where ExecutionMetricUnavailableV1::EventBudgetExceeded, )); } - admit(context, now_micros())?; + ApplicationProblem::ensure_admitted(context, now_micros())?; let page = match observations .query(boundary_query( &authorized_scope_ref, @@ -182,7 +182,7 @@ where } let fragments = if let Some(horizon) = full_days { - admit(context, now_micros())?; + ApplicationProblem::ensure_admitted(context, now_micros())?; let page = match rollups .query_rollup_fragments(ExecutionTopologyRollupFragmentQueryV1 { authorized_scope_ref: authorized_scope_ref.clone(), @@ -241,14 +241,6 @@ fn validate_request(request: &ExecutionTopologyMetricsRequestV1) -> Result<(), A Ok(()) } -fn admit(context: &RequestContext, observed_at: UtcMicros) -> Result<(), ApplicationProblem> { - match context.admission_at(observed_at) { - RequestAdmission::Admitted => Ok(()), - RequestAdmission::Cancelled => Err(ApplicationProblem::cancelled_before_admission()), - RequestAdmission::TimedOut => Err(ApplicationProblem::timed_out_before_admission()), - } -} - fn authorize(context: &RequestContext) -> Result<(), ApplicationProblem> { let capability = CapabilityId::new(EXECUTION_TOPOLOGY_CAPABILITY_ID_V1).map_err(|_| { invalid_problem( diff --git a/crates/tracedecay-contracts/src/result/problem.rs b/crates/tracedecay-contracts/src/result/problem.rs index a44e21648e..6998b85659 100644 --- a/crates/tracedecay-contracts/src/result/problem.rs +++ b/crates/tracedecay-contracts/src/result/problem.rs @@ -1,8 +1,10 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use tracedecay_domain::UtcMicros; use tracedecay_domain::errors::TraceDecayError; use super::{CancellationStage, EffectReceipt, EffectTermination}; +use crate::context::{RequestAdmission, RequestContext}; use crate::error::ApplicationContractError; /// Diagnostic code for an admitted route whose owner is still registering @@ -704,6 +706,16 @@ impl ApplicationProblem { } } + /// Admitted requests continue. Cancellation and deadline expiry become + /// the matching pre-admission problems. + pub fn ensure_admitted(context: &RequestContext, observed_at: UtcMicros) -> Result<(), Self> { + match context.admission_at(observed_at) { + RequestAdmission::Admitted => Ok(()), + RequestAdmission::Cancelled => Err(Self::cancelled_before_admission()), + RequestAdmission::TimedOut => Err(Self::timed_out_before_admission()), + } + } + pub fn cancelled(stage: CancellationStage) -> Result { let problem = Self::Cancelled { stage, diff --git a/crates/tracedecay-contracts/src/retained_surfaces/service.rs b/crates/tracedecay-contracts/src/retained_surfaces/service.rs index f59b51f139..ecc91bb4a4 100644 --- a/crates/tracedecay-contracts/src/retained_surfaces/service.rs +++ b/crates/tracedecay-contracts/src/retained_surfaces/service.rs @@ -23,8 +23,7 @@ use crate::retrieval::{ }; use crate::{ ApplicationOperation, ApplicationOutcome, ApplicationProblem, CancellationSignal, - CancellationStage, EffectReceipt, LegalAction, RequestAdmission, RequestContext, - RetryDirective, SafeDiagnostic, + CancellationStage, EffectReceipt, LegalAction, RequestContext, RetryDirective, SafeDiagnostic, }; pub type RetainedSurfaceExecutionFutureV1<'a> = Pin< @@ -219,7 +218,7 @@ impl<'a> RetainedSurfaceServiceV1<'a> { observed_at: UtcMicros, request: &RetainedSurfaceRequestV1, ) -> Result, ApplicationProblem> { - admit(context, observed_at)?; + ApplicationProblem::ensure_admitted(context, observed_at)?; if cancellation.context().token_id != context.cancellation().token_id { return Err(ApplicationProblem::not_found_or_not_authorized( RetryDirective::Never, @@ -515,14 +514,6 @@ pub const fn retained_surface_operation_is_effect(operation: RetainedSurfaceOper ) } -fn admit(context: &RequestContext, observed_at: UtcMicros) -> Result<(), ApplicationProblem> { - match context.admission_at(observed_at) { - RequestAdmission::Admitted => Ok(()), - RequestAdmission::Cancelled => Err(ApplicationProblem::cancelled_before_admission()), - RequestAdmission::TimedOut => Err(ApplicationProblem::timed_out_before_admission()), - } -} - /// Canonical semantic problem projection for a retained runtime failure. pub fn retained_surface_execution_problem( error: RetainedSurfaceExecutionErrorV1, diff --git a/crates/tracedecay-contracts/src/work_attempt.rs b/crates/tracedecay-contracts/src/work_attempt.rs index a86a388b73..e4945d9930 100644 --- a/crates/tracedecay-contracts/src/work_attempt.rs +++ b/crates/tracedecay-contracts/src/work_attempt.rs @@ -22,7 +22,7 @@ use tracedecay_domain::{ }; use crate::work::work_authority; -use crate::{ApplicationProblem, RequestAdmission, RequestContext}; +use crate::{ApplicationProblem, RequestContext}; mod capacity; mod problem; @@ -559,7 +559,7 @@ where context: &RequestContext, command: CancelWorkAttemptCommand, ) -> Result { - admit(context, command.occurred_at)?; + ApplicationProblem::ensure_admitted(context, command.occurred_at)?; let authority = work_authority(context)?; let identity = WorkAttemptIdentityV1::new( command.task_id.clone(), @@ -623,7 +623,7 @@ where context: &RequestContext, command: &ResumeWorkAttemptsCommand, ) -> Result { - admit(context, command.occurred_at)?; + ApplicationProblem::ensure_admitted(context, command.occurred_at)?; let authority = work_authority(context)?; let open = self .attempts @@ -1128,11 +1128,3 @@ fn cancellation_request(state: &WorkCancellationStateV1) -> Option<&WorkCancella } } } - -fn admit(context: &RequestContext, observed_at: UtcMicros) -> Result<(), ApplicationProblem> { - match context.admission_at(observed_at) { - RequestAdmission::Admitted => Ok(()), - RequestAdmission::Cancelled => Err(ApplicationProblem::cancelled_before_admission()), - RequestAdmission::TimedOut => Err(ApplicationProblem::timed_out_before_admission()), - } -} diff --git a/crates/tracedecay-contracts/src/work_attempt/product_admission.rs b/crates/tracedecay-contracts/src/work_attempt/product_admission.rs index 0608704b46..b616b3bfb7 100644 --- a/crates/tracedecay-contracts/src/work_attempt/product_admission.rs +++ b/crates/tracedecay-contracts/src/work_attempt/product_admission.rs @@ -11,13 +11,13 @@ use tracedecay_domain::{ }; use crate::{ - ApplicationProblem, RequestAdmission, RequestContext, WorkGraphReadPortV1, - WorkGraphReadRequestV1, WorkGraphReadV1, WorkProductApplicationErrorV1, - WorkProductAttemptAdmissionErrorV1, WorkProductAttemptAdmissionOutcomeV1, - WorkProductAttemptAdmissionPortV1, WorkProductAttemptAdmissionV1, WorkProductBindingV1, - WorkProductEventDraftV1, WorkProductOwnerAuthorizationErrorV1, - WorkProductOwnerAuthorizationPortV1, WorkProductPortContextV1, WorkProductRevisionPinsV1, - WorkProductSelectionScopeV1, WorkRelationScopeV1, + ApplicationProblem, RequestContext, WorkGraphReadPortV1, WorkGraphReadRequestV1, + WorkGraphReadV1, WorkProductApplicationErrorV1, WorkProductAttemptAdmissionErrorV1, + WorkProductAttemptAdmissionOutcomeV1, WorkProductAttemptAdmissionPortV1, + WorkProductAttemptAdmissionV1, WorkProductBindingV1, WorkProductEventDraftV1, + WorkProductOwnerAuthorizationErrorV1, WorkProductOwnerAuthorizationPortV1, + WorkProductPortContextV1, WorkProductRevisionPinsV1, WorkProductSelectionScopeV1, + WorkRelationScopeV1, }; use super::{ @@ -50,11 +50,7 @@ pub(crate) fn admit_product_attempt_request( if !context.allows(binding.capability_id(), binding.use_case_id()) { return Err(not_found_problem()); } - match context.admission_at(observed_at) { - RequestAdmission::Admitted => Ok(()), - RequestAdmission::Cancelled => Err(ApplicationProblem::cancelled_before_admission()), - RequestAdmission::TimedOut => Err(ApplicationProblem::timed_out_before_admission()), - } + ApplicationProblem::ensure_admitted(context, observed_at) } pub(crate) fn replayed_attempt_matches_command( diff --git a/crates/tracedecay-contracts/src/work_duplicate_adjudication.rs b/crates/tracedecay-contracts/src/work_duplicate_adjudication.rs index d96b988eb4..78f8f9fb0c 100644 --- a/crates/tracedecay-contracts/src/work_duplicate_adjudication.rs +++ b/crates/tracedecay-contracts/src/work_duplicate_adjudication.rs @@ -14,10 +14,7 @@ use tracedecay_domain::{ }; use crate::work::work_authority; -use crate::{ - ApplicationProblem, LegalAction, RequestAdmission, RequestContext, RetryDirective, - SafeDiagnostic, -}; +use crate::{ApplicationProblem, LegalAction, RequestContext, RetryDirective, SafeDiagnostic}; pub fn work_duplicate_adjudication_input_digest( command: &WorkDuplicateAdjudicationCommandV1, @@ -200,7 +197,7 @@ where context: &RequestContext, command: WorkDuplicateAdjudicationCommandV1, ) -> Result { - admit(context, command.occurred_at)?; + ApplicationProblem::ensure_admitted(context, command.occurred_at)?; let authority = work_authority(context)?; let command = command.canonicalized(); command.validate().map_err(|_| invalid_problem())?; @@ -240,7 +237,7 @@ where command_id: WorkCommandId, occurred_at: UtcMicros, ) -> Result { - admit(context, occurred_at)?; + ApplicationProblem::ensure_admitted(context, occurred_at)?; let authority = work_authority(context)?; if request.first_attempt == request.second_attempt { return Err(invalid_problem()); @@ -274,7 +271,7 @@ where context: &RequestContext, request: WorkDuplicateAttemptClassificationRequestV1, ) -> Result { - admit(context, request.observed_at)?; + ApplicationProblem::ensure_admitted(context, request.observed_at)?; let authority = work_authority(context)?; let mut attempts = request.attempts; attempts.sort(); @@ -402,14 +399,6 @@ fn classify_complete_attempt_relations( } } -fn admit(context: &RequestContext, observed_at: UtcMicros) -> Result<(), ApplicationProblem> { - match context.admission_at(observed_at) { - RequestAdmission::Admitted => Ok(()), - RequestAdmission::Cancelled => Err(ApplicationProblem::cancelled_before_admission()), - RequestAdmission::TimedOut => Err(ApplicationProblem::timed_out_before_admission()), - } -} - fn invalid_problem() -> ApplicationProblem { ApplicationProblem::InvalidRequest { diagnostic: SafeDiagnostic { diff --git a/crates/tracedecay-contracts/src/work_leak_adjudication.rs b/crates/tracedecay-contracts/src/work_leak_adjudication.rs index 9908312212..c212e6b4d6 100644 --- a/crates/tracedecay-contracts/src/work_leak_adjudication.rs +++ b/crates/tracedecay-contracts/src/work_leak_adjudication.rs @@ -16,8 +16,8 @@ use tracedecay_domain::{ use crate::work::work_authority; use crate::{ - ApplicationProblem, CancellationStage, LegalAction, RequestAdmission, RequestContext, - RetryDirective, SafeDiagnostic, + ApplicationProblem, CancellationStage, LegalAction, RequestContext, RetryDirective, + SafeDiagnostic, }; pub const MAX_WORK_LEAK_EVIDENCE_REFS_V1: usize = 8; @@ -269,7 +269,7 @@ where scan_started_at: UtcMicros, scan_deadline: UtcMicros, ) -> Result { - admit(context, scan_started_at)?; + ApplicationProblem::ensure_admitted(context, scan_started_at)?; if !command.validate() || scan_deadline.0 < scan_started_at.0 || u64::try_from(scan_deadline.0.saturating_sub(scan_started_at.0)) @@ -334,14 +334,6 @@ fn canonical_label(value: &str, maximum: usize) -> bool { .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b':' | b'-' | b'_')) } -fn admit(context: &RequestContext, observed_at: UtcMicros) -> Result<(), ApplicationProblem> { - match context.admission_at(observed_at) { - RequestAdmission::Admitted => Ok(()), - RequestAdmission::Cancelled => Err(ApplicationProblem::cancelled_before_admission()), - RequestAdmission::TimedOut => Err(ApplicationProblem::timed_out_before_admission()), - } -} - fn invalid_problem() -> ApplicationProblem { ApplicationProblem::InvalidRequest { diagnostic: SafeDiagnostic { diff --git a/crates/tracedecay-contracts/src/work_placement.rs b/crates/tracedecay-contracts/src/work_placement.rs index c067bfc19c..ee0c3a433e 100644 --- a/crates/tracedecay-contracts/src/work_placement.rs +++ b/crates/tracedecay-contracts/src/work_placement.rs @@ -34,10 +34,7 @@ use tracedecay_domain::{ }; use crate::work::work_authority; -use crate::{ - ApplicationProblem, LegalAction, RequestAdmission, RequestContext, RetryDirective, - SafeDiagnostic, -}; +use crate::{ApplicationProblem, LegalAction, RequestContext, RetryDirective, SafeDiagnostic}; #[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] pub enum WorkPlacementStorageError { @@ -176,7 +173,7 @@ where &WorkPlacementTargetV1, ) -> Result, ) -> Result { - admit(context, request.occurred_at)?; + ApplicationProblem::ensure_admitted(context, request.occurred_at)?; let authority = work_authority(context)?; let identity = WorkPlacementIdentityV1::new(request.task_id, request.run_id); self.evaluate(&authority, identity, request.target, observe) @@ -196,7 +193,7 @@ where &WorkPlacementTargetV1, ) -> Result, ) -> Result { - admit(context, command.occurred_at)?; + ApplicationProblem::ensure_admitted(context, command.occurred_at)?; let authority = work_authority(context)?; let identity = WorkPlacementIdentityV1::new(command.task_id, command.run_id); let existing = self @@ -270,7 +267,7 @@ where &WorkPlacementTargetV1, ) -> Result, ) -> Result { - admit(context, command.occurred_at)?; + ApplicationProblem::ensure_admitted(context, command.occurred_at)?; let authority = work_authority(context)?; let identity = WorkPlacementIdentityV1::new(command.task_id, command.run_id); let current = self @@ -337,14 +334,6 @@ where } } -fn admit(context: &RequestContext, observed_at: UtcMicros) -> Result<(), ApplicationProblem> { - match context.admission_at(observed_at) { - RequestAdmission::Admitted => Ok(()), - RequestAdmission::Cancelled => Err(ApplicationProblem::cancelled_before_admission()), - RequestAdmission::TimedOut => Err(ApplicationProblem::timed_out_before_admission()), - } -} - fn storage_problem(error: WorkPlacementStorageError) -> ApplicationProblem { match error { WorkPlacementStorageError::NotFoundOrNotAuthorized => not_found_problem(), diff --git a/crates/tracedecay-contracts/src/work_retry.rs b/crates/tracedecay-contracts/src/work_retry.rs index f6ddfbf23e..143dfa9326 100644 --- a/crates/tracedecay-contracts/src/work_retry.rs +++ b/crates/tracedecay-contracts/src/work_retry.rs @@ -26,8 +26,8 @@ use crate::work_attempt_effect::{ WorkAttemptEffectResolutionV1, WorkAttemptEffectStorageErrorV1, WorkAttemptEffectStoragePortV1, }; use crate::{ - ApplicationContractError, ApplicationProblem, LegalAction, RequestAdmission, RequestContext, - RetryDirective, SafeDiagnostic, WorkGraphReadPortV1, WorkProductAttemptAdmissionPortV1, + ApplicationContractError, ApplicationProblem, LegalAction, RequestContext, RetryDirective, + SafeDiagnostic, WorkGraphReadPortV1, WorkProductAttemptAdmissionPortV1, WorkProductAttemptAdmissionV1, WorkProductBindingV1, WorkProductOwnerAuthorizationPortV1, WorkProductRetryAdmissionV1, WorkProductRevisionPinsV1, WorkflowFanOutAttemptBindingV1, WorkflowRunAppendRequest, @@ -376,7 +376,7 @@ where restarted_at: UtcMicros, workflow_rebind: Option, ) -> Result { - admit(context, restarted_at)?; + ApplicationProblem::ensure_admitted(context, restarted_at)?; if !command.validate() { return Err(invalid_problem()); } @@ -789,14 +789,6 @@ fn validate_failure( Ok(()) } -fn admit(context: &RequestContext, observed_at: UtcMicros) -> Result<(), ApplicationProblem> { - match context.admission_at(observed_at) { - RequestAdmission::Admitted => Ok(()), - RequestAdmission::Cancelled => Err(ApplicationProblem::cancelled_before_admission()), - RequestAdmission::TimedOut => Err(ApplicationProblem::timed_out_before_admission()), - } -} - fn invalid_problem() -> ApplicationProblem { ApplicationProblem::InvalidRequest { diagnostic: SafeDiagnostic { diff --git a/crates/tracedecay-contracts/src/work_run_control.rs b/crates/tracedecay-contracts/src/work_run_control.rs index 97b51937e3..41bc81a612 100644 --- a/crates/tracedecay-contracts/src/work_run_control.rs +++ b/crates/tracedecay-contracts/src/work_run_control.rs @@ -39,10 +39,7 @@ use tracedecay_domain::{ }; use crate::work::work_authority; -use crate::{ - ApplicationProblem, LegalAction, RequestAdmission, RequestContext, RetryDirective, - SafeDiagnostic, -}; +use crate::{ApplicationProblem, LegalAction, RequestContext, RetryDirective, SafeDiagnostic}; #[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] pub enum WorkRunControlStorageError { @@ -304,7 +301,7 @@ where command: PauseWorkRunCommand, ) -> Result { hotpath::measure_block!("application.work.run_control.pause", { - admit(context, command.occurred_at)?; + ApplicationProblem::ensure_admitted(context, command.occurred_at)?; let authority = work_authority(context)?; let frontier = self .storage @@ -399,7 +396,7 @@ where command: ResumeWorkRunCommand, ) -> Result { hotpath::measure_block!("application.work.run_control.resume", { - admit(context, command.occurred_at)?; + ApplicationProblem::ensure_admitted(context, command.occurred_at)?; let authority = work_authority(context)?; let frontier = self .storage @@ -583,14 +580,6 @@ fn check_expected( } } -fn admit(context: &RequestContext, observed_at: UtcMicros) -> Result<(), ApplicationProblem> { - match context.admission_at(observed_at) { - RequestAdmission::Admitted => Ok(()), - RequestAdmission::Cancelled => Err(ApplicationProblem::cancelled_before_admission()), - RequestAdmission::TimedOut => Err(ApplicationProblem::timed_out_before_admission()), - } -} - fn storage_problem(error: WorkRunControlStorageError) -> ApplicationProblem { match error { WorkRunControlStorageError::NotFoundOrNotAuthorized => not_found_problem(), From f5eddebbe7ce4b5573bb53def9280a3e1ec8998c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:33:34 +0000 Subject: [PATCH 027/182] simplify(pass-3/5): collapse runtime binding constructors Project-scoped bindings and family mounts built the same JSON shape four times. One constructor keeps the shard identity and epochs. Co-authored-by: Zack Jackson --- .../repository_parity.rs | 56 +++++------ .../runtime_test_support.rs | 92 +++++++++++-------- .../writer_serialization.rs | 11 +-- 3 files changed, 80 insertions(+), 79 deletions(-) diff --git a/tests/storage_runtime_rusqlite_suite/repository_parity.rs b/tests/storage_runtime_rusqlite_suite/repository_parity.rs index fc85493905..96f0ed21c0 100644 --- a/tests/storage_runtime_rusqlite_suite/repository_parity.rs +++ b/tests/storage_runtime_rusqlite_suite/repository_parity.rs @@ -42,6 +42,15 @@ impl RuntimeRequestProbeV1 for Probe { } } +fn family_binding(shard_id: StoreShardIdV1) -> StoreRuntimeBindingV1 { + serde_json::from_value(serde_json::json!({ + "shard_id": shard_id, + "incarnation": 1, + "authority_epoch": 1 + })) + .unwrap() +} + fn health_request(binding: StoreRuntimeBindingV1) -> (RuntimeReadRequestV1, Probe) { let cancellation = RuntimeCancellationIdentityV1 { cancellation_id: RuntimeCancellationIdV1::new("cancel.repository-family-health").unwrap(), @@ -107,48 +116,33 @@ fn profile_project_and_session_production_mounts_serve_health_data_ports() { let families = [ ( "profile", - serde_json::from_value(serde_json::json!({ - "shard_id": StoreShardIdV1::profile( - id::("brain.repository-profile"), - id::("profile.repository"), - ), - "incarnation": 1, - "authority_epoch": 1 - })) - .unwrap(), + StoreShardIdV1::profile( + id::("brain.repository-profile"), + id::("profile.repository"), + ), ), ( "project", - serde_json::from_value(serde_json::json!({ - "shard_id": StoreShardIdV1::project( - id::("brain.repository-project"), - id::("profile.repository"), - id::("project.repository"), - ), - "incarnation": 1, - "authority_epoch": 1 - })) - .unwrap(), + StoreShardIdV1::project( + id::("brain.repository-project"), + id::("profile.repository"), + id::("project.repository"), + ), ), ( "sessions", - serde_json::from_value(serde_json::json!({ - "shard_id": StoreShardIdV1::project_sessions( - id::("brain.repository-sessions"), - id::("profile.repository"), - id::("project.repository"), - ), - "incarnation": 1, - "authority_epoch": 1 - })) - .unwrap(), + StoreShardIdV1::project_sessions( + id::("brain.repository-sessions"), + id::("profile.repository"), + id::("project.repository"), + ), ), ]; - for (family, binding) in families { + for (family, shard_id) in families { let path = directory.path().join(format!("{family}.db")); Connection::open(&path).unwrap(); let path = path.canonicalize().unwrap(); - assert_family_mount(binding, &path); + assert_family_mount(family_binding(shard_id), &path); } } diff --git a/tests/storage_runtime_rusqlite_suite/runtime_test_support.rs b/tests/storage_runtime_rusqlite_suite/runtime_test_support.rs index 1e100dc992..ff8f2a1420 100644 --- a/tests/storage_runtime_rusqlite_suite/runtime_test_support.rs +++ b/tests/storage_runtime_rusqlite_suite/runtime_test_support.rs @@ -77,18 +77,36 @@ pub(crate) fn acceptance_reader_budget(fixture: &ReaderRuntimeFixture) -> Reader budget } +fn project_scope_binding( + brain_id: &str, + profile_id: &str, + scope_kind: &str, + project_id: &str, + incarnation: u64, + authority_epoch: u64, +) -> StoreRuntimeBindingV1 { + serde_json::from_value(json!({ + "shard_id": { + "brain_id": brain_id, + "profile_id": profile_id, + "scope": { "kind": scope_kind, "project_id": project_id } + }, + "incarnation": incarnation, + "authority_epoch": authority_epoch + })) + .expect("construct runtime binding") +} + pub(crate) fn reader_runtime_fixture() -> ReaderRuntimeFixture { ReaderRuntimeFixture { - binding: serde_json::from_value(json!({ - "shard_id": { - "brain_id": "brain.runtime-reader", - "profile_id": "profile.runtime-reader", - "scope": { "kind": "project", "project_id": "project.runtime-reader" } - }, - "incarnation": 1, - "authority_epoch": 7 - })) - .expect("construct reader runtime binding"), + binding: project_scope_binding( + "brain.runtime-reader", + "profile.runtime-reader", + "project", + "project.runtime-reader", + 1, + 7, + ), reader_budget: ReaderBudgetFixture { min_per_hot_shard: 2, max_per_hot_shard: 2, @@ -100,40 +118,34 @@ pub(crate) fn reader_runtime_fixture() -> ReaderRuntimeFixture { } pub(crate) fn maintenance_binding() -> StoreRuntimeBindingV1 { - serde_json::from_value(json!({ - "shard_id": { - "brain_id": "brain.runtime-maintenance", - "profile_id": "profile.runtime-maintenance", - "scope": { "kind": "project", "project_id": "project.runtime-maintenance" } - }, - "incarnation": 3, - "authority_epoch": 11 - })) - .expect("construct maintenance runtime binding") + project_scope_binding( + "brain.runtime-maintenance", + "profile.runtime-maintenance", + "project", + "project.runtime-maintenance", + 3, + 11, + ) } pub(crate) fn writer_runtime_fixture() -> WriterRuntimeFixture { WriterRuntimeFixture { - origin_binding: serde_json::from_value(json!({ - "shard_id": { - "brain_id": "brain.runtime-writer", - "profile_id": "profile.runtime-writer", - "scope": { "kind": "project", "project_id": "project.runtime-writer-origin" } - }, - "incarnation": 5, - "authority_epoch": 13 - })) - .expect("construct writer origin binding"), - target_binding: serde_json::from_value(json!({ - "shard_id": { - "brain_id": "brain.runtime-writer", - "profile_id": "profile.runtime-writer", - "scope": { "kind": "project_sessions", "project_id": "project.runtime-writer-origin" } - }, - "incarnation": 6, - "authority_epoch": 17 - })) - .expect("construct writer target binding"), + origin_binding: project_scope_binding( + "brain.runtime-writer", + "profile.runtime-writer", + "project", + "project.runtime-writer-origin", + 5, + 13, + ), + target_binding: project_scope_binding( + "brain.runtime-writer", + "profile.runtime-writer", + "project_sessions", + "project.runtime-writer-origin", + 6, + 17, + ), effect_id: "effect.runtime.writer", ordering_key: "project.runtime-writer.serialized", commit_sequences: [1, 2], diff --git a/tests/storage_runtime_rusqlite_suite/writer_serialization.rs b/tests/storage_runtime_rusqlite_suite/writer_serialization.rs index 230432d2f8..2e7a40d7e8 100644 --- a/tests/storage_runtime_rusqlite_suite/writer_serialization.rs +++ b/tests/storage_runtime_rusqlite_suite/writer_serialization.rs @@ -1,10 +1,10 @@ use std::sync::Arc; use tracedecay_rusqlite_runtime::read_consistency::{CommitWatermarkSource, WatermarkSourceState}; -use tracedecay_store::{CommitSequenceV1, RuntimeSubmitOutcomeV1}; +use tracedecay_store::RuntimeSubmitOutcomeV1; use super::runtime_test_support::{ - Probe, TestDatabase, outbox_request, run, writer, writer_runtime_fixture, + Probe, TestDatabase, outbox_request, run, shard_watermark, writer, writer_runtime_fixture, }; #[test] @@ -53,12 +53,7 @@ fn writer_serializes_commit_checkpoints_and_publishes_only_committed_watermarks( assert_eq!(sequences, fixture.commit_sequences.to_vec()); assert_eq!( watermarks.current(&fixture.origin_binding.shard_id), - WatermarkSourceState::Available(tracedecay_store::ShardWatermarkV1 { - shard_id: fixture.origin_binding.shard_id.clone(), - incarnation: fixture.origin_binding.incarnation, - authority_epoch: fixture.origin_binding.authority_epoch, - commit_sequence: CommitSequenceV1(2), - }) + WatermarkSourceState::Available(shard_watermark(&fixture.origin_binding, 2)) ); Arc::try_unwrap(writer) From d03c784e3c32dd755f0311c667349214a449b578 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:34:05 +0000 Subject: [PATCH 028/182] simplify(pass-4/5): collapse installer fixture harness Beta and stable archives were staged twice, and the failure path copied the installer's environment. One stager and one runner remain. Co-authored-by: Zack Jackson --- tests/install_script_test.sh | 46 ++++++++++++++---------------------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/tests/install_script_test.sh b/tests/install_script_test.sh index 8265122ab0..4bb6aaf92c 100755 --- a/tests/install_script_test.sh +++ b/tests/install_script_test.sh @@ -20,27 +20,24 @@ STABLE_ASSET="tracedecay-${STABLE_TAG}-x86_64-linux.tar.gz" # Newer prerelease that Release Please published before assets uploaded. INCOMPLETE_TAG=v9.8.8-beta.1 -cat >"$tmpdir/archive/tracedecay" <<'SH' +stage_release_archive() { + local version=$1 + local asset=$2 + local sums=$3 + cat >"$tmpdir/archive/tracedecay" <SHA256SUMS -) - -cat >"$tmpdir/archive/tracedecay" <<'SH' -#!/usr/bin/env bash -printf 'tracedecay 9.8.7\n' -SH -chmod +x "$tmpdir/archive/tracedecay" -tar -czf "$tmpdir/${STABLE_ASSET}" -C "$tmpdir/archive" tracedecay -( - cd "$tmpdir" - sha256sum "$STABLE_ASSET" >STABLE_SHA256SUMS -) + chmod +x "$tmpdir/archive/tracedecay" + tar -czf "$tmpdir/${asset}" -C "$tmpdir/archive" tracedecay + ( + cd "$tmpdir" + sha256sum "$asset" >"$sums" + ) +} + +stage_release_archive "9.8.7-beta.1" "$BETA_ASSET" SHA256SUMS +stage_release_archive "9.8.7" "$STABLE_ASSET" STABLE_SHA256SUMS # Incomplete first, complete beta second: default latest must skip the empty # prerelease and install the beta that already lists archive + SHA256SUMS. @@ -153,7 +150,7 @@ run_installer() { TRACEDECAY_INSTALL_DIR="$tmpdir/install" \ TEST_RELEASES_JSON="$tmpdir/releases.json" \ TEST_ARCHIVE="$tmpdir/${BETA_ASSET}" \ - TEST_CHECKSUMS="$tmpdir/SHA256SUMS" \ + TEST_CHECKSUMS="${INSTALLER_CHECKSUMS:-$tmpdir/SHA256SUMS}" \ TEST_STABLE_ARCHIVE="$tmpdir/${STABLE_ASSET}" \ TEST_STABLE_CHECKSUMS="$tmpdir/STABLE_SHA256SUMS" \ "$@" @@ -174,14 +171,7 @@ expect_installer_failure() { local checksums=$1 local expected_message=$2 local output="$tmpdir/installer-failure.log" - if PATH="$tmpdir/bin:$PATH" \ - TRACEDECAY_INSTALL_DIR="$tmpdir/install" \ - TEST_RELEASES_JSON="$tmpdir/releases.json" \ - TEST_ARCHIVE="$tmpdir/${BETA_ASSET}" \ - TEST_CHECKSUMS="$checksums" \ - TEST_STABLE_ARCHIVE="$tmpdir/${STABLE_ASSET}" \ - TEST_STABLE_CHECKSUMS="$tmpdir/STABLE_SHA256SUMS" \ - "$INSTALLER" >"$output" 2>&1 + if INSTALLER_CHECKSUMS="$checksums" run_installer "$INSTALLER" >"$output" 2>&1 then echo "installer unexpectedly accepted invalid release inputs" >&2 exit 1 From 4c187fc179cdc126fac198471f3d1ae46781e6cf Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:34:26 +0000 Subject: [PATCH 029/182] simplify(pass-2/5): share conflict problem constructor Callers that built the same AfterRevalidate/Refresh conflict now use ApplicationProblem::conflict instead of a copied local helper. Co-authored-by: Zack Jackson --- .../src/result/problem.rs | 13 +++++ .../tracedecay-contracts/src/work_attempt.rs | 16 +++--- .../src/work_attempt/problem.rs | 19 ++----- .../src/work_attempt/product_admission.rs | 17 +++---- .../product_synthesis_admission.rs | 4 +- .../src/work_duplicate_adjudication.rs | 26 +++------- .../src/work_leak_adjudication.rs | 19 ++----- .../src/work_placement.rs | 31 +++--------- crates/tracedecay-contracts/src/work_retry.rs | 49 +++++++------------ .../src/work_run_control.rs | 23 +++------ 10 files changed, 80 insertions(+), 137 deletions(-) diff --git a/crates/tracedecay-contracts/src/result/problem.rs b/crates/tracedecay-contracts/src/result/problem.rs index 6998b85659..0df5d4dae3 100644 --- a/crates/tracedecay-contracts/src/result/problem.rs +++ b/crates/tracedecay-contracts/src/result/problem.rs @@ -716,6 +716,19 @@ impl ApplicationProblem { } } + /// Conflict that must be revalidated. Retry is `AfterRevalidate` and the + /// only legal action is `Refresh`. + pub fn conflict(code: impl Into, message: impl Into) -> Self { + Self::Conflict { + diagnostic: SafeDiagnostic { + code: code.into(), + message: message.into(), + }, + retry: RetryDirective::AfterRevalidate, + legal_actions: vec![LegalAction::Refresh], + } + } + pub fn cancelled(stage: CancellationStage) -> Result { let problem = Self::Cancelled { stage, diff --git a/crates/tracedecay-contracts/src/work_attempt.rs b/crates/tracedecay-contracts/src/work_attempt.rs index e4945d9930..e93fe37802 100644 --- a/crates/tracedecay-contracts/src/work_attempt.rs +++ b/crates/tracedecay-contracts/src/work_attempt.rs @@ -34,8 +34,8 @@ pub use capacity::{ WorkAttemptCapacityVerdictV1, }; use problem::{ - conflict_problem, contract_problem, denied_problem, invalid_problem, - list_page_contract_problem, not_found_problem, stale_cursor_problem, storage_problem, + contract_problem, denied_problem, invalid_problem, list_page_contract_problem, + not_found_problem, stale_cursor_problem, storage_problem, }; pub use product_admission::WorkProductAttemptServiceV1; pub(crate) use product_admission::{ @@ -575,7 +575,7 @@ where return if request.request_id() == &command.request_id { Ok(attempt) } else { - Err(conflict_problem( + Err(ApplicationProblem::conflict( "application.work-attempt.cancellation-conflict", "A different cancellation request is already recorded.", )) @@ -587,7 +587,7 @@ where | WorkAttemptStateV1::Running | WorkAttemptStateV1::RecoveryRequired ) { - return Err(conflict_problem( + return Err(ApplicationProblem::conflict( "application.work-attempt.not-cancellable", "Only an open Work attempt can accept a cancellation request.", )); @@ -750,7 +750,7 @@ where .load(&authority, identity) .map_err(storage_problem)?; let WorkCancellationStateV1::Requested(request) = attempt.cancellation().clone() else { - return Err(conflict_problem( + return Err(ApplicationProblem::conflict( "application.work-attempt.cancellation-not-requested", "There is no pending cancellation request to acknowledge.", )); @@ -788,7 +788,7 @@ where .map_err(storage_problem)?; let WorkCancellationStateV1::Acknowledged(acknowledgement) = attempt.cancellation().clone() else { - return Err(conflict_problem( + return Err(ApplicationProblem::conflict( "application.work-attempt.cancellation-not-acknowledged", "There is no acknowledged cancellation to escalate.", )); @@ -879,7 +879,7 @@ where .load(&authority, identity) .map_err(storage_problem)?; if attempt.state() != WorkAttemptStateV1::RecoveryRequired { - return Err(conflict_problem( + return Err(ApplicationProblem::conflict( "application.work-attempt.not-recovery-required", "Only an attempt awaiting recovery can be failed this way.", )); @@ -1082,7 +1082,7 @@ pub fn require_registered_work_topology( if snapshot.topology() == registered_topology { return Ok(()); } - Err(conflict_problem( + Err(ApplicationProblem::conflict( "application.work-attempt.topology-conflict", "The Work attempt topology differs from the registered runtime authority.", )) diff --git a/crates/tracedecay-contracts/src/work_attempt/problem.rs b/crates/tracedecay-contracts/src/work_attempt/problem.rs index 82cedd1f05..7785d3dc33 100644 --- a/crates/tracedecay-contracts/src/work_attempt/problem.rs +++ b/crates/tracedecay-contracts/src/work_attempt/problem.rs @@ -7,19 +7,19 @@ use super::WorkAttemptStorageError; pub(super) fn storage_problem(error: WorkAttemptStorageError) -> ApplicationProblem { match error { WorkAttemptStorageError::NotFoundOrNotAuthorized => not_found_problem(), - WorkAttemptStorageError::AttemptConflict => conflict_problem( + WorkAttemptStorageError::AttemptConflict => ApplicationProblem::conflict( "application.work-attempt.identity-conflict", "The Work attempt identity was already used with different content.", ), - WorkAttemptStorageError::RunAdmissionConflict => conflict_problem( + WorkAttemptStorageError::RunAdmissionConflict => ApplicationProblem::conflict( "application.work-attempt.run-admission-conflict", "The Work attempt differs from this run's first admitted deadline or topology.", ), - WorkAttemptStorageError::ReservationFenced => conflict_problem( + WorkAttemptStorageError::ReservationFenced => ApplicationProblem::conflict( "application.work-attempt.reservation-fenced", "The Work run control authority fenced new attempt reservations.", ), - WorkAttemptStorageError::FenceConflict => conflict_problem( + WorkAttemptStorageError::FenceConflict => ApplicationProblem::conflict( "application.work-attempt.fence-conflict", "The Work attempt lease fence changed after this transition was prepared.", ), @@ -86,14 +86,3 @@ pub(super) fn invalid_problem(code: &str, message: &str) -> ApplicationProblem { legal_actions: vec![LegalAction::CorrectRequest], } } - -pub(super) fn conflict_problem(code: &str, message: &str) -> ApplicationProblem { - ApplicationProblem::Conflict { - diagnostic: SafeDiagnostic { - code: code.to_owned(), - message: message.to_owned(), - }, - retry: RetryDirective::AfterRevalidate, - legal_actions: vec![LegalAction::Refresh], - } -} diff --git a/crates/tracedecay-contracts/src/work_attempt/product_admission.rs b/crates/tracedecay-contracts/src/work_attempt/product_admission.rs index b616b3bfb7..4ad95474ac 100644 --- a/crates/tracedecay-contracts/src/work_attempt/product_admission.rs +++ b/crates/tracedecay-contracts/src/work_attempt/product_admission.rs @@ -22,8 +22,7 @@ use crate::{ use super::{ StartWorkAttemptCommand, WorkAttemptAdmissionKind, WorkAttemptStorageError, - WorkAttemptStoragePort, conflict_problem, contract_problem, denied_problem, not_found_problem, - storage_problem, + WorkAttemptStoragePort, contract_problem, denied_problem, not_found_problem, storage_problem, }; const WORK_PRODUCT_START_INPUT_DIGEST_DOMAIN: &str = @@ -215,15 +214,15 @@ pub(crate) fn product_admission_problem( } } WorkProductAttemptAdmissionErrorV1::NotFoundOrNotAuthorized => not_found_problem(), - WorkProductAttemptAdmissionErrorV1::VersionConflict => conflict_problem( + WorkProductAttemptAdmissionErrorV1::VersionConflict => ApplicationProblem::conflict( "application.work-attempt.product-version-conflict", "The canonical Work product graph changed before attempt admission.", ), - WorkProductAttemptAdmissionErrorV1::IdentityConflict => conflict_problem( + WorkProductAttemptAdmissionErrorV1::IdentityConflict => ApplicationProblem::conflict( "application.work-attempt.identity-conflict", "The Work attempt identity was already used with different content.", ), - WorkProductAttemptAdmissionErrorV1::IdempotencyConflict => conflict_problem( + WorkProductAttemptAdmissionErrorV1::IdempotencyConflict => ApplicationProblem::conflict( "application.work-attempt.idempotency-conflict", "The Work attempt command identity was already used with different input.", ), @@ -287,7 +286,7 @@ where ) -> Result { admit_product_attempt_request(context, binding, command.occurred_at)?; if command.execution_snapshot.topology() != topology { - return Err(conflict_problem( + return Err(ApplicationProblem::conflict( "application.work-attempt.topology-conflict", "The Work attempt topology does not match the registered runtime authority.", )); @@ -308,7 +307,7 @@ where if admission_kind != WorkAttemptAdmissionKind::Ordinary || !replayed_attempt_matches_command(context, &command, &identity, &existing)? { - return Err(conflict_problem( + return Err(ApplicationProblem::conflict( "application.work-attempt.identity-conflict", "The Work attempt identity was already used with different content.", )); @@ -466,7 +465,7 @@ fn product_problem(error: WorkProductApplicationErrorV1) -> ApplicationProblem { } WorkProductApplicationErrorV1::TimedOut => ApplicationProblem::timed_out_before_admission(), WorkProductApplicationErrorV1::VersionConflict - | WorkProductApplicationErrorV1::RevisionConflict => conflict_problem( + | WorkProductApplicationErrorV1::RevisionConflict => ApplicationProblem::conflict( "application.work-attempt.product-version-conflict", "The canonical Work product graph changed before attempt admission.", ), @@ -478,7 +477,7 @@ fn product_problem(error: WorkProductApplicationErrorV1) -> ApplicationProblem { .to_owned(), }) } - WorkProductApplicationErrorV1::IdempotencyConflict => conflict_problem( + WorkProductApplicationErrorV1::IdempotencyConflict => ApplicationProblem::conflict( "application.work-attempt.product-idempotency-conflict", "The canonical Work product admission identity conflicts.", ), diff --git a/crates/tracedecay-contracts/src/work_attempt/product_synthesis_admission.rs b/crates/tracedecay-contracts/src/work_attempt/product_synthesis_admission.rs index 3e3ecc7dea..02ce016590 100644 --- a/crates/tracedecay-contracts/src/work_attempt/product_synthesis_admission.rs +++ b/crates/tracedecay-contracts/src/work_attempt/product_synthesis_admission.rs @@ -16,7 +16,7 @@ use crate::{ use super::{ CurrentWorkProductAttemptGraphV1, StartWorkAttemptCommand, WorkAttemptStorageError, WorkAttemptStoragePort, WorkSynthesisAdmissionStoragePort, WorkSynthesisInsertOutcome, - accepted_attempt_draft, admit_product_attempt_request, conflict_problem, contract_problem, + accepted_attempt_draft, admit_product_attempt_request, contract_problem, current_work_product_attempt_graph, denied_problem, not_found_problem, product_admission_problem, product_attempt_projection_binding, storage_problem, }; @@ -259,7 +259,7 @@ where } fn identity_conflict() -> ApplicationProblem { - conflict_problem( + ApplicationProblem::conflict( "application.work-attempt.identity-conflict", "The Work attempt identity was already used with different content.", ) diff --git a/crates/tracedecay-contracts/src/work_duplicate_adjudication.rs b/crates/tracedecay-contracts/src/work_duplicate_adjudication.rs index 78f8f9fb0c..2f9b3fdeb4 100644 --- a/crates/tracedecay-contracts/src/work_duplicate_adjudication.rs +++ b/crates/tracedecay-contracts/src/work_duplicate_adjudication.rs @@ -415,25 +415,15 @@ fn storage_problem(error: WorkDuplicateAdjudicationStorageErrorV1) -> Applicatio WorkDuplicateAdjudicationStorageErrorV1::NotFoundOrNotAuthorized => { ApplicationProblem::not_found_or_not_authorized(RetryDirective::Never) } - WorkDuplicateAdjudicationStorageErrorV1::RevisionConflict => ApplicationProblem::Conflict { - diagnostic: SafeDiagnostic { - code: "application.work.duplicate-adjudication.revision-conflict".to_owned(), - message: "The duplicate Work adjudication changed after this command was prepared." - .to_owned(), - }, - retry: RetryDirective::AfterRevalidate, - legal_actions: vec![LegalAction::Refresh], - }, + WorkDuplicateAdjudicationStorageErrorV1::RevisionConflict => ApplicationProblem::conflict( + "application.work.duplicate-adjudication.revision-conflict", + "The duplicate Work adjudication changed after this command was prepared.", + ), WorkDuplicateAdjudicationStorageErrorV1::IdempotencyConflict => { - ApplicationProblem::Conflict { - diagnostic: SafeDiagnostic { - code: "application.work.duplicate-adjudication.idempotency-conflict".to_owned(), - message: "The duplicate Work adjudication command identity was already used with different input." - .to_owned(), - }, - retry: RetryDirective::AfterRevalidate, - legal_actions: vec![LegalAction::Refresh], - } + ApplicationProblem::conflict( + "application.work.duplicate-adjudication.idempotency-conflict", + "The duplicate Work adjudication command identity was already used with different input.", + ) } WorkDuplicateAdjudicationStorageErrorV1::Unavailable => { ApplicationProblem::unavailable(SafeDiagnostic { diff --git a/crates/tracedecay-contracts/src/work_leak_adjudication.rs b/crates/tracedecay-contracts/src/work_leak_adjudication.rs index c212e6b4d6..c34b222c42 100644 --- a/crates/tracedecay-contracts/src/work_leak_adjudication.rs +++ b/crates/tracedecay-contracts/src/work_leak_adjudication.rs @@ -296,7 +296,7 @@ where .inspect(&authority, &command, scan_started_at, scan_deadline) .map_err(evidence_problem)?; if !evidence.validate_for(&command, scan_started_at, scan_deadline) { - return Err(conflict_problem( + return Err(ApplicationProblem::conflict( "application.work-leak.evidence-conflict", "The bounded leak scan did not prove a valid verdict.", )); @@ -345,23 +345,12 @@ fn invalid_problem() -> ApplicationProblem { } } -fn conflict_problem(code: &str, message: &str) -> ApplicationProblem { - ApplicationProblem::Conflict { - diagnostic: SafeDiagnostic { - code: code.to_owned(), - message: message.to_owned(), - }, - retry: RetryDirective::AfterRevalidate, - legal_actions: vec![LegalAction::Refresh], - } -} - fn evidence_problem(error: WorkLeakEvidenceErrorV1) -> ApplicationProblem { match error { WorkLeakEvidenceErrorV1::NotFoundOrNotAuthorized => { ApplicationProblem::not_found_or_not_authorized(RetryDirective::Never) } - WorkLeakEvidenceErrorV1::Conflict => conflict_problem( + WorkLeakEvidenceErrorV1::Conflict => ApplicationProblem::conflict( "application.work-leak.evidence-conflict", "The Work leak evidence changed during inspection.", ), @@ -382,11 +371,11 @@ fn storage_problem(error: WorkLeakAdjudicationStorageErrorV1) -> ApplicationProb WorkLeakAdjudicationStorageErrorV1::NotFoundOrNotAuthorized => { ApplicationProblem::not_found_or_not_authorized(RetryDirective::Never) } - WorkLeakAdjudicationStorageErrorV1::RevisionConflict => conflict_problem( + WorkLeakAdjudicationStorageErrorV1::RevisionConflict => ApplicationProblem::conflict( "application.work-leak.revision-conflict", "The Work leak adjudication changed before publication.", ), - WorkLeakAdjudicationStorageErrorV1::IdempotencyConflict => conflict_problem( + WorkLeakAdjudicationStorageErrorV1::IdempotencyConflict => ApplicationProblem::conflict( "application.work-leak.idempotency-conflict", "The Work leak command identity was already used with different input.", ), diff --git a/crates/tracedecay-contracts/src/work_placement.rs b/crates/tracedecay-contracts/src/work_placement.rs index ee0c3a433e..cce98ffde5 100644 --- a/crates/tracedecay-contracts/src/work_placement.rs +++ b/crates/tracedecay-contracts/src/work_placement.rs @@ -208,7 +208,7 @@ where { Ok(existing) } else { - Err(conflict_problem( + Err(ApplicationProblem::conflict( "application.work-placement.identity-conflict", "The Work run already holds a different placement.", )) @@ -347,11 +347,11 @@ fn storage_problem(error: WorkPlacementStorageError) -> ApplicationProblem { fn contract_problem(error: WorkPlacementContractError) -> ApplicationProblem { match error { - WorkPlacementContractError::AlreadyReleased => conflict_problem( + WorkPlacementContractError::AlreadyReleased => ApplicationProblem::conflict( "application.work-placement.already-released", "The Work placement was already released.", ), - WorkPlacementContractError::NonMonotonicTransition => conflict_problem( + WorkPlacementContractError::NonMonotonicTransition => ApplicationProblem::conflict( "application.work-placement.non-monotonic", "The Work placement transition is older than the published state.", ), @@ -380,18 +380,14 @@ fn blocked_problem( .map(placement_blocker_name) .collect::>() .join(", "); - ApplicationProblem::Conflict { - diagnostic: SafeDiagnostic { - code: "application.work-placement.blocked".to_owned(), - message: format!("The Work placement is blocked by: {named}."), - }, - retry: RetryDirective::AfterRevalidate, - legal_actions: vec![LegalAction::Refresh], - } + ApplicationProblem::conflict( + "application.work-placement.blocked", + format!("The Work placement is blocked by: {named}."), + ) } fn authority_conflict_problem() -> ApplicationProblem { - conflict_problem( + ApplicationProblem::conflict( "application.work-placement.authority-conflict", "The Work placement authority version changed after this command was prepared.", ) @@ -401,17 +397,6 @@ fn not_found_problem() -> ApplicationProblem { ApplicationProblem::not_found_or_not_authorized(RetryDirective::Never) } -fn conflict_problem(code: &str, message: &str) -> ApplicationProblem { - ApplicationProblem::Conflict { - diagnostic: SafeDiagnostic { - code: code.to_owned(), - message: message.to_owned(), - }, - retry: RetryDirective::AfterRevalidate, - legal_actions: vec![LegalAction::Refresh], - } -} - /// Wire names for the closed blocker vocabulary. Kept as a match so a new /// variant cannot silently become `"unknown"` through a JSON round-trip. const fn placement_blocker_name(blocker: WorkPlacementBlockerV1) -> &'static str { diff --git a/crates/tracedecay-contracts/src/work_retry.rs b/crates/tracedecay-contracts/src/work_retry.rs index 143dfa9326..8be68031f0 100644 --- a/crates/tracedecay-contracts/src/work_retry.rs +++ b/crates/tracedecay-contracts/src/work_retry.rs @@ -393,7 +393,7 @@ where .map_err(storage_problem)? { if replayed.receipt().canonical_input_digest != input_digest { - return Err(conflict_problem( + return Err(ApplicationProblem::conflict( "application.work-retry.idempotency-conflict", "The Work retry command identity was already used with different input.", )); @@ -447,7 +447,7 @@ where .map_err(evidence_problem)?; validate_failure(&command, &original, &failure)?; if failure.observed_at.0 > restarted_at.0 { - return Err(conflict_problem( + return Err(ApplicationProblem::conflict( "application.work-retry.failure-conflict", "The retry failure was observed after retry admission.", )); @@ -518,7 +518,7 @@ fn prepare_workflow_rebind( .get(&rebind.binding.step_id) .is_none_or(|plan| plan.plan_digest != rebind.binding.plan_digest) { - return Err(conflict_problem( + return Err(ApplicationProblem::conflict( "application.work-retry.workflow-binding-conflict", "The workflow child binding no longer matches its admitted plan.", )); @@ -541,7 +541,7 @@ fn prepare_workflow_rebind( && retry_receipt_digest == &receipt.owner_receipt_digest ); if !exact_replay { - return Err(conflict_problem( + return Err(ApplicationProblem::conflict( "application.work-retry.workflow-binding-conflict", "The workflow command identity was already used by another transition.", )); @@ -555,7 +555,7 @@ fn prepare_workflow_rebind( .projection .planned_fan_out_attempt(&receipt.command.original_attempt) .ok_or_else(|| { - conflict_problem( + ApplicationProblem::conflict( "application.work-retry.workflow-binding-conflict", "The original Work attempt is not the active workflow child.", ) @@ -578,7 +578,7 @@ fn prepare_workflow_rebind( }, ) .map_err(|_| { - conflict_problem( + ApplicationProblem::conflict( "application.work-retry.workflow-binding-conflict", "The workflow child retry transition is no longer authorized.", ) @@ -600,7 +600,7 @@ fn require_product_retry_admission( if !item.is_execution_admitted() || item.accepted_proposal() != Some(attempt.projection_binding().accepted_proposal()) { - return Err(conflict_problem( + return Err(ApplicationProblem::conflict( "application.work-retry.product-conflict", "The canonical Work product graph no longer admits this retry.", )); @@ -628,7 +628,7 @@ where { Ok(()) } else { - Err(conflict_problem( + Err(ApplicationProblem::conflict( "application.work-retry.effect-unknown", "The original Work attempt has an unresolved non-repeatable effect.", )) @@ -653,7 +653,7 @@ where if original.execution().execution_snapshot().topology() != topology || restarted_at.0 >= original.execution().deadline().0 { - return Err(conflict_problem( + return Err(ApplicationProblem::conflict( "application.work-retry.admission-conflict", "The original Work admission no longer permits this retry.", )); @@ -734,14 +734,14 @@ fn validate_failure( failure: &VerifiedWorkRetryFailureV1, ) -> Result<(), ApplicationProblem> { if failure.selector != command.failure || failure.evidence_digest.validate().is_err() { - return Err(conflict_problem( + return Err(ApplicationProblem::conflict( "application.work-retry.failure-conflict", "The resolved failure does not authorize this Work retry.", )); } if command.failure.cause == WorkRetryCauseV1::RestartRecoveryRequired { let WorkRecoveryStateV1::RecoveryRequired { observed_at, .. } = original.recovery() else { - return Err(conflict_problem( + return Err(ApplicationProblem::conflict( "application.work-retry.recovery-conflict", "The original Work attempt no longer requires restart recovery.", )); @@ -749,7 +749,7 @@ fn validate_failure( if original.state() != WorkAttemptStateV1::RecoveryRequired || observed_at != &failure.observed_at { - return Err(conflict_problem( + return Err(ApplicationProblem::conflict( "application.work-retry.recovery-conflict", "The restart recovery evidence no longer matches the original attempt.", )); @@ -757,7 +757,7 @@ fn validate_failure( return Ok(()); } let Some(terminal) = original.terminal() else { - return Err(conflict_problem( + return Err(ApplicationProblem::conflict( "application.work-retry.original-not-terminal", "A runtime failure retry requires terminal evidence.", )); @@ -781,7 +781,7 @@ fn validate_failure( } => (evidence_digest, observed_at, false), }; if !eligible || digest != &failure.evidence_digest || observed_at != &failure.observed_at { - return Err(conflict_problem( + return Err(ApplicationProblem::conflict( "application.work-retry.runtime-evidence-conflict", "The runtime failure no longer matches the original terminal receipt.", )); @@ -807,17 +807,6 @@ fn retry_receipt_problem(_error: ApplicationContractError) -> ApplicationProblem }) } -fn conflict_problem(code: &str, message: &str) -> ApplicationProblem { - ApplicationProblem::Conflict { - diagnostic: SafeDiagnostic { - code: code.to_owned(), - message: message.to_owned(), - }, - retry: RetryDirective::AfterRevalidate, - legal_actions: vec![LegalAction::Refresh], - } -} - fn not_found_problem() -> ApplicationProblem { ApplicationProblem::not_found_or_not_authorized(RetryDirective::Never) } @@ -825,7 +814,7 @@ fn not_found_problem() -> ApplicationProblem { fn evidence_problem(error: WorkRetryEvidenceErrorV1) -> ApplicationProblem { match error { WorkRetryEvidenceErrorV1::NotFoundOrNotAuthorized => not_found_problem(), - WorkRetryEvidenceErrorV1::Conflict => conflict_problem( + WorkRetryEvidenceErrorV1::Conflict => ApplicationProblem::conflict( "application.work-retry.failure-conflict", "The Work retry failure evidence changed.", ), @@ -839,17 +828,17 @@ fn evidence_problem(error: WorkRetryEvidenceErrorV1) -> ApplicationProblem { fn storage_problem(error: WorkAttemptStorageError) -> ApplicationProblem { match error { WorkAttemptStorageError::NotFoundOrNotAuthorized => not_found_problem(), - WorkAttemptStorageError::CapacityExceeded => conflict_problem( + WorkAttemptStorageError::CapacityExceeded => ApplicationProblem::conflict( "application.work-retry.capacity-exhausted", "Work retry capacity is exhausted.", ), - WorkAttemptStorageError::ReservationFenced => conflict_problem( + WorkAttemptStorageError::ReservationFenced => ApplicationProblem::conflict( "application.work-retry.reservation-fenced", "The Work run does not currently admit a retry reservation.", ), WorkAttemptStorageError::AttemptConflict | WorkAttemptStorageError::RunAdmissionConflict - | WorkAttemptStorageError::FenceConflict => conflict_problem( + | WorkAttemptStorageError::FenceConflict => ApplicationProblem::conflict( "application.work-retry.conflict", "The Work retry authority changed.", ), @@ -865,7 +854,7 @@ fn effect_storage_problem(error: WorkAttemptEffectStorageErrorV1) -> Application WorkAttemptEffectStorageErrorV1::NotFoundOrNotAuthorized => { ApplicationProblem::not_found_or_not_authorized(RetryDirective::Never) } - WorkAttemptEffectStorageErrorV1::Conflict => conflict_problem( + WorkAttemptEffectStorageErrorV1::Conflict => ApplicationProblem::conflict( "application.work-retry.effect-conflict", "The original Work attempt effect receipt changed.", ), diff --git a/crates/tracedecay-contracts/src/work_run_control.rs b/crates/tracedecay-contracts/src/work_run_control.rs index 41bc81a612..984170d4de 100644 --- a/crates/tracedecay-contracts/src/work_run_control.rs +++ b/crates/tracedecay-contracts/src/work_run_control.rs @@ -406,7 +406,7 @@ where let current = frontier.control.clone().ok_or_else(|| { // A run that was never paused has nothing to resume, and // answering "resumed" would be a false receipt. - conflict_problem( + ApplicationProblem::conflict( "application.work-run-control.not-paused", "The Work run has no published control state to resume.", ) @@ -498,7 +498,7 @@ where // are indistinguishable from an idle one. hotpath::gauge!("application.work.run_control.reservation.denied_paused") .inc(1u64); - Err(conflict_problem( + Err(ApplicationProblem::conflict( "application.work-run-control.paused", "The Work run is paused, so no new attempt reservation is admitted.", )) @@ -595,15 +595,15 @@ fn storage_problem(error: WorkRunControlStorageError) -> ApplicationProblem { fn contract_problem(error: WorkRunControlContractError) -> ApplicationProblem { match error { - WorkRunControlContractError::AlreadyPaused => conflict_problem( + WorkRunControlContractError::AlreadyPaused => ApplicationProblem::conflict( "application.work-run-control.already-paused", "The Work run is already paused.", ), - WorkRunControlContractError::NotPaused => conflict_problem( + WorkRunControlContractError::NotPaused => ApplicationProblem::conflict( "application.work-run-control.not-paused", "The Work run is not paused.", ), - WorkRunControlContractError::NonMonotonicTransition => conflict_problem( + WorkRunControlContractError::NonMonotonicTransition => ApplicationProblem::conflict( "application.work-run-control.non-monotonic", "The Work run control transition is older than the published state.", ), @@ -674,7 +674,7 @@ fn invalid_open_interval_durable_problem() -> ApplicationProblem { } fn authority_conflict_problem() -> ApplicationProblem { - conflict_problem( + ApplicationProblem::conflict( "application.work-run-control.authority-conflict", "The Work run control authority version changed after this command was prepared.", ) @@ -683,14 +683,3 @@ fn authority_conflict_problem() -> ApplicationProblem { fn not_found_problem() -> ApplicationProblem { ApplicationProblem::not_found_or_not_authorized(RetryDirective::Never) } - -fn conflict_problem(code: &str, message: &str) -> ApplicationProblem { - ApplicationProblem::Conflict { - diagnostic: SafeDiagnostic { - code: code.to_owned(), - message: message.to_owned(), - }, - retry: RetryDirective::AfterRevalidate, - legal_actions: vec![LegalAction::Refresh], - } -} From 9f87321eda0a674142b135b4a265e05e36cedb7b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:34:48 +0000 Subject: [PATCH 030/182] simplify(pass-1/5): share the LCM token counter Co-authored-by: Zack Jackson --- crates/tracedecay-lcm/src/compression.rs | 9 +-------- crates/tracedecay-lcm/src/compression_policy.rs | 7 +------ 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/crates/tracedecay-lcm/src/compression.rs b/crates/tracedecay-lcm/src/compression.rs index 09717cb955..129fecded7 100644 --- a/crates/tracedecay-lcm/src/compression.rs +++ b/crates/tracedecay-lcm/src/compression.rs @@ -13,7 +13,7 @@ use super::compression_decision::{ CondensationDecision, CondensationDecisionInput, OverflowRecoveryCapInput, PreflightDecisionInput, }; -use super::compression_policy::is_policy_anchor_role; +use super::compression_policy::{is_policy_anchor_role, source_token_count}; use super::extraction; use super::summarizer::CompressionSummarizerAdapter; use super::types::{LcmExtractionResult, LcmRelationProjectionStatus, LcmSummarySourceRange}; @@ -2640,13 +2640,6 @@ fn summary_replay_message(summary: &LcmSummaryNode) -> Value { }) } -fn source_token_count(backlog: &[LcmRawMessage]) -> i64 { - backlog - .iter() - .map(|message| crate::lcm_budget_tokens(&message.content)) - .sum::() -} - fn debt_for_deferred_backlog(deferred_backlog: &[LcmRawMessage]) -> Vec { match (deferred_backlog.first(), deferred_backlog.last()) { (Some(first), Some(last)) => vec![LcmMaintenanceDebt::RawBacklog { diff --git a/crates/tracedecay-lcm/src/compression_policy.rs b/crates/tracedecay-lcm/src/compression_policy.rs index b0d404d26b..ca9a97cd3b 100644 --- a/crates/tracedecay-lcm/src/compression_policy.rs +++ b/crates/tracedecay-lcm/src/compression_policy.rs @@ -203,12 +203,7 @@ pub fn forced_overflow_pressure( current_tokens: Option, max_assembly_tokens: Option, ) -> bool { - match (current_tokens, max_assembly_tokens) { - (Some(current_tokens), Some(max_assembly_tokens)) if max_assembly_tokens > 0 => { - current_tokens >= max_assembly_tokens - } - _ => false, - } + threshold_pressure(current_tokens, max_assembly_tokens) } pub fn source_token_count(backlog: &[LcmRawMessage]) -> i64 { From fa088b666c230c0903aeef19282b087e0ff4c242 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:35:15 +0000 Subject: [PATCH 031/182] simplify(pass-1/5): use str::floor_char_boundary for cuts Co-authored-by: Zack Jackson --- crates/tracedecay-lsp/src/diagnostics.rs | 9 +-------- .../tracedecay-runtime-core/src/db/access/owner_io.rs | 8 +------- crates/tracedecay-runtime-core/src/text.rs | 10 +--------- 3 files changed, 3 insertions(+), 24 deletions(-) diff --git a/crates/tracedecay-lsp/src/diagnostics.rs b/crates/tracedecay-lsp/src/diagnostics.rs index d4a8bb6325..770f66324f 100644 --- a/crates/tracedecay-lsp/src/diagnostics.rs +++ b/crates/tracedecay-lsp/src/diagnostics.rs @@ -436,14 +436,7 @@ fn utf16_column_to_byte_offset( } pub(crate) fn truncate_utf8(value: &mut String, max_bytes: usize) { - if value.len() <= max_bytes { - return; - } - let mut boundary = max_bytes; - while !value.is_char_boundary(boundary) { - boundary -= 1; - } - value.truncate(boundary); + value.truncate(value.floor_char_boundary(max_bytes)); } #[cfg(test)] diff --git a/crates/tracedecay-runtime-core/src/db/access/owner_io.rs b/crates/tracedecay-runtime-core/src/db/access/owner_io.rs index 706df4e045..e650112e9c 100644 --- a/crates/tracedecay-runtime-core/src/db/access/owner_io.rs +++ b/crates/tracedecay-runtime-core/src/db/access/owner_io.rs @@ -481,12 +481,6 @@ fn sanitize_metadata(value: &str) -> String { const MAX_METADATA_BYTES: usize = 256; let mut sanitized = value.replace(['\t', '\r', '\n'], " "); - if sanitized.len() > MAX_METADATA_BYTES { - let mut boundary = MAX_METADATA_BYTES; - while !sanitized.is_char_boundary(boundary) { - boundary -= 1; - } - sanitized.truncate(boundary); - } + sanitized.truncate(sanitized.floor_char_boundary(MAX_METADATA_BYTES)); sanitized } diff --git a/crates/tracedecay-runtime-core/src/text.rs b/crates/tracedecay-runtime-core/src/text.rs index eb4777dc18..c19a075b71 100644 --- a/crates/tracedecay-runtime-core/src/text.rs +++ b/crates/tracedecay-runtime-core/src/text.rs @@ -7,15 +7,7 @@ /// This is the safe replacement for `&s[..max_bytes]` when `s` may contain /// non-ASCII text and the caller has a byte budget rather than a char budget. pub fn utf8_prefix_at_or_before(s: &str, max_bytes: usize) -> &str { - if s.len() <= max_bytes { - return s; - } - - let mut end = max_bytes; - while !s.is_char_boundary(end) && end > 0 { - end -= 1; - } - &s[..end] + &s[..s.floor_char_boundary(max_bytes)] } /// Formats a token count as a compact string (e.g. "1.2M", "45.3k"). From 4ae6246bed6979079faa14d8fa3c79de87465dc4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:35:15 +0000 Subject: [PATCH 032/182] simplify(pass-2/5): one lifecycle write path Co-authored-by: Zack Jackson --- crates/tracedecay-lcm/src/compression.rs | 58 ++++++++---------------- 1 file changed, 18 insertions(+), 40 deletions(-) diff --git a/crates/tracedecay-lcm/src/compression.rs b/crates/tracedecay-lcm/src/compression.rs index 129fecded7..3d0f531970 100644 --- a/crates/tracedecay-lcm/src/compression.rs +++ b/crates/tracedecay-lcm/src/compression.rs @@ -114,14 +114,7 @@ pub async fn update_lifecycle( conn: &impl Executor, update: LcmLifecycleUpdate, ) -> Result { - upsert_lifecycle_state(conn, &update).await?; - replace_maintenance_debt( - conn, - &update.provider, - &update.conversation_id, - &update.maintenance_debt, - ) - .await?; + persist_lifecycle_update(conn, &update).await?; lifecycle_state(conn, &update.provider, &update.conversation_id).await } @@ -206,14 +199,6 @@ async fn link_session_boundary( conn: &impl Executor, request: &LcmSessionBoundaryRequest, old_session_id: &str, -) -> Result { - link_in_transaction(conn, request, old_session_id).await -} - -async fn link_in_transaction( - conn: &impl Executor, - request: &LcmSessionBoundaryRequest, - old_session_id: &str, ) -> Result { ensure_session(conn, &request.provider, &request.session_id).await?; let old_state = @@ -237,14 +222,7 @@ async fn link_in_transaction( last_finalized_frontier_store_id: carried_frontier, maintenance_debt: old_state.maintenance_debt.clone(), }; - upsert_lifecycle_state(conn, &update).await?; - replace_maintenance_debt( - conn, - &update.provider, - &update.conversation_id, - &update.maintenance_debt, - ) - .await?; + persist_lifecycle_update(conn, &update).await?; Ok(session_boundary_response( true, @@ -1050,14 +1028,7 @@ async fn persist_compression_transaction_writes<'a>( last_finalized_frontier_store_id: write.existing_frontier.last_finalized_frontier_store_id, maintenance_debt: debt_for_deferred_backlog(remaining_backlog), }; - upsert_lifecycle_state(conn, &update).await?; - replace_maintenance_debt( - conn, - &update.provider, - &update.conversation_id, - &update.maintenance_debt, - ) - .await?; + persist_lifecycle_update(conn, &update).await?; Ok(CompressionTransactionWriteResult { created_summaries, @@ -1066,6 +1037,20 @@ async fn persist_compression_transaction_writes<'a>( }) } +async fn persist_lifecycle_update( + conn: &impl Executor, + update: &LcmLifecycleUpdate, +) -> Result<(), LcmError> { + upsert_lifecycle_state(conn, update).await?; + replace_maintenance_debt( + conn, + &update.provider, + &update.conversation_id, + &update.maintenance_debt, + ) + .await +} + async fn upsert_lifecycle_state( conn: &impl Executor, update: &LcmLifecycleUpdate, @@ -1852,14 +1837,7 @@ async fn condense_summary_nodes_if_ready( last_finalized_frontier_store_id: existing_frontier.last_finalized_frontier_store_id, maintenance_debt: existing_frontier.maintenance_debt.clone(), }; - upsert_lifecycle_state(conn, &update).await?; - replace_maintenance_debt( - conn, - &update.provider, - &update.conversation_id, - &update.maintenance_debt, - ) - .await?; + persist_lifecycle_update(conn, &update).await?; let frontier = lifecycle_state(conn, &update.provider, &update.conversation_id).await?; // Mirrors hermes-lcm: `_assemble_context` always follows // `_maybe_condense`, so a condensation-only pass still returns the From d2360a739209adf83b606058a9e567fb97f42eff Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:35:37 +0000 Subject: [PATCH 033/182] simplify(pass-2/5): map exact SQL rows in one place Co-authored-by: Zack Jackson --- .../src/db/engine/connection.rs | 23 +++---------------- .../src/db/engine/row.rs | 10 ++++++++ .../src/db/engine/snapshot.rs | 12 ++-------- .../src/db/engine/transaction.rs | 12 ++-------- 4 files changed, 17 insertions(+), 40 deletions(-) diff --git a/crates/tracedecay-runtime-core/src/db/engine/connection.rs b/crates/tracedecay-runtime-core/src/db/engine/connection.rs index c4ee65e924..fb75047d5c 100644 --- a/crates/tracedecay-runtime-core/src/db/engine/connection.rs +++ b/crates/tracedecay-runtime-core/src/db/engine/connection.rs @@ -10,8 +10,7 @@ pub use tracedecay_rusqlite_runtime::reader::{ReaderPoolSnapshot, ReaderPoolStat #[cfg(any(test, feature = "test-helpers"))] use super::Statement; use super::{ - Error, IntoParams, ReadSnapshot, Result, Rows, Transaction, TransactionBehavior, Value, - WriteStatement, + Error, IntoParams, ReadSnapshot, Result, Rows, Transaction, TransactionBehavior, WriteStatement, }; const READER_WAIT: Duration = Duration::from_secs(5); @@ -166,30 +165,14 @@ impl Connection { }) .await .map_err(join_error)??; - Ok(Rows::from_parts( - rows.columns, - rows.rows - .into_iter() - .map(|row| { - super::Row::from_values(row.values.into_iter().map(Value::from).collect()) - }) - .collect(), - )) + Ok(Rows::from_exact(rows)) } #[hotpath::skip] pub async fn checkpoint_wal_truncate(&self) -> Result { let runtime = Arc::clone(&self.runtime); let rows = runtime.checkpoint_wal_truncate_async().await?; - Ok(Rows::from_parts( - rows.columns, - rows.rows - .into_iter() - .map(|row| { - super::Row::from_values(row.values.into_iter().map(Value::from).collect()) - }) - .collect(), - )) + Ok(Rows::from_exact(rows)) } #[hotpath::skip] diff --git a/crates/tracedecay-runtime-core/src/db/engine/row.rs b/crates/tracedecay-runtime-core/src/db/engine/row.rs index 9fa898cc26..ec66bf7e41 100644 --- a/crates/tracedecay-runtime-core/src/db/engine/row.rs +++ b/crates/tracedecay-runtime-core/src/db/engine/row.rs @@ -38,6 +38,16 @@ impl Rows { } } + pub(super) fn from_exact(rows: tracedecay_rusqlite_runtime::exact_sql::ExactSqlRows) -> Self { + Self::from_parts( + rows.columns, + rows.rows + .into_iter() + .map(|row| Row::from_values(row.values.into_iter().map(Value::from).collect())) + .collect(), + ) + } + pub fn column_count(&self) -> i32 { i32::try_from(self.columns.len()).unwrap_or(i32::MAX) } diff --git a/crates/tracedecay-runtime-core/src/db/engine/snapshot.rs b/crates/tracedecay-runtime-core/src/db/engine/snapshot.rs index b92717e3a4..e2919001b5 100644 --- a/crates/tracedecay-runtime-core/src/db/engine/snapshot.rs +++ b/crates/tracedecay-runtime-core/src/db/engine/snapshot.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use tracedecay_rusqlite_runtime::exact_sql::ExactSqlReadSnapshot; -use super::{IntoParams, Result, Rows, Value, connection::statement}; +use super::{IntoParams, Result, Rows, connection::statement}; pub struct ReadSnapshot { /// One snapshot holds one pooled reader worker for its whole lifetime, and @@ -33,15 +33,7 @@ impl ReadSnapshot { }) .await .map_err(join_error)??; - Ok(Rows::from_parts( - rows.columns, - rows.rows - .into_iter() - .map(|row| { - super::Row::from_values(row.values.into_iter().map(Value::from).collect()) - }) - .collect(), - )) + Ok(Rows::from_exact(rows)) } } diff --git a/crates/tracedecay-runtime-core/src/db/engine/transaction.rs b/crates/tracedecay-runtime-core/src/db/engine/transaction.rs index 3c4947c140..48b4c256af 100644 --- a/crates/tracedecay-runtime-core/src/db/engine/transaction.rs +++ b/crates/tracedecay-runtime-core/src/db/engine/transaction.rs @@ -6,7 +6,7 @@ use tracedecay_rusqlite_runtime::exact_sql::{ ExactSqlAttachment, ExactSqlHandle, ExactSqlTransaction as RuntimeTransaction, }; -use super::{Error, IntoParams, Result, Rows, Value, WriteStatement, connection::statement}; +use super::{Error, IntoParams, Result, Rows, WriteStatement, connection::statement}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum TransactionBehavior { @@ -128,15 +128,7 @@ impl Transaction { }) .await .map_err(join_error)??; - Ok(Rows::from_parts( - rows.columns, - rows.rows - .into_iter() - .map(|row| { - super::Row::from_values(row.values.into_iter().map(Value::from).collect()) - }) - .collect(), - )) + Ok(Rows::from_exact(rows)) } #[hotpath::skip] From de9b4a9a2ac7a306804049f0aab22625befbdb89 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:32:09 +0000 Subject: [PATCH 034/182] simplify(pass-1/5): compress MCP binding rows Co-authored-by: Zack Jackson --- commitlint.config.cjs | 1 + crates/tracedecay-mcp/src/tools/binding.rs | 209 ++++++++------------- 2 files changed, 84 insertions(+), 126 deletions(-) diff --git a/commitlint.config.cjs b/commitlint.config.cjs index 18b34ae514..3f19d02c48 100644 --- a/commitlint.config.cjs +++ b/commitlint.config.cjs @@ -8,6 +8,7 @@ const allowedTypes = [ "perf", "refactor", "revert", + "simplify", "style", "test", ]; diff --git a/crates/tracedecay-mcp/src/tools/binding.rs b/crates/tracedecay-mcp/src/tools/binding.rs index 53f47eb3df..cb2aaa4c58 100644 --- a/crates/tracedecay-mcp/src/tools/binding.rs +++ b/crates/tracedecay-mcp/src/tools/binding.rs @@ -239,131 +239,89 @@ pub struct McpToolBinding { pub project: RegisteredProjectAccess, } +struct BindingGroup { + group: Option, + project: RegisteredProjectAccess, + names: &'static [&'static str], +} + +macro_rules! binding_groups { + ($([$group:expr, $project:expr, $($name:literal),+ $(,)?]),+ $(,)?) => { + &[$(BindingGroup { group: $group, project: $project, names: &[$($name),+] },)+] + }; +} + +/// Dispatch-catalog order. Consecutive tools that share a group and project +/// access are one slice; a different access starts a new slice. #[rustfmt::skip] -const MCP_TOOL_BINDING_SPECS: &[McpToolBinding] = &[ - McpToolBinding { name: "tracedecay_search", group: Some(McpToolDispatchGroup::Graph), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_grep", group: Some(McpToolDispatchGroup::Graph), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_ast_grep_search", group: Some(McpToolDispatchGroup::Graph), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_retrieve", group: Some(McpToolDispatchGroup::Graph), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_context", group: Some(McpToolDispatchGroup::Graph), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_callers", group: Some(McpToolDispatchGroup::Graph), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_callees", group: Some(McpToolDispatchGroup::Graph), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_impact", group: Some(McpToolDispatchGroup::Graph), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_node", group: Some(McpToolDispatchGroup::Graph), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_similar", group: Some(McpToolDispatchGroup::Graph), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_redundancy", group: Some(McpToolDispatchGroup::Graph), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_rename_preview", group: Some(McpToolDispatchGroup::Graph), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_implementations", group: Some(McpToolDispatchGroup::Graph), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_callers_for", group: Some(McpToolDispatchGroup::Graph), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_find_exact_symbol", group: Some(McpToolDispatchGroup::Graph), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_by_qualified_name", group: Some(McpToolDispatchGroup::Graph), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_signature", group: Some(McpToolDispatchGroup::Graph), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_impls", group: Some(McpToolDispatchGroup::Graph), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_derives", group: Some(McpToolDispatchGroup::Graph), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_status", group: Some(McpToolDispatchGroup::Info), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_remote_status", group: Some(McpToolDispatchGroup::Info), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_active_project", group: Some(McpToolDispatchGroup::Info), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_project_list", group: Some(McpToolDispatchGroup::Info), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_project_search", group: Some(McpToolDispatchGroup::Info), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_project_context", group: Some(McpToolDispatchGroup::Info), project: RegisteredProjectAccess::SelectorOnly }, - McpToolBinding { name: "tracedecay_files", group: Some(McpToolDispatchGroup::Info), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_admin_sync", group: Some(McpToolDispatchGroup::Info), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_port_status", group: Some(McpToolDispatchGroup::Info), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_port_order", group: Some(McpToolDispatchGroup::Info), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_type_hierarchy", group: Some(McpToolDispatchGroup::Info), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_body", group: Some(McpToolDispatchGroup::Info), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_todos", group: Some(McpToolDispatchGroup::Info), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_read", group: Some(McpToolDispatchGroup::Info), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_outline", group: Some(McpToolDispatchGroup::Info), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_config", group: Some(McpToolDispatchGroup::Info), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_signature_search", group: Some(McpToolDispatchGroup::Info), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_hook_runtime", group: Some(McpToolDispatchGroup::Admin), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_admin_cli", group: Some(McpToolDispatchGroup::Admin), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_admin_project", group: Some(McpToolDispatchGroup::Admin), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_dead_code", group: Some(McpToolDispatchGroup::Analysis), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_circular", group: Some(McpToolDispatchGroup::Analysis), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_hotspots", group: Some(McpToolDispatchGroup::Analysis), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_unmounted_files", group: Some(McpToolDispatchGroup::Analysis), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_rank", group: Some(McpToolDispatchGroup::Analysis), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_largest", group: Some(McpToolDispatchGroup::Analysis), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_coupling", group: Some(McpToolDispatchGroup::Analysis), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_inheritance_depth", group: Some(McpToolDispatchGroup::Analysis), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_distribution", group: Some(McpToolDispatchGroup::Analysis), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_recursion", group: Some(McpToolDispatchGroup::Analysis), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_complexity", group: Some(McpToolDispatchGroup::Analysis), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_doc_coverage", group: Some(McpToolDispatchGroup::Analysis), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_god_class", group: Some(McpToolDispatchGroup::Analysis), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_unsafe_patterns", group: Some(McpToolDispatchGroup::Analysis), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_constructors", group: Some(McpToolDispatchGroup::Analysis), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_field_sites", group: Some(McpToolDispatchGroup::Analysis), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_admin_branch_add", group: Some(McpToolDispatchGroup::Git), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_affected", group: Some(McpToolDispatchGroup::Git), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_diff_context", group: Some(McpToolDispatchGroup::Git), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_changelog", group: Some(McpToolDispatchGroup::Git), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_commit_context", group: Some(McpToolDispatchGroup::Git), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_pr_context", group: Some(McpToolDispatchGroup::Git), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_branch_search", group: Some(McpToolDispatchGroup::Git), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_branch_diff", group: Some(McpToolDispatchGroup::Git), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_branch_list", group: Some(McpToolDispatchGroup::Git), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_str_replace", group: Some(McpToolDispatchGroup::Edit), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_multi_str_replace", group: Some(McpToolDispatchGroup::Edit), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_insert_at", group: Some(McpToolDispatchGroup::Edit), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_ast_grep_rewrite", group: Some(McpToolDispatchGroup::Edit), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_replace_symbol", group: Some(McpToolDispatchGroup::Edit), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_insert_at_symbol", group: Some(McpToolDispatchGroup::Edit), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_move_symbol", group: Some(McpToolDispatchGroup::Edit), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_rename_symbol", group: Some(McpToolDispatchGroup::Edit), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_source_edit_reconcile", group: Some(McpToolDispatchGroup::Edit), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_source_edit_rollback", group: Some(McpToolDispatchGroup::Edit), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_test_map", group: Some(McpToolDispatchGroup::Health), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_gini", group: Some(McpToolDispatchGroup::Health), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_dependency_depth", group: Some(McpToolDispatchGroup::Health), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_health", group: Some(McpToolDispatchGroup::Health), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_runtime", group: Some(McpToolDispatchGroup::Health), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_dsm", group: Some(McpToolDispatchGroup::Health), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_test_risk", group: Some(McpToolDispatchGroup::Health), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_automation_run_list", group: Some(McpToolDispatchGroup::Memory), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_automation_run_view", group: Some(McpToolDispatchGroup::Memory), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_automation_run_artifact_view", group: Some(McpToolDispatchGroup::Memory), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_analytics", group: Some(McpToolDispatchGroup::Memory), project: RegisteredProjectAccess::SelectorOnly }, - McpToolBinding { name: "tracedecay_skill_list", group: Some(McpToolDispatchGroup::Memory), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_skill_view", group: Some(McpToolDispatchGroup::Memory), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_hermes_skill_bridge", group: Some(McpToolDispatchGroup::Memory), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_multi_root_scope_set_read", group: Some(McpToolDispatchGroup::MultiRoot), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_multi_root_scope_set_compare_and_swap", group: Some(McpToolDispatchGroup::MultiRoot), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_multi_root_execute", group: Some(McpToolDispatchGroup::MultiRoot), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_diagnose", group: Some(McpToolDispatchGroup::SessionWorkflow), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_run_affected_tests", group: Some(McpToolDispatchGroup::SessionWorkflow), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_dashboard", group: Some(McpToolDispatchGroup::SessionWorkflow), project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_fact_feedback", group: None, project: RegisteredProjectAccess::SelectorOnly }, - McpToolBinding { name: "tracedecay_lcm_describe", group: None, project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_lcm_doctor", group: None, project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_lcm_expand", group: None, project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_lcm_expand_query", group: None, project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_lcm_grep", group: None, project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_lcm_load_session", group: None, project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_lcm_status", group: None, project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_session_refresh_begin", group: None, project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_session_refresh_status", group: None, project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_session_refresh_cancel", group: None, project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_sessions_for", group: None, project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_workflows", group: None, project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_fact_store_curate", group: None, project: RegisteredProjectAccess::ActiveProjectOnly }, - McpToolBinding { name: "tracedecay_fact_store_add", group: None, project: RegisteredProjectAccess::SelectorOnly }, - McpToolBinding { name: "tracedecay_fact_store_search", group: None, project: RegisteredProjectAccess::SelectorOnly }, - McpToolBinding { name: "tracedecay_fact_store_probe", group: None, project: RegisteredProjectAccess::SelectorOnly }, - McpToolBinding { name: "tracedecay_fact_store_related", group: None, project: RegisteredProjectAccess::SelectorOnly }, - McpToolBinding { name: "tracedecay_fact_store_reason", group: None, project: RegisteredProjectAccess::SelectorOnly }, - McpToolBinding { name: "tracedecay_fact_store_contradict", group: None, project: RegisteredProjectAccess::SelectorOnly }, - McpToolBinding { name: "tracedecay_fact_store_get", group: None, project: RegisteredProjectAccess::SelectorOnly }, - McpToolBinding { name: "tracedecay_fact_store_update", group: None, project: RegisteredProjectAccess::SelectorOnly }, - McpToolBinding { name: "tracedecay_fact_store_remove", group: None, project: RegisteredProjectAccess::SelectorOnly }, - McpToolBinding { name: "tracedecay_fact_store_supersede", group: None, project: RegisteredProjectAccess::SelectorOnly }, - McpToolBinding { name: "tracedecay_fact_store_list", group: None, project: RegisteredProjectAccess::SelectorOnly }, - McpToolBinding { name: "tracedecay_memory_status", group: None, project: RegisteredProjectAccess::SelectorOnly }, - McpToolBinding { name: "tracedecay_message_search", group: None, project: RegisteredProjectAccess::SelectorOnly }, +const BINDING_GROUPS: &[BindingGroup] = binding_groups![ + [Some(McpToolDispatchGroup::Graph), RegisteredProjectAccess::ActiveProjectOnly, + "tracedecay_search", "tracedecay_grep", "tracedecay_ast_grep_search", "tracedecay_retrieve", + "tracedecay_context", "tracedecay_callers", "tracedecay_callees", "tracedecay_impact", + "tracedecay_node", "tracedecay_similar", "tracedecay_redundancy", "tracedecay_rename_preview", + "tracedecay_implementations", "tracedecay_callers_for", "tracedecay_find_exact_symbol", + "tracedecay_by_qualified_name", "tracedecay_signature", "tracedecay_impls", "tracedecay_derives"], + [Some(McpToolDispatchGroup::Info), RegisteredProjectAccess::ActiveProjectOnly, + "tracedecay_status", "tracedecay_remote_status", "tracedecay_active_project", + "tracedecay_project_list", "tracedecay_project_search"], + [Some(McpToolDispatchGroup::Info), RegisteredProjectAccess::SelectorOnly, + "tracedecay_project_context"], + [Some(McpToolDispatchGroup::Info), RegisteredProjectAccess::ActiveProjectOnly, + "tracedecay_files", "tracedecay_admin_sync", "tracedecay_port_status", "tracedecay_port_order", + "tracedecay_type_hierarchy", "tracedecay_body", "tracedecay_todos", "tracedecay_read", + "tracedecay_outline", "tracedecay_config", "tracedecay_signature_search"], + [Some(McpToolDispatchGroup::Admin), RegisteredProjectAccess::ActiveProjectOnly, + "tracedecay_hook_runtime", "tracedecay_admin_cli", "tracedecay_admin_project"], + [Some(McpToolDispatchGroup::Analysis), RegisteredProjectAccess::ActiveProjectOnly, + "tracedecay_dead_code", "tracedecay_circular", "tracedecay_hotspots", "tracedecay_unmounted_files", + "tracedecay_rank", "tracedecay_largest", "tracedecay_coupling", "tracedecay_inheritance_depth", + "tracedecay_distribution", "tracedecay_recursion", "tracedecay_complexity", "tracedecay_doc_coverage", + "tracedecay_god_class", "tracedecay_unsafe_patterns", "tracedecay_constructors", "tracedecay_field_sites"], + [Some(McpToolDispatchGroup::Git), RegisteredProjectAccess::ActiveProjectOnly, + "tracedecay_admin_branch_add", "tracedecay_affected", "tracedecay_diff_context", "tracedecay_changelog", + "tracedecay_commit_context", "tracedecay_pr_context", "tracedecay_branch_search", + "tracedecay_branch_diff", "tracedecay_branch_list"], + [Some(McpToolDispatchGroup::Edit), RegisteredProjectAccess::ActiveProjectOnly, + "tracedecay_str_replace", "tracedecay_multi_str_replace", "tracedecay_insert_at", "tracedecay_ast_grep_rewrite", + "tracedecay_replace_symbol", "tracedecay_insert_at_symbol", "tracedecay_move_symbol", "tracedecay_rename_symbol", + "tracedecay_source_edit_reconcile", "tracedecay_source_edit_rollback"], + [Some(McpToolDispatchGroup::Health), RegisteredProjectAccess::ActiveProjectOnly, + "tracedecay_test_map", "tracedecay_gini", "tracedecay_dependency_depth", "tracedecay_health", + "tracedecay_runtime", "tracedecay_dsm", "tracedecay_test_risk"], + [Some(McpToolDispatchGroup::Memory), RegisteredProjectAccess::ActiveProjectOnly, + "tracedecay_automation_run_list", "tracedecay_automation_run_view", "tracedecay_automation_run_artifact_view"], + [Some(McpToolDispatchGroup::Memory), RegisteredProjectAccess::SelectorOnly, "tracedecay_analytics"], + [Some(McpToolDispatchGroup::Memory), RegisteredProjectAccess::ActiveProjectOnly, + "tracedecay_skill_list", "tracedecay_skill_view", "tracedecay_hermes_skill_bridge"], + [Some(McpToolDispatchGroup::MultiRoot), RegisteredProjectAccess::ActiveProjectOnly, + "tracedecay_multi_root_scope_set_read", "tracedecay_multi_root_scope_set_compare_and_swap", + "tracedecay_multi_root_execute"], + [Some(McpToolDispatchGroup::SessionWorkflow), RegisteredProjectAccess::ActiveProjectOnly, + "tracedecay_diagnose", "tracedecay_run_affected_tests", "tracedecay_dashboard"], + [None, RegisteredProjectAccess::SelectorOnly, "tracedecay_fact_feedback"], + [None, RegisteredProjectAccess::ActiveProjectOnly, + "tracedecay_lcm_describe", "tracedecay_lcm_doctor", "tracedecay_lcm_expand", "tracedecay_lcm_expand_query", + "tracedecay_lcm_grep", "tracedecay_lcm_load_session", "tracedecay_lcm_status", + "tracedecay_session_refresh_begin", "tracedecay_session_refresh_status", "tracedecay_session_refresh_cancel", + "tracedecay_sessions_for", "tracedecay_workflows", "tracedecay_fact_store_curate"], + [None, RegisteredProjectAccess::SelectorOnly, + "tracedecay_fact_store_add", "tracedecay_fact_store_search", "tracedecay_fact_store_probe", + "tracedecay_fact_store_related", "tracedecay_fact_store_reason", "tracedecay_fact_store_contradict", + "tracedecay_fact_store_get", "tracedecay_fact_store_update", "tracedecay_fact_store_remove", + "tracedecay_fact_store_supersede", "tracedecay_fact_store_list", "tracedecay_memory_status", + "tracedecay_message_search"], ]; +fn binding_specs() -> impl Iterator { + BINDING_GROUPS.iter().flat_map(|group| { + group.names.iter().copied().map(|name| McpToolBinding { + name, + group: group.group, + project: group.project, + }) + }) +} + pub static MCP_TOOL_BINDINGS: LazyLock> = LazyLock::new(assemble_mcp_tool_bindings); @@ -379,12 +337,11 @@ fn registered_project_readers() -> &'static HashSet<&'static str> { fn assemble_mcp_tool_bindings() -> Vec { let readers = registered_project_readers(); let mut assigned = HashSet::new(); - let bindings = MCP_TOOL_BINDING_SPECS - .iter() + let bindings = binding_specs() .map(|spec| { assert!( spec.project != RegisteredProjectAccess::Reader, - "MCP_TOOL_BINDING_SPECS must not encode Reader for '{}'; list it in tracedecay-mcp::project_access", + "binding specs must not encode Reader for '{}'; list it in tracedecay-mcp::project_access", spec.name ); let project = if readers.contains(spec.name) { @@ -409,7 +366,7 @@ fn assemble_mcp_tool_bindings() -> Vec { .collect(); assert!( missing.is_empty(), - "tracedecay-mcp reader tools have no MCP_TOOL_BINDING_SPECS row: {missing:?}" + "tracedecay-mcp reader tools have no binding spec row: {missing:?}" ); bindings } From 55ea65d818dc20183b474bfd0b75c5de6055f697 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:36:08 +0000 Subject: [PATCH 035/182] simplify(pass-3/5): share raw message upsert SQL Co-authored-by: Zack Jackson --- crates/tracedecay-lcm/src/raw.rs | 153 ++++++++++++++----------------- 1 file changed, 71 insertions(+), 82 deletions(-) diff --git a/crates/tracedecay-lcm/src/raw.rs b/crates/tracedecay-lcm/src/raw.rs index c5fa60493f..9df10ff272 100644 --- a/crates/tracedecay-lcm/src/raw.rs +++ b/crates/tracedecay-lcm/src/raw.rs @@ -365,20 +365,20 @@ fn externalized_payload_placeholder( field_path: &str, quarantine_reason: Option<&str>, ) -> String { - if let Some(reason) = quarantine_reason { - return format!( - "[Externalized LCM ingest payload: assistant output quarantined; kind={}; reason={}; field={}; chars={}; bytes={}; ref={}]", + let body = payload_placeholder_body(payload_ref, field_path); + match quarantine_reason { + Some(reason) => format!( + "[Externalized LCM ingest payload: assistant output quarantined; kind={}; reason={}; {body}]", safe_placeholder_metadata(&payload_ref.kind), safe_placeholder_metadata(reason), - safe_placeholder_metadata(field_path), - payload_ref.char_count, - payload_ref.byte_count, - payload_ref.payload_ref - ); + ), + None => format!("[Externalized LCM ingest payload: kind={}; {body}]", safe_placeholder_metadata(&payload_ref.kind)), } +} + +fn payload_placeholder_body(payload_ref: &LcmPayloadRef, field_path: &str) -> String { format!( - "[Externalized LCM ingest payload: kind={}; field={}; chars={}; bytes={}; ref={}]", - safe_placeholder_metadata(&payload_ref.kind), + "field={}; chars={}; bytes={}; ref={}", safe_placeholder_metadata(field_path), payload_ref.char_count, payload_ref.byte_count, @@ -395,6 +395,31 @@ async fn upsert_inline_raw_message( let snippet = derived_text_for_snippet(text); let index = derived_text_for_index(text); let content_hash = projected_content_hash(text); + upsert_owned_raw_message( + conn, + message, + Some(text), + content_hash.as_str(), + LcmStorageKind::Inline, + None, + snippet.as_str(), + index.as_str(), + metadata_json, + ) + .await +} + +async fn upsert_owned_raw_message( + conn: &(impl Executor + ?Sized), + message: &SessionMessageRecord, + content: Option<&str>, + content_hash: &str, + storage_kind: LcmStorageKind, + payload_ref: Option<&str>, + snippet: &str, + index_text: &str, + metadata_json: Option<&str>, +) -> Result<(), LcmError> { let affected = conn .execute( "INSERT INTO lcm_raw_messages ( @@ -402,7 +427,7 @@ async fn upsert_inline_raw_message( content, content_hash, storage_kind, payload_ref, snippet_text, index_text, legacy_source, legacy_truncated, metadata_json ) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, NULL, ?10, ?11, 0, 0, ?12) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, 0, 0, ?13) ON CONFLICT(provider, message_id) DO UPDATE SET session_id = excluded.session_id, role = excluded.role, @@ -425,11 +450,12 @@ async fn upsert_inline_raw_message( message.role.as_str(), message.ordinal, message.timestamp, - text, - content_hash.as_str(), - LcmStorageKind::Inline.as_str(), - snippet.as_str(), - index.as_str(), + content, + content_hash, + storage_kind.as_str(), + payload_ref, + snippet, + index_text, metadata_json, ], ) @@ -468,6 +494,20 @@ async fn persist_raw_predecessor_range( /// [`PREDECESSOR_RANGE_UPSERT_BY_STORE_RANGE`]) instead of per call. fn predecessor_range_upsert_sql(current_predicate: &str) -> String { let role_list = crate::compression_policy::policy_anchor_role_sql_in_list(); + let bound = |order: &str| { + format!( + "SELECT candidate.store_id + FROM lcm_raw_messages AS candidate + WHERE candidate.provider = current.provider + AND candidate.session_id = current.session_id + AND candidate.store_id < current.store_id + AND candidate.role NOT IN ({role_list}) + ORDER BY candidate.store_id{order} + LIMIT 1" + ) + }; + let first = bound(""); + let prior = bound(" DESC"); format!( "INSERT INTO lcm_raw_predecessor_ranges ( provider, message_id, session_id, from_store_id, to_store_id @@ -477,25 +517,11 @@ fn predecessor_range_upsert_sql(current_predicate: &str) -> String { FROM lcm_raw_messages AS current JOIN lcm_raw_messages AS first ON first.store_id = ( - SELECT candidate.store_id - FROM lcm_raw_messages AS candidate - WHERE candidate.provider = current.provider - AND candidate.session_id = current.session_id - AND candidate.store_id < current.store_id - AND candidate.role NOT IN ({role_list}) - ORDER BY candidate.store_id - LIMIT 1 + {first} ) JOIN lcm_raw_messages AS prior ON prior.store_id = ( - SELECT candidate.store_id - FROM lcm_raw_messages AS candidate - WHERE candidate.provider = current.provider - AND candidate.session_id = current.session_id - AND candidate.store_id < current.store_id - AND candidate.role NOT IN ({role_list}) - ORDER BY candidate.store_id DESC - LIMIT 1 + {prior} ) WHERE {current_predicate} ON CONFLICT(provider, message_id) DO UPDATE SET @@ -791,48 +817,18 @@ pub async fn commit_staged_raw_message( }); }; payload::upsert_payload_metadata(conn, &whole_message.payload_ref).await?; - let affected = conn - .execute( - "INSERT INTO lcm_raw_messages ( - provider, message_id, session_id, role, ordinal, timestamp, - content, content_hash, storage_kind, payload_ref, snippet_text, - index_text, legacy_source, legacy_truncated, metadata_json - ) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, NULL, ?7, ?8, ?9, ?10, ?11, 0, 0, ?12) - ON CONFLICT(provider, message_id) DO UPDATE SET - session_id = excluded.session_id, - role = excluded.role, - ordinal = excluded.ordinal, - timestamp = excluded.timestamp, - content = excluded.content, - content_hash = excluded.content_hash, - storage_kind = excluded.storage_kind, - payload_ref = excluded.payload_ref, - snippet_text = excluded.snippet_text, - index_text = excluded.index_text, - legacy_source = 0, - legacy_truncated = 0, - metadata_json = excluded.metadata_json - WHERE lcm_raw_messages.session_id = excluded.session_id", - params![ - message.provider.as_str(), - message.message_id.as_str(), - message.session_id.as_str(), - message.role.as_str(), - message.ordinal, - message.timestamp, - whole_message.payload_ref.content_hash.as_str(), - LcmStorageKind::External.as_str(), - whole_message.payload_ref.payload_ref.as_str(), - whole_message.placeholder.as_str(), - whole_message.placeholder.as_str(), - whole_message.metadata_json.as_str(), - ], - ) - .await?; - if affected != 1 { - return Err(LcmError::SummarySourceNotOwnedBySession); - } + upsert_owned_raw_message( + conn, + message, + None, + whole_message.payload_ref.content_hash.as_str(), + LcmStorageKind::External, + Some(whole_message.payload_ref.payload_ref.as_str()), + whole_message.placeholder.as_str(), + whole_message.placeholder.as_str(), + Some(whole_message.metadata_json.as_str()), + ) + .await?; persist_raw_predecessor_range(conn, message).await?; Ok(RawMessageUpsert { projection_text: whole_message.placeholder, @@ -1144,14 +1140,7 @@ fn externalize_spans( } fn ingest_payload_placeholder(payload_ref: &LcmPayloadRef, field_path: &str) -> String { - format!( - "[Externalized LCM ingest payload: kind={}; field={}; chars={}; bytes={}; ref={}]", - safe_placeholder_metadata(&payload_ref.kind), - safe_placeholder_metadata(field_path), - payload_ref.char_count, - payload_ref.byte_count, - payload_ref.payload_ref - ) + externalized_payload_placeholder(payload_ref, field_path, None) } fn safe_placeholder_metadata(value: &str) -> String { From 03cb090954f9d4a8bba698a2015918aa12e9aa0a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:36:11 +0000 Subject: [PATCH 036/182] simplify(pass-3/5): share invalid-request problems The repeated Never/CorrectRequest constructor is ApplicationProblem::invalid_request. Local two-argument copies are gone. Co-authored-by: Zack Jackson --- .../execution_topology_metrics/rollup_read.rs | 10 ++++---- .../src/execution_topology_metrics/support.rs | 21 ++++------------- .../src/result/problem.rs | 13 +++++++++++ .../src/work_artifact_hydration.rs | 15 ++---------- .../tracedecay-contracts/src/work_attempt.rs | 8 +++---- .../src/work_attempt/capacity.rs | 4 ++-- .../src/work_attempt/problem.rs | 13 +---------- .../src/work_duplicate_adjudication.rs | 14 ++++------- .../src/work_leak_adjudication.rs | 12 ++++------ crates/tracedecay-contracts/src/work_retry.rs | 21 +++++++---------- .../src/work_synthesis.rs | 23 +++++-------------- 11 files changed, 54 insertions(+), 100 deletions(-) diff --git a/crates/tracedecay-contracts/src/execution_topology_metrics/rollup_read.rs b/crates/tracedecay-contracts/src/execution_topology_metrics/rollup_read.rs index e4f0e1335d..39e8350452 100644 --- a/crates/tracedecay-contracts/src/execution_topology_metrics/rollup_read.rs +++ b/crates/tracedecay-contracts/src/execution_topology_metrics/rollup_read.rs @@ -20,7 +20,7 @@ use super::rollup::{ canonical_execution_topology_rollup_fragment_bytes, project_execution_topology_fragments_with_boundaries, }; -use super::support::{invalid_problem, unavailable_model, unavailable_model_with_state_at}; +use super::support::{unavailable_model, unavailable_model_with_state_at}; use super::{ EXECUTION_TOPOLOGY_CAPABILITY_ID_V1, EXECUTION_TOPOLOGY_EVENT_KINDS_V1, EXECUTION_TOPOLOGY_USE_CASE_ID_V1, ExecutionMetricUnavailableV1, @@ -227,13 +227,13 @@ where fn validate_request(request: &ExecutionTopologyMetricsRequestV1) -> Result<(), ApplicationProblem> { if request.horizon.until_micros <= request.horizon.since_micros { - return Err(invalid_problem( + return Err(ApplicationProblem::invalid_request( "application.execution-topology-rollup.invalid-horizon", "The execution topology metrics horizon must end after it starts.", )); } if request.max_events == 0 || request.max_events > MAX_EXECUTION_TOPOLOGY_EVENTS_V1 { - return Err(invalid_problem( + return Err(ApplicationProblem::invalid_request( "application.execution-topology-rollup.invalid-event-budget", "The execution topology metrics event budget must be between 1 and 10000.", )); @@ -243,13 +243,13 @@ fn validate_request(request: &ExecutionTopologyMetricsRequestV1) -> Result<(), A fn authorize(context: &RequestContext) -> Result<(), ApplicationProblem> { let capability = CapabilityId::new(EXECUTION_TOPOLOGY_CAPABILITY_ID_V1).map_err(|_| { - invalid_problem( + ApplicationProblem::invalid_request( "application.execution-topology-rollup.invalid-authority", "The execution topology metrics authority is unavailable.", ) })?; let use_case = UseCaseId::new(EXECUTION_TOPOLOGY_USE_CASE_ID_V1).map_err(|_| { - invalid_problem( + ApplicationProblem::invalid_request( "application.execution-topology-rollup.invalid-authority", "The execution topology metrics authority is unavailable.", ) diff --git a/crates/tracedecay-contracts/src/execution_topology_metrics/support.rs b/crates/tracedecay-contracts/src/execution_topology_metrics/support.rs index c2c04c3f90..e627073d12 100644 --- a/crates/tracedecay-contracts/src/execution_topology_metrics/support.rs +++ b/crates/tracedecay-contracts/src/execution_topology_metrics/support.rs @@ -1,11 +1,5 @@ use tracedecay_domain::CoverageStateV1; -use crate::observability::{ - MetricCohortV1, MetricCoverageV1, MetricEvidenceClassV1, MetricProvenanceV1, MetricSourceV1, - MetricTemporalV1, MetricUncertaintyV1, MetricValueV1, ObservabilityHorizonV1, -}; -use crate::{ApplicationProblem, LegalAction, RetryDirective, SafeDiagnostic}; - use super::projection::ProjectionContext; use super::{ CONFLICT_MIN_ADJUDICATED_CASES_V1, EXECUTION_TOPOLOGY_DESCRIPTOR_REVISION_V1, @@ -14,6 +8,10 @@ use super::{ MAX_CENSORING_RATIO_V1, MAX_METRIC_DIMENSIONS_V1, MIN_COVERAGE_RATIO_V1, RATE_MIN_ELIGIBLE_CASES_V1, }; +use crate::observability::{ + MetricCohortV1, MetricCoverageV1, MetricEvidenceClassV1, MetricProvenanceV1, MetricSourceV1, + MetricTemporalV1, MetricUncertaintyV1, MetricValueV1, ObservabilityHorizonV1, +}; const SOURCE_REVISION_V1: &str = "observability-envelope.v1"; @@ -493,17 +491,6 @@ fn span(start: i64, end: i64) -> u64 { end.abs_diff(start) } -pub(super) fn invalid_problem(code: &str, message: &str) -> ApplicationProblem { - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { - code: code.to_owned(), - message: message.to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::CorrectRequest], - } -} - #[cfg(test)] #[path = "support_descriptor_tests.rs"] mod descriptor_tests; diff --git a/crates/tracedecay-contracts/src/result/problem.rs b/crates/tracedecay-contracts/src/result/problem.rs index 0df5d4dae3..f9e19a0dc9 100644 --- a/crates/tracedecay-contracts/src/result/problem.rs +++ b/crates/tracedecay-contracts/src/result/problem.rs @@ -729,6 +729,19 @@ impl ApplicationProblem { } } + /// Invalid request that must not be retried unchanged. The only legal + /// action is `CorrectRequest`. + pub fn invalid_request(code: impl Into, message: impl Into) -> Self { + Self::InvalidRequest { + diagnostic: SafeDiagnostic { + code: code.into(), + message: message.into(), + }, + retry: RetryDirective::Never, + legal_actions: vec![LegalAction::CorrectRequest], + } + } + pub fn cancelled(stage: CancellationStage) -> Result { let problem = Self::Cancelled { stage, diff --git a/crates/tracedecay-contracts/src/work_artifact_hydration.rs b/crates/tracedecay-contracts/src/work_artifact_hydration.rs index 352345c6f0..cd2ac9dd05 100644 --- a/crates/tracedecay-contracts/src/work_artifact_hydration.rs +++ b/crates/tracedecay-contracts/src/work_artifact_hydration.rs @@ -22,7 +22,7 @@ use crate::work_attempt::{ WorkAttemptListCursorV1, WorkAttemptStorageError, WorkAttemptTopologyBindingV1, WorkAttemptTopologyStateV1, }; -use crate::{ApplicationProblem, LegalAction, RequestContext, RetryDirective, SafeDiagnostic}; +use crate::{ApplicationProblem, RequestContext, RetryDirective, SafeDiagnostic}; /// One page of attempt rows joined with their sealed evidence records, in the /// same stable task/run/attempt identity order as the attempt list, read @@ -149,7 +149,7 @@ where topology: impl FnOnce(&WorkAuthority) -> Result, ) -> Result { if request.page_size == 0 || request.page_size > MAX_WORK_ATTEMPT_LIST_PAGE_SIZE { - return Err(invalid_problem( + return Err(ApplicationProblem::invalid_request( "application.work-artifact-hydration.invalid-page-size", "The Work artifact hydration page size must be between 1 and 1000.", )); @@ -279,14 +279,3 @@ fn page_contract_problem() -> ApplicationProblem { message: "The Work attempt storage returned an inconsistent hydration page.".to_owned(), }) } - -fn invalid_problem(code: &str, message: &str) -> ApplicationProblem { - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { - code: code.to_owned(), - message: message.to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::CorrectRequest], - } -} diff --git a/crates/tracedecay-contracts/src/work_attempt.rs b/crates/tracedecay-contracts/src/work_attempt.rs index e93fe37802..250252d116 100644 --- a/crates/tracedecay-contracts/src/work_attempt.rs +++ b/crates/tracedecay-contracts/src/work_attempt.rs @@ -34,8 +34,8 @@ pub use capacity::{ WorkAttemptCapacityVerdictV1, }; use problem::{ - contract_problem, denied_problem, invalid_problem, list_page_contract_problem, - not_found_problem, stale_cursor_problem, storage_problem, + contract_problem, denied_problem, list_page_contract_problem, not_found_problem, + stale_cursor_problem, storage_problem, }; pub use product_admission::WorkProductAttemptServiceV1; pub(crate) use product_admission::{ @@ -299,7 +299,7 @@ pub struct WorkAttemptEvidenceRecordV1 { impl WorkAttemptEvidenceRecordV1 { pub fn digest(&self) -> Result { canonical_sha256(&(WORK_ATTEMPT_EVIDENCE_DOMAIN, self)).map_err(|_| { - invalid_problem( + ApplicationProblem::invalid_request( "application.work-attempt.invalid-evidence", "The Work attempt evidence record could not be canonicalized.", ) @@ -491,7 +491,7 @@ where topology: impl FnOnce() -> Result, ) -> Result { if request.page_size == 0 || request.page_size > MAX_WORK_ATTEMPT_LIST_PAGE_SIZE { - return Err(invalid_problem( + return Err(ApplicationProblem::invalid_request( "application.work-attempt.invalid-page-size", "The Work attempt list page size must be between 1 and 1000.", )); diff --git a/crates/tracedecay-contracts/src/work_attempt/capacity.rs b/crates/tracedecay-contracts/src/work_attempt/capacity.rs index 7024b32f93..9336a96993 100644 --- a/crates/tracedecay-contracts/src/work_attempt/capacity.rs +++ b/crates/tracedecay-contracts/src/work_attempt/capacity.rs @@ -7,7 +7,7 @@ use tracedecay_domain::{TaskId, WorkTopologyPolicyV1, configuration::TopologyCon use crate::work::work_authority; use crate::{ApplicationProblem, RequestContext}; -use super::{WorkAttemptService, WorkAttemptStoragePort, invalid_problem, storage_problem}; +use super::{WorkAttemptService, WorkAttemptStoragePort, storage_problem}; /// Maximum prospective task identities in one exact capacity census. pub const MAX_WORK_ATTEMPT_CAPACITY_TASKS: usize = u16::MAX as usize; @@ -151,7 +151,7 @@ where } fn capacity_query_problem() -> ApplicationProblem { - invalid_problem( + ApplicationProblem::invalid_request( "application.work-attempt.invalid-capacity-query", "Capacity task identities must be strictly sorted, unique, and within the batch bound.", ) diff --git a/crates/tracedecay-contracts/src/work_attempt/problem.rs b/crates/tracedecay-contracts/src/work_attempt/problem.rs index 7785d3dc33..070627d69a 100644 --- a/crates/tracedecay-contracts/src/work_attempt/problem.rs +++ b/crates/tracedecay-contracts/src/work_attempt/problem.rs @@ -40,7 +40,7 @@ pub(super) fn storage_problem(error: WorkAttemptStorageError) -> ApplicationProb } pub(super) fn contract_problem(_error: WorkRuntimeContractError) -> ApplicationProblem { - invalid_problem( + ApplicationProblem::invalid_request( "application.work-attempt.invalid-transition", "The Work attempt command or stored state is invalid.", ) @@ -75,14 +75,3 @@ pub(super) fn denied_problem(code: &str, message: &str) -> ApplicationProblem { legal_actions: vec![LegalAction::Refresh], } } - -pub(super) fn invalid_problem(code: &str, message: &str) -> ApplicationProblem { - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { - code: code.to_owned(), - message: message.to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::CorrectRequest], - } -} diff --git a/crates/tracedecay-contracts/src/work_duplicate_adjudication.rs b/crates/tracedecay-contracts/src/work_duplicate_adjudication.rs index 2f9b3fdeb4..7689d28de9 100644 --- a/crates/tracedecay-contracts/src/work_duplicate_adjudication.rs +++ b/crates/tracedecay-contracts/src/work_duplicate_adjudication.rs @@ -14,7 +14,7 @@ use tracedecay_domain::{ }; use crate::work::work_authority; -use crate::{ApplicationProblem, LegalAction, RequestContext, RetryDirective, SafeDiagnostic}; +use crate::{ApplicationProblem, RequestContext, RetryDirective, SafeDiagnostic}; pub fn work_duplicate_adjudication_input_digest( command: &WorkDuplicateAdjudicationCommandV1, @@ -400,14 +400,10 @@ fn classify_complete_attempt_relations( } fn invalid_problem() -> ApplicationProblem { - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { - code: "application.work.duplicate-adjudication.invalid".to_owned(), - message: "The duplicate Work adjudication is invalid.".to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::CorrectRequest], - } + ApplicationProblem::invalid_request( + "application.work.duplicate-adjudication.invalid", + "The duplicate Work adjudication is invalid.", + ) } fn storage_problem(error: WorkDuplicateAdjudicationStorageErrorV1) -> ApplicationProblem { diff --git a/crates/tracedecay-contracts/src/work_leak_adjudication.rs b/crates/tracedecay-contracts/src/work_leak_adjudication.rs index c34b222c42..6a0a7525b0 100644 --- a/crates/tracedecay-contracts/src/work_leak_adjudication.rs +++ b/crates/tracedecay-contracts/src/work_leak_adjudication.rs @@ -335,14 +335,10 @@ fn canonical_label(value: &str, maximum: usize) -> bool { } fn invalid_problem() -> ApplicationProblem { - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { - code: "application.work-leak.invalid".to_owned(), - message: "The Work leak adjudication request is invalid.".to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::CorrectRequest], - } + ApplicationProblem::invalid_request( + "application.work-leak.invalid", + "The Work leak adjudication request is invalid.", + ) } fn evidence_problem(error: WorkLeakEvidenceErrorV1) -> ApplicationProblem { diff --git a/crates/tracedecay-contracts/src/work_retry.rs b/crates/tracedecay-contracts/src/work_retry.rs index 8be68031f0..fcaaf8e954 100644 --- a/crates/tracedecay-contracts/src/work_retry.rs +++ b/crates/tracedecay-contracts/src/work_retry.rs @@ -26,11 +26,10 @@ use crate::work_attempt_effect::{ WorkAttemptEffectResolutionV1, WorkAttemptEffectStorageErrorV1, WorkAttemptEffectStoragePortV1, }; use crate::{ - ApplicationContractError, ApplicationProblem, LegalAction, RequestContext, RetryDirective, - SafeDiagnostic, WorkGraphReadPortV1, WorkProductAttemptAdmissionPortV1, - WorkProductAttemptAdmissionV1, WorkProductBindingV1, WorkProductOwnerAuthorizationPortV1, - WorkProductRetryAdmissionV1, WorkProductRevisionPinsV1, WorkflowFanOutAttemptBindingV1, - WorkflowRunAppendRequest, + ApplicationContractError, ApplicationProblem, RequestContext, RetryDirective, SafeDiagnostic, + WorkGraphReadPortV1, WorkProductAttemptAdmissionPortV1, WorkProductAttemptAdmissionV1, + WorkProductBindingV1, WorkProductOwnerAuthorizationPortV1, WorkProductRetryAdmissionV1, + WorkProductRevisionPinsV1, WorkflowFanOutAttemptBindingV1, WorkflowRunAppendRequest, }; const RETRY_INPUT_DIGEST_DOMAIN: &str = "tracedecay.application.work-retry-input.v1"; @@ -790,14 +789,10 @@ fn validate_failure( } fn invalid_problem() -> ApplicationProblem { - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { - code: "application.work-retry.invalid".to_owned(), - message: "The Work retry command is invalid.".to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::CorrectRequest], - } + ApplicationProblem::invalid_request( + "application.work-retry.invalid", + "The Work retry command is invalid.", + ) } fn retry_receipt_problem(_error: ApplicationContractError) -> ApplicationProblem { diff --git a/crates/tracedecay-contracts/src/work_synthesis.rs b/crates/tracedecay-contracts/src/work_synthesis.rs index dfc9f2bf64..c848f38257 100644 --- a/crates/tracedecay-contracts/src/work_synthesis.rs +++ b/crates/tracedecay-contracts/src/work_synthesis.rs @@ -33,9 +33,9 @@ use crate::work_attempt::{ }; use crate::workflow_synthesis::WorkflowSynthesisDraft; use crate::{ - ApplicationProblem, LegalAction, RequestContext, RetryDirective, SafeDiagnostic, - WorkGraphReadPortV1, WorkProductAttemptAdmissionPortV1, WorkProductBindingV1, - WorkProductOwnerAuthorizationPortV1, WorkProductRevisionPinsV1, + ApplicationProblem, RequestContext, SafeDiagnostic, WorkGraphReadPortV1, + WorkProductAttemptAdmissionPortV1, WorkProductBindingV1, WorkProductOwnerAuthorizationPortV1, + WorkProductRevisionPinsV1, }; const WORK_SYNTHESIS_SOURCE_SET_DOMAIN: &str = @@ -218,7 +218,7 @@ where registered_topology, )?; if command.sources.is_empty() { - return Err(invalid_problem( + return Err(ApplicationProblem::invalid_request( "application.work-synthesis.no-sources", "A synthesis attempt must name at least one source attempt.", )); @@ -226,7 +226,7 @@ where let mut seen = BTreeSet::new(); for source in &command.sources { if !seen.insert(source.clone()) { - return Err(invalid_problem( + return Err(ApplicationProblem::invalid_request( "application.work-synthesis.duplicate-source", "A synthesis source attempt was named more than once.", )); @@ -235,7 +235,7 @@ where && source.run_id() == &command.start.run_id && source.attempt_id() == &command.start.attempt_id { - return Err(invalid_problem( + return Err(ApplicationProblem::invalid_request( "application.work-synthesis.self-citation", "A synthesis attempt cannot name itself as a source.", )); @@ -394,17 +394,6 @@ fn evidence_groups(sources: &[WorkSynthesisSourceEnvelopeV1]) -> Vec ApplicationProblem { - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { - code: code.to_owned(), - message: message.to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::CorrectRequest], - } -} - fn contract_problem() -> ApplicationProblem { ApplicationProblem::unavailable(SafeDiagnostic { code: "application.work-synthesis.evidence-inconsistent".to_owned(), From e015f5d782f4247b337746b82469c5531e78142d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:36:47 +0000 Subject: [PATCH 037/182] simplify(pass-4/5): decode convergence rows once Co-authored-by: Zack Jackson --- crates/tracedecay-lcm/src/query.rs | 35 +++++++------------ .../tracedecay-lcm/src/summary_convergence.rs | 26 +++++--------- 2 files changed, 20 insertions(+), 41 deletions(-) diff --git a/crates/tracedecay-lcm/src/query.rs b/crates/tracedecay-lcm/src/query.rs index 59f2914c72..59815f9c6f 100644 --- a/crates/tracedecay-lcm/src/query.rs +++ b/crates/tracedecay-lcm/src/query.rs @@ -993,30 +993,19 @@ fn grep_order_by( recency_column: &str, role_penalty_expr: Option<&str>, ) -> String { - match sort { - LcmGrepSort::Relevance => match role_penalty_expr { - Some(role_penalty_expr) => { - format!("rank ASC, {role_penalty_expr} ASC, {recency_column} DESC") - } - None => format!("rank ASC, {recency_column} DESC"), - }, - LcmGrepSort::Hybrid => { - let blended = format!( + let (leading, trailing) = match sort { + LcmGrepSort::Relevance => ("rank ASC".to_string(), format!("{recency_column} DESC")), + LcmGrepSort::Hybrid => ( + format!( "(rank / (1 + (MAX(0.0, ((strftime('%s','now') - {recency_column}) / 3600.0)) * {AGE_DECAY_RATE})))" - ); - match role_penalty_expr { - Some(role_penalty_expr) => { - format!("{blended} ASC, {role_penalty_expr} ASC, {recency_column} DESC") - } - None => format!("{blended} ASC, {recency_column} DESC"), - } - } - LcmGrepSort::Recency => match role_penalty_expr { - Some(role_penalty_expr) => { - format!("{recency_column} DESC, {role_penalty_expr} ASC, rank ASC") - } - None => format!("{recency_column} DESC, rank ASC"), - }, + ), + format!("{recency_column} DESC"), + ), + LcmGrepSort::Recency => (format!("{recency_column} DESC"), "rank ASC".to_string()), + }; + match role_penalty_expr { + Some(penalty) => format!("{leading}, {penalty} ASC, {trailing}"), + None => format!("{leading}, {trailing}"), } } diff --git a/crates/tracedecay-lcm/src/summary_convergence.rs b/crates/tracedecay-lcm/src/summary_convergence.rs index 508ec574e9..fd524b9531 100644 --- a/crates/tracedecay-lcm/src/summary_convergence.rs +++ b/crates/tracedecay-lcm/src/summary_convergence.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use std::collections::hash_map::Entry; -use tracedecay_runtime_core::db::engine::{Executor, QueryExecutor, params}; +use tracedecay_runtime_core::db::engine::{Executor, QueryExecutor, Row, params}; use crate::{LcmCompressionResponse, LcmError, schema}; @@ -682,21 +682,7 @@ pub async fn next_candidate( let Some(row) = rows.next().await? else { return Ok(None); }; - let failure_count = u32::try_from(row.get::(5)?).map_err(|error| { - LcmError::Db(format!( - "invalid LCM summary convergence failure count: {error}" - )) - })?; - Ok(Some(LcmSummaryConvergenceCandidate { - provider: row.get(0)?, - session_id: row.get(1)?, - newest_raw_store_id: row.get(2)?, - protection_frontier_store_id: row.get(3)?, - attempted_raw_store_id: row.get(4)?, - failure_count, - raw_revision_generation: row.get(6)?, - stale_from_store_id: row.get(7)?, - })) + Ok(Some(convergence_candidate_from_row(&row)?)) } pub async fn candidate_for_session( @@ -719,12 +705,16 @@ pub async fn candidate_for_session( let Some(row) = rows.next().await? else { return Ok(None); }; + Ok(Some(convergence_candidate_from_row(&row)?)) +} + +fn convergence_candidate_from_row(row: &Row) -> Result { let failure_count = u32::try_from(row.get::(5)?).map_err(|error| { LcmError::Db(format!( "invalid LCM summary convergence failure count: {error}" )) })?; - Ok(Some(LcmSummaryConvergenceCandidate { + Ok(LcmSummaryConvergenceCandidate { provider: row.get(0)?, session_id: row.get(1)?, newest_raw_store_id: row.get(2)?, @@ -733,7 +723,7 @@ pub async fn candidate_for_session( failure_count, raw_revision_generation: row.get(6)?, stale_from_store_id: row.get(7)?, - })) + }) } pub async fn record_current_protection_progress( From de2ef87fc043a4709b3e8d976ac62c58200e461c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:37:09 +0000 Subject: [PATCH 038/182] simplify(pass-5/5): share sweep clock and attempt counters The catalog sweep duplicated its timestamp helper, both feedback journeys decoded JSON stdout the same way, and the MCP smoke fake counted retries twice. Callers keep their own verdicts. Co-authored-by: Zack Jackson --- tests/mcp_conformance_smoke_test.sh | 25 ++++++++++--------- tests/tool_sweep_suite/feedback_journey.py | 7 ++---- .../tool_sweep_suite/feedback_read_journey.py | 7 ++---- tests/tool_sweep_suite/orchestrator.py | 12 +++------ tests/tool_sweep_suite/outcomes.py | 12 +++++++++ tests/tool_sweep_suite/runner.py | 10 +++----- 6 files changed, 36 insertions(+), 37 deletions(-) diff --git a/tests/mcp_conformance_smoke_test.sh b/tests/mcp_conformance_smoke_test.sh index 581771c500..df766fc4ee 100755 --- a/tests/mcp_conformance_smoke_test.sh +++ b/tests/mcp_conformance_smoke_test.sh @@ -42,6 +42,17 @@ while (($# > 0)); do esac done +bump_attempts() { + local file=$1 + local attempts=0 + if [[ -f $file ]]; then + attempts=$(<"$file") + fi + attempts=$((attempts + 1)) + printf '%s\n' "$attempts" >"$file" + printf '%s\n' "$attempts" +} + case "$method:$tool" in tools/list:) printf '%s\n' '{"tools":[{"name":"tracedecay_search","inputSchema":{"type":"object"}},{"name":"tracedecay_diagnostics","inputSchema":{"type":"object"}},{"name":"tracedecay_impact","inputSchema":{"type":"object"}},{"name":"tracedecay_affected","inputSchema":{"type":"object"}},{"name":"tracedecay_test_map","inputSchema":{"type":"object"}},{"name":"tracedecay_find_exact_symbol","inputSchema":{"type":"object"}}]}' @@ -56,12 +67,7 @@ case "$method:$tool" in printf '%s\n' '{"content":[{"type":"text","text":"typed evidence"}]}' ;; tools/call:tracedecay_test_map) - attempts=0 - if [[ -f "$FAKE_TEST_MAP_ATTEMPTS" ]]; then - attempts=$(<"$FAKE_TEST_MAP_ATTEMPTS") - fi - attempts=$((attempts + 1)) - printf '%s\n' "$attempts" >"$FAKE_TEST_MAP_ATTEMPTS" + attempts=$(bump_attempts "$FAKE_TEST_MAP_ATTEMPTS") if [[ ${FAKE_TEST_MAP_TERMINAL:-0} == 1 ]]; then echo "Failed to call tool tracedecay_test_map: MCP error -32602: tool project route failed: reason_code=code-graph-invalid-request retryable=false: invalid test-map arguments" >&2 exit 1 @@ -73,12 +79,7 @@ case "$method:$tool" in printf '%s\n' '{"content":[{"type":"text","text":"typed evidence"}]}' ;; tools/call:tracedecay_impact) - attempts=0 - if [[ -f "$FAKE_IMPACT_ATTEMPTS" ]]; then - attempts=$(<"$FAKE_IMPACT_ATTEMPTS") - fi - attempts=$((attempts + 1)) - printf '%s\n' "$attempts" >"$FAKE_IMPACT_ATTEMPTS" + attempts=$(bump_attempts "$FAKE_IMPACT_ATTEMPTS") if ((attempts == 1)); then echo "Failed to call tool tracedecay_impact: MCP error -32603: tool project route failed: reason_code=code-graph-stale retryable=true: the exact graph generation is changing" >&2 exit 1 diff --git a/tests/tool_sweep_suite/feedback_journey.py b/tests/tool_sweep_suite/feedback_journey.py index e4b0f92287..6849438ad4 100644 --- a/tests/tool_sweep_suite/feedback_journey.py +++ b/tests/tool_sweep_suite/feedback_journey.py @@ -19,7 +19,7 @@ from orchestrator import _phase_environment, run_bounded_command from runner import McpClient -from outcomes import objects, response_problem_code +from outcomes import decode_json_stdout, objects, response_problem_code RUNTIME = Path.cwd() PROJECT = RUNTIME / "project" @@ -37,10 +37,7 @@ def write_json(name: str, value: object) -> None: def command(args: list[str], *, timeout: int = 120, cwd: Path | None = None) -> dict[str, object]: started = time.monotonic() run = subprocess.run(args, cwd=cwd or PROJECT, text=True, capture_output=True, timeout=timeout) - try: - stdout: object = json.loads(run.stdout) - except json.JSONDecodeError: - stdout = run.stdout + stdout: object = decode_json_stdout(run.stdout) return {"command": args, "exit_code": run.returncode, "elapsed_ms": round((time.monotonic() - started) * 1000), "stdout": stdout, "stderr": run.stderr} diff --git a/tests/tool_sweep_suite/feedback_read_journey.py b/tests/tool_sweep_suite/feedback_read_journey.py index 4dffc022c8..9fc8759345 100644 --- a/tests/tool_sweep_suite/feedback_read_journey.py +++ b/tests/tool_sweep_suite/feedback_read_journey.py @@ -17,7 +17,7 @@ from journeys import _application_payload from orchestrator import _phase_environment, _terminate -from outcomes import objects +from outcomes import decode_json_stdout, objects from runner import MOUNT_RETRY_BUDGET_S, MOUNT_RETRY_DELAY_S, McpClient MISSING = "feedback_journey_missing_symbol" @@ -30,10 +30,7 @@ def write(out: Path, name: str, value: object) -> None: def command(args: list[str], project: Path, out: Path, name: str) -> dict: result = subprocess.run(args, cwd=project, capture_output=True, text=True, timeout=180) - try: - stdout = json.loads(result.stdout) - except json.JSONDecodeError: - stdout = result.stdout + stdout = decode_json_stdout(result.stdout) row = dict(command=args, exit_code=result.returncode, stdout=stdout, stderr=result.stderr) write(out, name, row) return row diff --git a/tests/tool_sweep_suite/orchestrator.py b/tests/tool_sweep_suite/orchestrator.py index cf951a0a5c..c8252fe5bb 100644 --- a/tests/tool_sweep_suite/orchestrator.py +++ b/tests/tool_sweep_suite/orchestrator.py @@ -5,7 +5,6 @@ import argparse from dataclasses import asdict, dataclass -from datetime import UTC, datetime import json import os from pathlib import Path @@ -24,6 +23,7 @@ if str(SUITE_DIR) not in sys.path: sys.path.insert(0, str(SUITE_DIR)) +from outcomes import utc_now from runner import READ_EFFECTS, SweepError, load_manifest, tool_policy @@ -67,10 +67,6 @@ def expired(self) -> bool: return self.remaining_s() == 0.0 -def _utc_now() -> str: - return datetime.now(UTC).isoformat().replace("+00:00", "Z") - - def catalog_entries(manifest: dict[str, Any]) -> list[dict[str, str]]: """Give every live public item one aggregate identity, without a count target.""" entries: list[dict[str, str]] = [] @@ -466,7 +462,7 @@ def run(args: argparse.Namespace) -> int: """Run discovery first, then isolated mutation journeys, always emitting final artifacts.""" deadline = WholeRunDeadline(args.whole_run_deadline_ms) report: dict[str, Any] = { - "schema_version": 1, "phase": "aggregate", "started_at": _utc_now(), "entries": [], + "schema_version": 1, "phase": "aggregate", "started_at": utc_now(), "entries": [], "summary": {"discovered": 0, "completed": 0, "failed": 0, "cancelled": 0}, } phases: list[PhaseResult] = [] @@ -505,8 +501,8 @@ def run(args: argparse.Namespace) -> int: report["fatal"] = str(error) report["fatal_problem_code"] = "tool_sweep.orchestration_failed" finally: - report["started_at"] = report.get("started_at", _utc_now()) - report["finished_at"] = _utc_now() + report["started_at"] = report.get("started_at", utc_now()) + report["finished_at"] = utc_now() report["phases"] = [phase.value() for phase in phases] write_final_report(args.out, report) return 0 if "fatal" not in report and report["summary"]["failed"] == 0 and report["summary"]["cancelled"] == 0 else 1 diff --git a/tests/tool_sweep_suite/outcomes.py b/tests/tool_sweep_suite/outcomes.py index 42c0aa346b..ed8ad73d9d 100644 --- a/tests/tool_sweep_suite/outcomes.py +++ b/tests/tool_sweep_suite/outcomes.py @@ -4,9 +4,21 @@ import json import re +from datetime import UTC, datetime from typing import Any +def utc_now() -> str: + return datetime.now(UTC).isoformat().replace("+00:00", "Z") + + +def decode_json_stdout(stdout: str) -> Any: + try: + return json.loads(stdout) + except json.JSONDecodeError: + return stdout + + _NOT_FOUND = re.compile( r"\b(?:not found|no (?:matching |such )?(?:symbol|node|file|fact|session|response|result)s?\b)", re.IGNORECASE, diff --git a/tests/tool_sweep_suite/runner.py b/tests/tool_sweep_suite/runner.py index bb811bae15..f0b9a2fb9c 100644 --- a/tests/tool_sweep_suite/runner.py +++ b/tests/tool_sweep_suite/runner.py @@ -5,7 +5,6 @@ import argparse from contextlib import contextmanager -from datetime import UTC, datetime import hashlib import json import os @@ -52,6 +51,7 @@ response_problem_code, response_handle, text_blocks, + utc_now, ) def response_row( @@ -246,10 +246,6 @@ def load_manifest(path: Path) -> dict[str, Any]: return canonical -def _utc_now() -> str: - return datetime.now(UTC).isoformat().replace("+00:00", "Z") - - class McpClient: """A bounded stdio MCP client backed by the release binary under test.""" @@ -2863,7 +2859,7 @@ def run_phase(args: argparse.Namespace) -> int: report: dict[str, Any] = { "schema_version": 1, "phase": args.phase, - "started_at": _utc_now(), + "started_at": utc_now(), "entries": [], "summary": {"discovered": 0, "completed": 0, "failed": 0, "cancelled": 0}, } @@ -2956,7 +2952,7 @@ def run_phase(args: argparse.Namespace) -> int: client.close() report["entries"] = sorted(report["entries"], key=lambda row: (row["kind"], row["name"])) report["summary"] = _phase_summary(report["entries"]) - report["finished_at"] = _utc_now() + report["finished_at"] = utc_now() _write_phase_report(args.out, report) return 0 if "fatal" not in report and report["summary"]["failed"] == 0 else 1 From ccae226738cae79749d198b807b3dcc39d1b1384 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:37:32 +0000 Subject: [PATCH 039/182] simplify(pass-4/5): collapse observation structure counting Scalar and compound serde cases share one counter instead of repeating the same value and depth accounting. Co-authored-by: Zack Jackson --- crates/tracedecay-domain/src/observation.rs | 295 ++++++-------------- 1 file changed, 90 insertions(+), 205 deletions(-) diff --git a/crates/tracedecay-domain/src/observation.rs b/crates/tracedecay-domain/src/observation.rs index 954855587e..ba874b71ed 100644 --- a/crates/tracedecay-domain/src/observation.rs +++ b/crates/tracedecay-domain/src/observation.rs @@ -1100,6 +1100,36 @@ struct StructureLimitSerializer<'count> { depth: usize, } +impl<'count> StructureLimitSerializer<'count> { + fn count_node(self) -> Result<(), StructureLimitError> { + visit_structure_value(self.values, self.depth) + } + + fn descend( + self, + extra_ancestor: bool, + ) -> Result, StructureLimitError> { + visit_structure_value(self.values, self.depth)?; + if extra_ancestor { + visit_structure_value(self.values, self.depth + 1)?; + } + Ok(StructureLimitCompound { + values: self.values, + child_depth: self.depth + 1 + usize::from(extra_ancestor), + }) + } +} + +macro_rules! count_scalar { + ($($name:ident ( $($params:tt)* );)*) => { + $( + fn $name(self, $($params)*) -> Result<(), StructureLimitError> { + self.count_node() + } + )* + }; +} + impl<'count> serde::Serializer for StructureLimitSerializer<'count> { type Ok = (); type Error = StructureLimitError; @@ -1112,64 +1142,30 @@ impl<'count> serde::Serializer for StructureLimitSerializer<'count> { type SerializeStruct = StructureLimitCompound<'count>; type SerializeStructVariant = StructureLimitCompound<'count>; - fn serialize_bool(self, _value: bool) -> Result<(), StructureLimitError> { - visit_structure_value(self.values, self.depth) - } - - fn serialize_i8(self, _value: i8) -> Result<(), StructureLimitError> { - visit_structure_value(self.values, self.depth) - } - - fn serialize_i16(self, _value: i16) -> Result<(), StructureLimitError> { - visit_structure_value(self.values, self.depth) - } - - fn serialize_i32(self, _value: i32) -> Result<(), StructureLimitError> { - visit_structure_value(self.values, self.depth) - } - - fn serialize_i64(self, _value: i64) -> Result<(), StructureLimitError> { - visit_structure_value(self.values, self.depth) - } - - fn serialize_i128(self, _value: i128) -> Result<(), StructureLimitError> { - visit_structure_value(self.values, self.depth) - } - - fn serialize_u8(self, _value: u8) -> Result<(), StructureLimitError> { - visit_structure_value(self.values, self.depth) - } - - fn serialize_u16(self, _value: u16) -> Result<(), StructureLimitError> { - visit_structure_value(self.values, self.depth) - } - - fn serialize_u32(self, _value: u32) -> Result<(), StructureLimitError> { - visit_structure_value(self.values, self.depth) - } - - fn serialize_u64(self, _value: u64) -> Result<(), StructureLimitError> { - visit_structure_value(self.values, self.depth) - } - - fn serialize_u128(self, _value: u128) -> Result<(), StructureLimitError> { - visit_structure_value(self.values, self.depth) - } - - fn serialize_f32(self, _value: f32) -> Result<(), StructureLimitError> { - visit_structure_value(self.values, self.depth) - } - - fn serialize_f64(self, _value: f64) -> Result<(), StructureLimitError> { - visit_structure_value(self.values, self.depth) - } - - fn serialize_char(self, _value: char) -> Result<(), StructureLimitError> { - visit_structure_value(self.values, self.depth) - } - - fn serialize_str(self, _value: &str) -> Result<(), StructureLimitError> { - visit_structure_value(self.values, self.depth) + count_scalar! { + serialize_bool(_value: bool); + serialize_i8(_value: i8); + serialize_i16(_value: i16); + serialize_i32(_value: i32); + serialize_i64(_value: i64); + serialize_i128(_value: i128); + serialize_u8(_value: u8); + serialize_u16(_value: u16); + serialize_u32(_value: u32); + serialize_u64(_value: u64); + serialize_u128(_value: u128); + serialize_f32(_value: f32); + serialize_f64(_value: f64); + serialize_char(_value: char); + serialize_str(_value: &str); + serialize_none(); + serialize_unit(); + serialize_unit_struct(_name: &'static str); + serialize_unit_variant( + _name: &'static str, + _variant_index: u32, + _variant: &'static str + ); } fn serialize_bytes(self, value: &[u8]) -> Result<(), StructureLimitError> { @@ -1181,10 +1177,6 @@ impl<'count> serde::Serializer for StructureLimitSerializer<'count> { Ok(()) } - fn serialize_none(self) -> Result<(), StructureLimitError> { - visit_structure_value(self.values, self.depth) - } - fn serialize_some(self, value: &T) -> Result<(), StructureLimitError> where T: ?Sized + Serialize, @@ -1192,23 +1184,6 @@ impl<'count> serde::Serializer for StructureLimitSerializer<'count> { value.serialize(self) } - fn serialize_unit(self) -> Result<(), StructureLimitError> { - visit_structure_value(self.values, self.depth) - } - - fn serialize_unit_struct(self, _name: &'static str) -> Result<(), StructureLimitError> { - visit_structure_value(self.values, self.depth) - } - - fn serialize_unit_variant( - self, - _name: &'static str, - _variant_index: u32, - _variant: &'static str, - ) -> Result<(), StructureLimitError> { - visit_structure_value(self.values, self.depth) - } - fn serialize_newtype_struct( self, _name: &'static str, @@ -1241,11 +1216,7 @@ impl<'count> serde::Serializer for StructureLimitSerializer<'count> { self, _len: Option, ) -> Result, StructureLimitError> { - visit_structure_value(self.values, self.depth)?; - Ok(StructureLimitCompound { - values: self.values, - child_depth: self.depth + 1, - }) + self.descend(false) } fn serialize_tuple( @@ -1270,23 +1241,14 @@ impl<'count> serde::Serializer for StructureLimitSerializer<'count> { _variant: &'static str, _len: usize, ) -> Result, StructureLimitError> { - visit_structure_value(self.values, self.depth)?; - visit_structure_value(self.values, self.depth + 1)?; - Ok(StructureLimitCompound { - values: self.values, - child_depth: self.depth + 2, - }) + self.descend(true) } fn serialize_map( self, _len: Option, ) -> Result, StructureLimitError> { - visit_structure_value(self.values, self.depth)?; - Ok(StructureLimitCompound { - values: self.values, - child_depth: self.depth + 1, - }) + self.descend(false) } fn serialize_struct( @@ -1294,11 +1256,7 @@ impl<'count> serde::Serializer for StructureLimitSerializer<'count> { _name: &'static str, _len: usize, ) -> Result, StructureLimitError> { - visit_structure_value(self.values, self.depth)?; - Ok(StructureLimitCompound { - values: self.values, - child_depth: self.depth + 1, - }) + self.descend(false) } fn serialize_struct_variant( @@ -1308,19 +1266,14 @@ impl<'count> serde::Serializer for StructureLimitSerializer<'count> { _variant: &'static str, _len: usize, ) -> Result, StructureLimitError> { - visit_structure_value(self.values, self.depth)?; - visit_structure_value(self.values, self.depth + 1)?; - Ok(StructureLimitCompound { - values: self.values, - child_depth: self.depth + 2, - }) + self.descend(true) } fn collect_str(self, _value: &T) -> Result<(), StructureLimitError> where T: ?Sized + fmt::Display, { - visit_structure_value(self.values, self.depth) + self.count_node() } } @@ -1343,69 +1296,30 @@ impl StructureLimitCompound<'_> { } } -impl serde::ser::SerializeSeq for StructureLimitCompound<'_> { - type Ok = (); - type Error = StructureLimitError; - - fn serialize_element(&mut self, value: &T) -> Result<(), StructureLimitError> - where - T: ?Sized + Serialize, - { - self.child(value) - } - - fn end(self) -> Result<(), StructureLimitError> { - Ok(()) - } -} - -impl serde::ser::SerializeTuple for StructureLimitCompound<'_> { - type Ok = (); - type Error = StructureLimitError; +macro_rules! impl_structure_limit_compound { + ($trait:path, $method:ident, ($($extra:tt)*)) => { + impl $trait for StructureLimitCompound<'_> { + type Ok = (); + type Error = StructureLimitError; - fn serialize_element(&mut self, value: &T) -> Result<(), StructureLimitError> - where - T: ?Sized + Serialize, - { - self.child(value) - } + fn $method(&mut self, $($extra)* value: &T) -> Result<(), StructureLimitError> + where + T: ?Sized + Serialize, + { + self.child(value) + } - fn end(self) -> Result<(), StructureLimitError> { - Ok(()) - } + fn end(self) -> Result<(), StructureLimitError> { + Ok(()) + } + } + }; } -impl serde::ser::SerializeTupleStruct for StructureLimitCompound<'_> { - type Ok = (); - type Error = StructureLimitError; - - fn serialize_field(&mut self, value: &T) -> Result<(), StructureLimitError> - where - T: ?Sized + Serialize, - { - self.child(value) - } - - fn end(self) -> Result<(), StructureLimitError> { - Ok(()) - } -} - -impl serde::ser::SerializeTupleVariant for StructureLimitCompound<'_> { - type Ok = (); - type Error = StructureLimitError; - - fn serialize_field(&mut self, value: &T) -> Result<(), StructureLimitError> - where - T: ?Sized + Serialize, - { - self.child(value) - } - - fn end(self) -> Result<(), StructureLimitError> { - Ok(()) - } -} +impl_structure_limit_compound!(serde::ser::SerializeSeq, serialize_element, ()); +impl_structure_limit_compound!(serde::ser::SerializeTuple, serialize_element, ()); +impl_structure_limit_compound!(serde::ser::SerializeTupleStruct, serialize_field, ()); +impl_structure_limit_compound!(serde::ser::SerializeTupleVariant, serialize_field, ()); impl serde::ser::SerializeMap for StructureLimitCompound<'_> { type Ok = (); @@ -1431,45 +1345,16 @@ impl serde::ser::SerializeMap for StructureLimitCompound<'_> { } } -impl serde::ser::SerializeStruct for StructureLimitCompound<'_> { - type Ok = (); - type Error = StructureLimitError; - - fn serialize_field( - &mut self, - _key: &'static str, - value: &T, - ) -> Result<(), StructureLimitError> - where - T: ?Sized + Serialize, - { - self.child(value) - } - - fn end(self) -> Result<(), StructureLimitError> { - Ok(()) - } -} - -impl serde::ser::SerializeStructVariant for StructureLimitCompound<'_> { - type Ok = (); - type Error = StructureLimitError; - - fn serialize_field( - &mut self, - _key: &'static str, - value: &T, - ) -> Result<(), StructureLimitError> - where - T: ?Sized + Serialize, - { - self.child(value) - } - - fn end(self) -> Result<(), StructureLimitError> { - Ok(()) - } -} +impl_structure_limit_compound!( + serde::ser::SerializeStruct, + serialize_field, + (_key: &'static str,) +); +impl_structure_limit_compound!( + serde::ser::SerializeStructVariant, + serialize_field, + (_key: &'static str,) +); struct ByteLimitWriter { written: usize, From 8875cadbb76043eddac5d92d5159ddfece978f98 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:37:56 +0000 Subject: [PATCH 040/182] simplify(pass-2/5): table memory tool schemas Co-authored-by: Zack Jackson --- .../tracedecay-mcp-catalog/src/definitions.rs | 20 +- .../src/definitions/memory.rs | 231 ++++++++---------- 2 files changed, 112 insertions(+), 139 deletions(-) diff --git a/crates/tracedecay-mcp-catalog/src/definitions.rs b/crates/tracedecay-mcp-catalog/src/definitions.rs index 2f83213e38..ba5b2b9993 100644 --- a/crates/tracedecay-mcp-catalog/src/definitions.rs +++ b/crates/tracedecay-mcp-catalog/src/definitions.rs @@ -47,7 +47,6 @@ use git::*; use graph::*; pub use graph::{SEARCH_MAX_LEXICAL_ANCHOR_BYTES, SEARCH_MAX_LEXICAL_ANCHORS}; use lcm::*; -use memory::*; use multi_root::*; use skills::*; use testing::*; @@ -454,20 +453,9 @@ fn build_maximal_tool_definitions() -> Result, McpCatalogErr def_diagnose(), def_derives(), def_run_affected_tests(), - def_fact_store_add(request_schema("fact_store_add")?), - def_fact_store_search(request_schema("fact_store_search")?), - def_fact_store_probe(request_schema("fact_store_probe")?), - def_fact_store_related(request_schema("fact_store_related")?), - def_fact_store_reason(request_schema("fact_store_reason")?), - def_fact_store_contradict(request_schema("fact_store_contradict")?), - def_fact_store_get(request_schema("fact_store_get")?), - def_fact_store_update(request_schema("fact_store_update")?), - def_fact_store_remove(request_schema("fact_store_remove")?), - def_fact_store_supersede(request_schema("fact_store_supersede")?), - def_fact_store_list(request_schema("fact_store_list")?), - def_fact_feedback(request_schema("fact_feedback")?), - def_memory_status(request_schema("memory_status")?), - def_fact_store_curate(request_schema("fact_store_curate")?), + ]; + definitions.extend(memory::memory_definitions(&request_schema)?); + definitions.extend([ def_automation_run_list(), def_automation_run_view(), def_automation_run_artifact_view(), @@ -504,7 +492,7 @@ fn build_maximal_tool_definitions() -> Result, McpCatalogErr def_source_edit_reconcile(), def_source_edit_rollback(), def_find_exact_symbol(), - ]; + ]); definitions.extend(application_definitions()?); let work = work_worker.join().map_err(|_| { McpCatalogError::Initialization( diff --git a/crates/tracedecay-mcp-catalog/src/definitions/memory.rs b/crates/tracedecay-mcp-catalog/src/definitions/memory.rs index aec86d0ab0..66828a7d64 100644 --- a/crates/tracedecay-mcp-catalog/src/definitions/memory.rs +++ b/crates/tracedecay-mcp-catalog/src/definitions/memory.rs @@ -3,130 +3,115 @@ use serde_json::Value; use super::{def, def_rw}; -use crate::ToolDefinition; +use crate::{McpCatalogError, ToolDefinition}; -pub(super) fn def_fact_store_add(input_schema: Value) -> ToolDefinition { - def_rw( - "tracedecay_fact_store_add", - "Fact Store Add", - "Add one holographic memory fact. The result includes a write-time diff report for near duplicates, possible conflicts, and rejected secret-like content. Calibrate trust to the evidence instead of defaulting high.", - input_schema, - ) +struct MemoryTool { + operation: &'static str, + title: &'static str, + description: &'static str, + write: bool, } -pub(super) fn def_fact_store_curate(input_schema: Value) -> ToolDefinition { - def_rw( - "tracedecay_fact_store_curate", - "Fact Store Curate", - "Run the daemon-owned automatic Memory Curator. Callers may bound review size and confidence only; TraceDecay derives the run, operations, validation, policy, and apply authority. Inspect the durable terminal with the read-only automation run tools.", - input_schema, - ) -} - -pub(super) fn def_fact_store_search(input_schema: Value) -> ToolDefinition { - def( - "tracedecay_fact_store_search", - "Fact Store Search", - "Search durable project or user memory facts by text and trust.", - input_schema, - ) -} - -pub(super) fn def_fact_store_probe(input_schema: Value) -> ToolDefinition { - def( - "tracedecay_fact_store_probe", - "Fact Store Probe", - "Find holographic memory facts connected to one entity.", - input_schema, - ) -} - -pub(super) fn def_fact_store_related(input_schema: Value) -> ToolDefinition { - def( - "tracedecay_fact_store_related", - "Fact Store Related", - "List entities related to one entity through holographic memory facts.", - input_schema, - ) -} - -pub(super) fn def_fact_store_reason(input_schema: Value) -> ToolDefinition { - def( - "tracedecay_fact_store_reason", - "Fact Store Reason", - "Reason over holographic memory facts connecting multiple entities.", - input_schema, - ) -} - -pub(super) fn def_fact_store_contradict(input_schema: Value) -> ToolDefinition { - def( - "tracedecay_fact_store_contradict", - "Fact Store Contradict", - "Find potentially contradictory holographic memory facts above an optional threshold.", - input_schema, - ) -} - -pub(super) fn def_fact_store_get(input_schema: Value) -> ToolDefinition { - def( - "tracedecay_fact_store_get", - "Fact Store Get", - "Get one holographic memory fact, including trust history explaining score changes.", - input_schema, - ) -} - -pub(super) fn def_fact_store_update(input_schema: Value) -> ToolDefinition { - def_rw( - "tracedecay_fact_store_update", - "Fact Store Update", - "Update one existing holographic memory fact without changing its identity.", - input_schema, - ) -} - -pub(super) fn def_fact_store_remove(input_schema: Value) -> ToolDefinition { - def_rw( - "tracedecay_fact_store_remove", - "Fact Store Remove", - "Remove one holographic memory fact by exact fact id.", - input_schema, - ) -} - -pub(super) fn def_fact_store_supersede(input_schema: Value) -> ToolDefinition { - def_rw( - "tracedecay_fact_store_supersede", - "Fact Store Supersede", - "Mark one holographic memory fact as superseded by another fact id. The old fact leaves default list/search/probe results but stays readable by id through its history; payload and trust are untouched. Use this when a newer fact corrects an older one instead of removing the old one.", - input_schema, - ) -} - -pub(super) fn def_fact_store_list(input_schema: Value) -> ToolDefinition { - def( - "tracedecay_fact_store_list", - "Fact Store List", - "List holographic memory facts with optional category, trust, and project selectors.", - input_schema, - ) -} - -pub(super) fn def_fact_feedback(input_schema: Value) -> ToolDefinition { - def_rw( - "tracedecay_fact_feedback", - "Fact Feedback", - "Record whether an active-project memory fact materially helped or misled the current work, and adjust its trust score.", - input_schema, - ) -} - -pub(super) fn def_memory_status(input_schema: Value) -> ToolDefinition { - def( - "tracedecay_memory_status", - "Memory Status", - "Inspect canonical memory state: return the owner, fact/entity counts, algebra identity and capacity, trust distribution, below-threshold facts, feedback totals, and retrieval funnel. Defaults to the active project; pass an exact project_selector only when intentionally checking another registered project. Human/operator equivalent: `tracedecay memory status`.", - input_schema, - ) +const MEMORY_TOOLS: &[MemoryTool] = &[ + MemoryTool { + operation: "fact_store_add", + title: "Fact Store Add", + description: "Add one holographic memory fact. The result includes a write-time diff report for near duplicates, possible conflicts, and rejected secret-like content. Calibrate trust to the evidence instead of defaulting high.", + write: true, + }, + MemoryTool { + operation: "fact_store_search", + title: "Fact Store Search", + description: "Search durable project or user memory facts by text and trust.", + write: false, + }, + MemoryTool { + operation: "fact_store_probe", + title: "Fact Store Probe", + description: "Find holographic memory facts connected to one entity.", + write: false, + }, + MemoryTool { + operation: "fact_store_related", + title: "Fact Store Related", + description: "List entities related to one entity through holographic memory facts.", + write: false, + }, + MemoryTool { + operation: "fact_store_reason", + title: "Fact Store Reason", + description: "Reason over holographic memory facts connecting multiple entities.", + write: false, + }, + MemoryTool { + operation: "fact_store_contradict", + title: "Fact Store Contradict", + description: "Find potentially contradictory holographic memory facts above an optional threshold.", + write: false, + }, + MemoryTool { + operation: "fact_store_get", + title: "Fact Store Get", + description: "Get one holographic memory fact, including trust history explaining score changes.", + write: false, + }, + MemoryTool { + operation: "fact_store_update", + title: "Fact Store Update", + description: "Update one existing holographic memory fact without changing its identity.", + write: true, + }, + MemoryTool { + operation: "fact_store_remove", + title: "Fact Store Remove", + description: "Remove one holographic memory fact by exact fact id.", + write: true, + }, + MemoryTool { + operation: "fact_store_supersede", + title: "Fact Store Supersede", + description: "Mark one holographic memory fact as superseded by another fact id. The old fact leaves default list/search/probe results but stays readable by id through its history; payload and trust are untouched. Use this when a newer fact corrects an older one instead of removing the old one.", + write: true, + }, + MemoryTool { + operation: "fact_store_list", + title: "Fact Store List", + description: "List holographic memory facts with optional category, trust, and project selectors.", + write: false, + }, + MemoryTool { + operation: "fact_feedback", + title: "Fact Feedback", + description: "Record whether an active-project memory fact materially helped or misled the current work, and adjust its trust score.", + write: true, + }, + MemoryTool { + operation: "memory_status", + title: "Memory Status", + description: "Inspect canonical memory state: return the owner, fact/entity counts, algebra identity and capacity, trust distribution, below-threshold facts, feedback totals, and retrieval funnel. Defaults to the active project; pass an exact project_selector only when intentionally checking another registered project. Human/operator equivalent: `tracedecay memory status`.", + write: false, + }, + MemoryTool { + operation: "fact_store_curate", + title: "Fact Store Curate", + description: "Run the daemon-owned automatic Memory Curator. Callers may bound review size and confidence only; TraceDecay derives the run, operations, validation, policy, and apply authority. Inspect the durable terminal with the read-only automation run tools.", + write: true, + }, +]; + +pub(super) fn memory_definitions( + mut schema: impl FnMut(&str) -> Result, +) -> Result, McpCatalogError> { + MEMORY_TOOLS + .iter() + .map(|tool| { + let name = format!("tracedecay_{}", tool.operation); + let input_schema = schema(tool.operation)?; + Ok(if tool.write { + def_rw(&name, tool.title, tool.description, input_schema) + } else { + def(&name, tool.title, tool.description, input_schema) + }) + }) + .collect() } From 3419d08f9fcdc07813f65a82e07eafefb6d1711a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:38:41 +0000 Subject: [PATCH 041/182] simplify(pass-3/5): share work and workflow discovery Co-authored-by: Zack Jackson --- .../tracedecay-mcp-catalog/src/definitions.rs | 51 +++++++++++- .../src/definitions/work.rs | 64 ++++++--------- .../src/definitions/workflow.rs | 77 +++++++------------ 3 files changed, 103 insertions(+), 89 deletions(-) diff --git a/crates/tracedecay-mcp-catalog/src/definitions.rs b/crates/tracedecay-mcp-catalog/src/definitions.rs index ba5b2b9993..a1949225f3 100644 --- a/crates/tracedecay-mcp-catalog/src/definitions.rs +++ b/crates/tracedecay-mcp-catalog/src/definitions.rs @@ -12,7 +12,10 @@ use serde_json::{Value, json}; use std::collections::BTreeSet; use std::sync::LazyLock; -use tracedecay_tool_catalog::{ApplicationSurfaceOperation, ScopeDimension}; +use tracedecay_tool_catalog::{ + ApplicationSurfaceOperation, CatalogValidationError, ExecutableBindingRegistryV1, OperationId, + ScopeDimension, +}; use crate::McpCatalogError; use crate::ToolDefinition; @@ -515,6 +518,52 @@ fn build_maximal_tool_definitions() -> Result, McpCatalogErr Ok(definitions) } +pub(super) struct FamilyOperation { + pub operation_id: String, + pub name: String, + pub title: String, + pub description: String, +} + +/// Project one executable registry into MCP tools. Callers own the transport +/// prefix, title, and description; the registry owns the schema and effect. +pub(super) fn project_executable_family( + registry: &ExecutableBindingRegistryV1, + operations: &[FamilyOperation], + incomplete: (&'static str, &'static str), + identity: (&'static str, &'static str), + missing: (&'static str, &'static str), +) -> Result, McpCatalogError> { + if registry.iter().count() != operations.len() { + return Err(catalog_invalid(incomplete.0, incomplete.1)); + } + operations + .iter() + .map(|operation| { + let operation_id = OperationId::new(operation.operation_id.clone()) + .map_err(|_| catalog_invalid(identity.0, identity.1))?; + let binding = registry + .get(&operation_id) + .and_then(|availability| availability.binding()) + .ok_or_else(|| catalog_invalid(missing.0, missing.1))?; + Ok(ToolDefinition { + name: operation.name.clone(), + description: operation.description.clone(), + input_schema: binding.request_schema().body().clone(), + annotations: Some(json!({ + "readOnlyHint": binding.effect().is_read_only(), + "title": operation.title, + })), + meta: None, + }) + }) + .collect() +} + +fn catalog_invalid(field: &'static str, reason: &'static str) -> McpCatalogError { + CatalogValidationError::InvalidValue { field, reason }.into() +} + fn spawn_definition_worker( name: &'static str, worker: impl FnOnce() -> Result, McpCatalogError> + Send + 'static, diff --git a/crates/tracedecay-mcp-catalog/src/definitions/work.rs b/crates/tracedecay-mcp-catalog/src/definitions/work.rs index 5faa25758c..8353679043 100644 --- a/crates/tracedecay-mcp-catalog/src/definitions/work.rs +++ b/crates/tracedecay-mcp-catalog/src/definitions/work.rs @@ -3,10 +3,9 @@ //! The executable owns Work's operation set, request schemas, effects, and //! availability. MCP contributes only its transport prefix and presentation. -use serde_json::json; use tracedecay_api::WorkOperation; -use tracedecay_tool_catalog::{CatalogValidationError, OperationId}; +use super::{FamilyOperation, project_executable_family}; use crate::ToolDefinition; type DiscoveryResult = Result; @@ -21,46 +20,31 @@ type DiscoveryResult = Result; pub(super) fn work_definitions() -> DiscoveryResult> { let registry = tracedecay_contracts::work_executable_binding_registry() .map_err(crate::McpCatalogError::CatalogValidation)?; - if registry.iter().count() != WorkOperation::ALL.len() { - return Err(invalid_work_discovery( - "MCP Work executable registry", - "must expose exactly every canonical Work operation", - )); - } - WorkOperation::ALL + let operations = WorkOperation::ALL .into_iter() - .map(|operation| { - let operation_id = OperationId::new(operation.operation_id()).map_err(|_| { - invalid_work_discovery( - "MCP Work operation identity", - "must name one canonical Work operation", - ) - })?; - let binding = registry - .get(&operation_id) - .and_then(|availability| availability.binding()) - .ok_or_else(|| { - invalid_work_discovery( - "MCP Work executable binding", - "canonical Work operation is not executable", - ) - })?; - Ok(ToolDefinition { - name: format!("tracedecay_work_{}", operation.operation_key()), - description: operation.description().to_owned(), - input_schema: binding.request_schema().body().clone(), - annotations: Some(json!({ - "readOnlyHint": binding.effect().is_read_only(), - "title": format!("Work {}", operation.operation_key()), - })), - meta: None, - }) + .map(|operation| FamilyOperation { + operation_id: operation.operation_id(), + name: format!("tracedecay_work_{}", operation.operation_key()), + title: format!("Work {}", operation.operation_key()), + description: operation.description().to_owned(), }) - .collect() -} - -fn invalid_work_discovery(field: &'static str, reason: &'static str) -> crate::McpCatalogError { - CatalogValidationError::InvalidValue { field, reason }.into() + .collect::>(); + project_executable_family( + registry, + &operations, + ( + "MCP Work executable registry", + "must expose exactly every canonical Work operation", + ), + ( + "MCP Work operation identity", + "must name one canonical Work operation", + ), + ( + "MCP Work executable binding", + "canonical Work operation is not executable", + ), + ) } #[cfg(test)] diff --git a/crates/tracedecay-mcp-catalog/src/definitions/workflow.rs b/crates/tracedecay-mcp-catalog/src/definitions/workflow.rs index 4cd41186a8..54db627769 100644 --- a/crates/tracedecay-mcp-catalog/src/definitions/workflow.rs +++ b/crates/tracedecay-mcp-catalog/src/definitions/workflow.rs @@ -1,14 +1,12 @@ //! MCP discovery projected from the canonical Workflow executable registry. //! //! The executable owns Workflow's operation set, request schemas, effects, and -//! availability. MCP contributes only its transport prefix and presentation. -//! the same division the Work family holds, so the two closed families cannot -//! drift into two different discovery stories. +//! availability. MCP contributes only its transport prefix and presentation, +//! the same division the Work family holds. -use serde_json::json; use tracedecay_api::WorkflowOperation; -use tracedecay_tool_catalog::{CatalogValidationError, OperationId}; +use super::{FamilyOperation, project_executable_family}; use crate::ToolDefinition; type DiscoveryResult = Result; @@ -17,55 +15,38 @@ type DiscoveryResult = Result; /// /// Discovery fails loudly if the registry is incomplete rather than silently /// omitting a callable operation: a Workflow operation that no adapter -/// publishes is invisible to every agent, which is exactly how all sixteen of -/// them stayed off MCP while CLI and HTTP carried them. +/// publishes is invisible to every agent. pub(super) fn workflow_definitions() -> DiscoveryResult> { let registry = tracedecay_contracts::workflow_executable_binding_registry() .map_err(crate::McpCatalogError::CatalogValidation)?; - if registry.iter().count() != WorkflowOperation::ALL.len() { - return Err(invalid_workflow_discovery( - "MCP Workflow executable registry", - "must expose exactly every canonical Workflow operation", - )); - } - WorkflowOperation::ALL + let operations = WorkflowOperation::ALL .into_iter() .map(|operation| { - let operation_id = - OperationId::new(operation.operation_id_str().to_owned()).map_err(|_| { - invalid_workflow_discovery( - "MCP Workflow operation identity", - "must name one canonical Workflow operation", - ) - })?; - let binding = registry - .get(&operation_id) - .and_then(|availability| availability.binding()) - .ok_or_else(|| { - invalid_workflow_discovery( - "MCP Workflow executable binding", - "canonical Workflow operation is not executable", - ) - })?; - Ok(ToolDefinition { - name: format!("tracedecay_workflow_{}", operation.operation_key()), - description: format!( - "Invoke the Workflow {} operation.", - operation.operation_key() - ), - input_schema: binding.request_schema().body().clone(), - annotations: Some(json!({ - "readOnlyHint": binding.effect().is_read_only(), - "title": format!("Workflow {}", operation.operation_key()), - })), - meta: None, - }) + let key = operation.operation_key(); + FamilyOperation { + operation_id: operation.operation_id_str().to_owned(), + name: format!("tracedecay_workflow_{key}"), + title: format!("Workflow {key}"), + description: format!("Invoke the Workflow {key} operation."), + } }) - .collect() -} - -fn invalid_workflow_discovery(field: &'static str, reason: &'static str) -> crate::McpCatalogError { - CatalogValidationError::InvalidValue { field, reason }.into() + .collect::>(); + project_executable_family( + registry, + &operations, + ( + "MCP Workflow executable registry", + "must expose exactly every canonical Workflow operation", + ), + ( + "MCP Workflow operation identity", + "must name one canonical Workflow operation", + ), + ( + "MCP Workflow executable binding", + "canonical Workflow operation is not executable", + ), + ) } #[cfg(test)] From 6affb7a72fe56cddbb8b279dafce7184f8dea004 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:39:45 +0000 Subject: [PATCH 042/182] simplify(pass-4/5): walk the production feature graph once Co-authored-by: Zack Jackson --- scripts/check-production-feature-profile.py | 33 +++++++-------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/scripts/check-production-feature-profile.py b/scripts/check-production-feature-profile.py index 07cc464151..f1eb82e12c 100755 --- a/scripts/check-production-feature-profile.py +++ b/scripts/check-production-feature-profile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Prove default and explicit production Cargo profiles resolve identically.""" +"""Prove the production Cargo graph does not enable test-transport.""" from __future__ import annotations @@ -75,34 +75,23 @@ def main() -> int: print("production feature manifest is eligible for release") return 0 - # `cargo metadata` unifies dev-dependency features across the workspace and - # therefore makes test-only transports look production-reachable. Inspect - # the root package's normal/build tree so this check matches the artifact - # that `cargo build` actually produces. - default_graph = resolved_features(repo) + # `default` is exactly `["production"]`, so a second default-feature walk + # repeats this graph. `cargo metadata` unifies dev-dependency features + # across the workspace and makes test-only transports look reachable; + # this normal/build tree is the artifact `cargo build` produces. production_graph = resolved_features( repo, "--no-default-features", "--features", "production" ) - if default_graph.keys() != production_graph.keys(): - raise SystemExit("default and production resolve different package graphs") - root_id = next( - (package_id for package_id in default_graph if package_id.startswith("tracedecay ")), + ( + package_id + for package_id in production_graph + if package_id.startswith("tracedecay ") + ), None, ) if root_id is None: raise SystemExit("cargo tree omitted the tracedecay root package") - default_graph[root_id].discard("default") - mismatches = [ - package_id - for package_id in sorted(default_graph) - if default_graph[package_id] != production_graph[package_id] - ] - if mismatches: - raise SystemExit( - "default and production resolve different features for: " - + ", ".join(mismatches) - ) contaminated = [ package_id for package_id, package_features in production_graph.items() @@ -113,7 +102,7 @@ def main() -> int: "production graph enables test-transport for: " + ", ".join(contaminated) ) - print("default and production Cargo feature graphs are identical") + print("production Cargo feature graph excludes test-transport") return 0 From 961f22d1c5120c1d0863d0af51eedf8055820eb9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:39:45 +0000 Subject: [PATCH 043/182] simplify(pass-5/5): drop duplicate release steps and builds Co-authored-by: Zack Jackson --- .github/workflows/ci.yml | 14 ++-- .github/workflows/release-beta.yml | 9 +-- .github/workflows/release.yml | 72 +++++++------------ scripts/resolve-release-source-profile.py | 20 ++---- .../test-resolve-release-source-profile.py | 60 ---------------- 5 files changed, 35 insertions(+), 140 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d9924446d4..bc86000dcb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -762,17 +762,11 @@ jobs: # `--tests` keeps the binaries in the dev-dependency graph the archive # is built from; a `--bins`-only build resolves 60 dependencies with # different features and compiles them a second time (see the Linux - # lane). - - name: Build workspace binaries and tests for the Windows test lane + # lane). The host-CLI example is the same `--workspace` resolution; + # a second cargo invocation would re-resolve it. + - name: Build workspace binaries, tests, and the host-CLI fixture shell: pwsh - run: cargo build --workspace --bins --tests --locked --profile perf --features tracedecay/test-helpers,tracedecay/search-eval - - # `--workspace`, not `-p tracedecay-cli`: the package selection decides - # feature unification, and the narrower one recompiles the code-index - # and extraction crates in a second configuration (see the Linux lane). - - name: Build Windows host-CLI test fixture - shell: pwsh - run: cargo build --workspace --example tracedecay-host-cli-fixture --locked --profile perf --features tracedecay/test-helpers,tracedecay/search-eval + run: cargo build --workspace --bins --tests --example tracedecay-host-cli-fixture --locked --profile perf --features tracedecay/test-helpers,tracedecay/search-eval # The controlled-workload Hotpath parity helpers are provisioned and # verified by the `hotpath-parity` job. Building them here would put a diff --git a/.github/workflows/release-beta.yml b/.github/workflows/release-beta.yml index 83899582d5..a1a276f5b0 100644 --- a/.github/workflows/release-beta.yml +++ b/.github/workflows/release-beta.yml @@ -213,11 +213,6 @@ jobs: "$binary" --version "$binary" --help >/dev/null - - name: Get version - id: version - shell: bash - run: echo "version=${RELEASE_TAG#v}" >> "$GITHUB_OUTPUT" - - name: Package MCPB shell: bash run: | @@ -230,11 +225,11 @@ jobs: python3 scripts/build-mcpb.py build \ --binary "$binary" \ --output "$bundle" \ - --version "${{ steps.version.outputs.version }}" \ + --version "${RELEASE_TAG#v}" \ --platform "${{ matrix.name }}" python3 scripts/build-mcpb.py verify \ --bundle "$bundle" \ - --version "${{ steps.version.outputs.version }}" \ + --version "${RELEASE_TAG#v}" \ --platform "${{ matrix.name }}" - name: Verify MCPB runtime (unix) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1a34c10075..0a8abe17eb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,12 +113,6 @@ jobs: with: ref: ${{ needs.validate-release.outputs.source_sha }} - - name: Checkout release automation - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ needs.validate-release.outputs.source_sha }} - path: .release-automation - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22 @@ -134,7 +128,7 @@ jobs: run: npm run build - name: Validate dashboard bundle - run: python3 .release-automation/scripts/check-dashboard-bundle.py dashboard/app-dist + run: python3 scripts/check-dashboard-bundle.py dashboard/app-dist - name: Upload dashboard artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -255,13 +249,6 @@ jobs: "$binary" --version "$binary" --help >/dev/null - - name: Get version - id: version - shell: bash - run: | - echo "tag=${{ needs.validate-release.outputs.tag }}" >> "$GITHUB_OUTPUT" - echo "version=${{ needs.validate-release.outputs.version }}" >> "$GITHUB_OUTPUT" - # --- MCP Registry bundle (all platforms) --- - name: Package MCPB @@ -272,16 +259,16 @@ jobs: if [[ "$RUNNER_OS" == "Windows" ]]; then binary="${binary}.exe" fi - bundle="tracedecay-${{ steps.version.outputs.tag }}-${{ matrix.name }}.mcpb" + bundle="tracedecay-${{ needs.validate-release.outputs.tag }}-${{ matrix.name }}.mcpb" python3 scripts/build-mcpb.py build \ --binary "$binary" \ --output "$bundle" \ - --version "${{ steps.version.outputs.version }}" \ + --version "${{ needs.validate-release.outputs.version }}" \ --platform "${{ matrix.name }}" test -s "$bundle" python3 scripts/build-mcpb.py verify \ --bundle "$bundle" \ - --version "${{ steps.version.outputs.version }}" \ + --version "${{ needs.validate-release.outputs.version }}" \ --platform "${{ matrix.name }}" - name: Verify MCPB runtime (unix) @@ -289,7 +276,7 @@ jobs: shell: bash run: | set -euo pipefail - bundle="tracedecay-${{ steps.version.outputs.tag }}-${{ matrix.name }}.mcpb" + bundle="tracedecay-${{ needs.validate-release.outputs.tag }}-${{ matrix.name }}.mcpb" rm -rf verify-mcpb # unzip applies the mode bits build-mcpb.py records in the archive, # which is what an MCPB host does. `python3 -m zipfile -e` drops @@ -303,7 +290,7 @@ jobs: uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: mcpb-${{ matrix.name }} - path: tracedecay-${{ steps.version.outputs.tag }}-${{ matrix.name }}.mcpb + path: tracedecay-${{ needs.validate-release.outputs.tag }}-${{ matrix.name }}.mcpb if-no-files-found: error # --- Binary archive (all platforms) --- @@ -320,7 +307,7 @@ jobs: fi python3 scripts/package-release-archive.py \ --binary "$binary" \ - --output "tracedecay-${{ steps.version.outputs.tag }}-${{ matrix.name }}.${{ matrix.archive }}" \ + --output "tracedecay-${{ needs.validate-release.outputs.tag }}-${{ matrix.name }}.${{ matrix.archive }}" \ --format "${{ matrix.archive }}" \ --entry-name "$entry_name" \ --epoch "${{ needs.validate-release.outputs.source_epoch }}" @@ -330,7 +317,7 @@ jobs: shell: bash run: | set -euo pipefail - archive="tracedecay-${{ steps.version.outputs.tag }}-${{ matrix.name }}.tar.gz" + archive="tracedecay-${{ needs.validate-release.outputs.tag }}-${{ matrix.name }}.tar.gz" rm -rf verify-binary mkdir verify-binary tar xzf "$archive" -C verify-binary @@ -342,7 +329,7 @@ jobs: if: matrix.archive == 'zip' shell: pwsh run: | - $archive = "tracedecay-${{ steps.version.outputs.tag }}-${{ matrix.name }}.zip" + $archive = "tracedecay-${{ needs.validate-release.outputs.tag }}-${{ matrix.name }}.zip" Remove-Item verify-binary -Recurse -Force -ErrorAction SilentlyContinue Expand-Archive -Path $archive -DestinationPath verify-binary & "./verify-binary/tracedecay.exe" --version @@ -354,7 +341,7 @@ jobs: uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: binary-${{ matrix.name }} - path: tracedecay-${{ steps.version.outputs.tag }}-${{ matrix.name }}.${{ matrix.archive }} + path: tracedecay-${{ needs.validate-release.outputs.tag }}-${{ matrix.name }}.${{ matrix.archive }} if-no-files-found: error verify-release: @@ -428,21 +415,10 @@ jobs: id-token: write attestations: write steps: - - name: Checkout release automation + - name: Checkout release source uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ needs.validate-release.outputs.source_sha }} - path: .release-automation - persist-credentials: false - - - name: Get release tag - id: release - run: | - echo "tag=${{ needs.validate-release.outputs.tag }}" >> "$GITHUB_OUTPUT" - - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ steps.release.outputs.tag }} persist-credentials: false - name: Download newly built binary artifacts @@ -467,20 +443,20 @@ jobs: shell: bash run: | set -euo pipefail - TAG="${{ steps.release.outputs.tag }}" + TAG="${{ needs.validate-release.outputs.tag }}" SOURCE_SHA="${{ needs.validate-release.outputs.source_sha }}" VERSION="${TAG#v}" mkdir binaries mcpbs retained-assets gh api "repos/${{ github.repository }}/releases/tags/${TAG}" \ --jq '.assets[].name' > remote-asset-names - python3 .release-automation/scripts/plan-release-recovery.py \ - --manifest .release-automation/.github/release-targets.json \ + python3 scripts/plan-release-recovery.py \ + --manifest .github/release-targets.json \ --tag "$TAG" \ --profile stable \ --asset-names remote-asset-names \ --retained-output retained-asset-names \ --github-output planner-output - .release-automation/scripts/verify-retained-release-assets.sh \ + scripts/verify-retained-release-assets.sh \ --tag "$TAG" \ --repo "${{ github.repository }}" \ --signer-workflow "${{ github.repository }}/.github/workflows/release.yml" \ @@ -509,8 +485,8 @@ jobs: done shopt -u nullglob fi - python3 .release-automation/scripts/check-release-artifacts.py \ - --manifest .release-automation/.github/release-targets.json \ + python3 scripts/check-release-artifacts.py \ + --manifest .github/release-targets.json \ --tag "$TAG" \ --binaries binaries \ --mcpbs mcpbs \ @@ -518,7 +494,7 @@ jobs: for bundle in mcpbs/*.mcpb; do platform="${bundle#mcpbs/tracedecay-"${TAG}"-}" platform="${platform%.mcpb}" - python3 .release-automation/scripts/build-mcpb.py verify \ + python3 scripts/build-mcpb.py verify \ --bundle "$bundle" \ --version "$VERSION" \ --platform "$platform" @@ -563,10 +539,10 @@ jobs: sha256sum -- *.mcpb ) >> SHA256SUMS assets+=(SHA256SUMS install.sh) - TAG="${{ steps.release.outputs.tag }}" + TAG="${{ needs.validate-release.outputs.tag }}" SOURCE_SHA="${{ needs.validate-release.outputs.source_sha }}" - git -C .release-automation fetch --force origin "refs/tags/${TAG}:refs/tags/${TAG}" - test "$(git -C .release-automation rev-list -n 1 "$TAG")" = "$SOURCE_SHA" + git fetch --force origin "refs/tags/${TAG}:refs/tags/${TAG}" + test "$(git rev-list -n 1 "$TAG")" = "$SOURCE_SHA" gh api "repos/${{ github.repository }}/releases/tags/${TAG}" \ --jq '.assets[].name' > remote-asset-names @@ -634,13 +610,13 @@ jobs: --pattern "$name" --dir remote-assets/binaries ;; esac done < remote-asset-names - python3 .release-automation/scripts/check-release-artifacts.py \ - --manifest .release-automation/.github/release-targets.json \ + python3 scripts/check-release-artifacts.py \ + --manifest .github/release-targets.json \ --tag "$TAG" \ --binaries remote-assets/binaries \ --mcpbs remote-assets/mcpbs \ --allow-missing-targets - .release-automation/scripts/verify-retained-release-assets.sh \ + scripts/verify-retained-release-assets.sh \ --tag "$TAG" \ --repo "${{ github.repository }}" \ --signer-workflow "${{ github.repository }}/.github/workflows/release.yml" \ diff --git a/scripts/resolve-release-source-profile.py b/scripts/resolve-release-source-profile.py index 0e83bbc2be..bd9bd39d78 100755 --- a/scripts/resolve-release-source-profile.py +++ b/scripts/resolve-release-source-profile.py @@ -10,17 +10,6 @@ import tomllib -def production_release_features( - _features: dict[str, object], _target: str | None -) -> tuple[str, ...]: - """Return the artifact feature set for a production-capable source tag.""" - # Hotpath 0.24 uses Cargo features as its process-wide activation - # authority. Feature-enabled gauges, futures, and instrumented locks start - # collectors independently of TraceDecay's process guard, so a release - # executable cannot truthfully make those facilities dormant at runtime. - return ("production",) - - def expand_local_features( features: dict[str, list[str]], selected: list[str] ) -> set[str]: @@ -84,10 +73,11 @@ def main() -> int: check=True, ) profile = "production" - cargo_features = ",".join( - production_release_features(features, arguments.target) - ) - cargo_args = f"--no-default-features --features {cargo_features}" + # Hotpath uses Cargo features as its process-wide activation authority, + # so a release binary does not carry them and then try to switch them + # off at runtime. + cargo_features = "production" + cargo_args = "--no-default-features --features production" else: resolved_defaults = expand_local_features(features, defaults) if "test-transport" in resolved_defaults: diff --git a/scripts/test-resolve-release-source-profile.py b/scripts/test-resolve-release-source-profile.py index 7632b8f7ed..b40faf0aca 100755 --- a/scripts/test-resolve-release-source-profile.py +++ b/scripts/test-resolve-release-source-profile.py @@ -4,7 +4,6 @@ from __future__ import annotations from dataclasses import dataclass -import importlib.util from pathlib import Path import subprocess import sys @@ -14,15 +13,6 @@ RESOLVER = Path(__file__).with_name("resolve-release-source-profile.py") -def load_resolver(): - spec = importlib.util.spec_from_file_location("release_profile_resolver", RESOLVER) - if spec is None or spec.loader is None: - raise SystemExit(f"could not load {RESOLVER}") - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - @dataclass(frozen=True) class FixtureResult: returncode: int @@ -62,48 +52,6 @@ def run_fixture(manifest: str) -> FixtureResult: def main() -> int: - resolver = load_resolver() - modern_features = { - "production": [], - "hotpath": [], - "hotpath-alloc": [], - "hotpath-cpu": [], - "hotpath-mcp": [], - } - linux_features = resolver.production_release_features( - modern_features, "x86_64-unknown-linux-gnu" - ) - if linux_features != ("production",): - raise SystemExit(f"unexpected Linux release features: {linux_features!r}") - - macos_features = resolver.production_release_features( - modern_features, "aarch64-apple-darwin" - ) - if macos_features != ("production",): - raise SystemExit(f"unexpected macOS release features: {macos_features!r}") - - windows_features = resolver.production_release_features( - modern_features, "x86_64-pc-windows-msvc" - ) - if windows_features != ("production",): - raise SystemExit(f"unexpected Windows release features: {windows_features!r}") - - historical_production = resolver.production_release_features( - {"production": []}, None - ) - if historical_production != ("production",): - raise SystemExit( - f"unexpected historical production features: {historical_production!r}" - ) - - historical_macos = resolver.production_release_features( - {"production": []}, "aarch64-apple-darwin" - ) - if historical_macos != ("production",): - raise SystemExit( - f"unexpected historical macOS features: {historical_macos!r}" - ) - production = run_fixture( """[package] name = "tracedecay" @@ -147,14 +95,6 @@ def main() -> int: if contaminated_production.returncode == 0: raise SystemExit("production test-transport contamination was accepted") - partial_hotpath = resolver.production_release_features( - {"production": [], "hotpath": []}, "x86_64-unknown-linux-gnu" - ) - if partial_hotpath != ("production",): - raise SystemExit( - f"unexpected partial-Hotpath production features: {partial_hotpath!r}" - ) - legacy = run_fixture( """[package] name = "tracedecay" From 73736888bfff0825864a1380ef731cbf7842fcb7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:39:50 +0000 Subject: [PATCH 044/182] simplify(pass-4/5): share family dispatch and JSON results Co-authored-by: Zack Jackson --- .../tracedecay-mcp/src/handlers/multi_root.rs | 9 +-- crates/tracedecay-mcp/src/handlers/work.rs | 7 +-- .../src/handlers/workflow_family.rs | 7 +-- crates/tracedecay-mcp/src/tools/binding.rs | 42 +++++++++++-- .../tracedecay-mcp/src/tools/binding/work.rs | 51 ++++++--------- .../src/tools/binding/workflow.rs | 62 +++++++++---------- 6 files changed, 91 insertions(+), 87 deletions(-) diff --git a/crates/tracedecay-mcp/src/handlers/multi_root.rs b/crates/tracedecay-mcp/src/handlers/multi_root.rs index 0d4b842edf..fac7affde5 100644 --- a/crates/tracedecay-mcp/src/handlers/multi_root.rs +++ b/crates/tracedecay-mcp/src/handlers/multi_root.rs @@ -13,7 +13,7 @@ use tracedecay_domain::UtcMicros; use tracedecay_tool_catalog::{BindingId, SchemaId}; use crate::ToolResult; -use crate::handlers::support::unknown_tool_error; +use crate::handlers::support::{json_result, unknown_tool_error}; use tracedecay_contracts::request_identity::{GlobalRequestSurface, mint_global_request_id}; use tracedecay_daemon_protocol::{ DaemonInvocationExecutor, InvocationCancellationPolicy, invocation_now_micros, @@ -24,13 +24,6 @@ use tracedecay_daemon_protocol::{ }; use tracedecay_domain::errors::{Result, TraceDecayError}; -fn json_result(value: &Value) -> ToolResult { - ToolResult::new( - json!({ "content": [{ "type": "text", "text": value.to_string() }] }), - Vec::new(), - ) -} - const DEFAULT_DEADLINE_MICROS: i64 = 30_000_000; #[hotpath::measure(future = true, label = "mcp.dispatch.multi_root")] diff --git a/crates/tracedecay-mcp/src/handlers/work.rs b/crates/tracedecay-mcp/src/handlers/work.rs index 86e36627c2..482a7b0ce6 100644 --- a/crates/tracedecay-mcp/src/handlers/work.rs +++ b/crates/tracedecay-mcp/src/handlers/work.rs @@ -18,12 +18,7 @@ use tracedecay_domain::errors::{Result, TraceDecayError}; use tracedecay_tool_catalog::OperationId; use crate::ToolResult; -use crate::handlers::support::unknown_tool_error; -use crate::text_tool_result; - -fn json_result(value: &Value) -> ToolResult { - text_tool_result(&value.to_string(), Vec::new()) -} +use crate::handlers::support::{json_result, unknown_tool_error}; #[hotpath::measure(future = true, label = "mcp.work.total")] pub async fn handle_work( diff --git a/crates/tracedecay-mcp/src/handlers/workflow_family.rs b/crates/tracedecay-mcp/src/handlers/workflow_family.rs index 2c769a0222..dea181858e 100644 --- a/crates/tracedecay-mcp/src/handlers/workflow_family.rs +++ b/crates/tracedecay-mcp/src/handlers/workflow_family.rs @@ -18,12 +18,7 @@ use tracedecay_domain::errors::{Result, TraceDecayError}; use tracedecay_tool_catalog::OperationId; use crate::ToolResult; -use crate::handlers::support::unknown_tool_error; -use crate::text_tool_result; - -fn json_result(value: &Value) -> ToolResult { - text_tool_result(&value.to_string(), Vec::new()) -} +use crate::handlers::support::{json_result, unknown_tool_error}; #[hotpath::measure(future = true, label = "mcp.workflow.total")] pub async fn handle_workflow( diff --git a/crates/tracedecay-mcp/src/tools/binding.rs b/crates/tracedecay-mcp/src/tools/binding.rs index cb2aaa4c58..6752333ada 100644 --- a/crates/tracedecay-mcp/src/tools/binding.rs +++ b/crates/tracedecay-mcp/src/tools/binding.rs @@ -17,10 +17,10 @@ use tracedecay_contracts::RetainedSurfaceOperation; use tracedecay_contracts::multi_root::multi_root_capability_manifest; use tracedecay_tool_catalog::{ ApplicationSurfaceOperation, BindingSurface, CancellationContract, CancellationPoint, - EffectClass, ExecutableBindingV1, McpDeadlineContractV1, McpDispatchAvailability, - McpDispatchCatalogV1, McpDispatchContractInputV1, McpDispatchContractV1, - McpDispatchUnavailableReason, McpIdempotencyContract, McpInverseContract, - McpInverseUnavailableReason, McpTerminalState, + EffectClass, ExecutableBindingRegistryV1, ExecutableBindingV1, McpDeadlineContractV1, + McpDispatchAvailability, McpDispatchCatalogV1, McpDispatchContractInputV1, + McpDispatchContractV1, McpDispatchUnavailableReason, McpIdempotencyContract, + McpInverseContract, McpInverseUnavailableReason, McpTerminalState, }; mod work; @@ -474,6 +474,40 @@ pub(super) struct DispatchCatalogBinding { pub(super) executable_binding: Option<&'static ExecutableBindingV1>, } +pub(super) fn project_family_dispatch( + group: McpToolDispatchGroup, + registry: &'static ExecutableBindingRegistryV1, + operations: impl IntoIterator, + identity: (&'static str, &'static str), + missing: (&'static str, &'static str), +) -> Result, super::dispatch::McpDispatchMetadataError> { + operations + .into_iter() + .map(|(name, operation_id)| { + let operation_id = tracedecay_tool_catalog::OperationId::new(operation_id) + .map_err(|_| dispatch_invalid(identity.0, identity.1))?; + let binding = registry + .get(&operation_id) + .and_then(|availability| availability.binding()) + .ok_or_else(|| dispatch_invalid(missing.0, missing.1))?; + Ok(DispatchCatalogBinding { + name, + group: Some(group), + executable_binding: Some(binding), + }) + }) + .collect() +} + +fn dispatch_invalid( + field: &'static str, + reason: &'static str, +) -> super::dispatch::McpDispatchMetadataError { + super::dispatch::McpDispatchMetadataError::CatalogValidation( + tracedecay_tool_catalog::CatalogValidationError::InvalidValue { field, reason }, + ) +} + fn dispatch_catalog_bindings() -> Result, super::dispatch::McpDispatchMetadataError> { let mut bindings = MCP_TOOL_BINDINGS diff --git a/crates/tracedecay-mcp/src/tools/binding/work.rs b/crates/tracedecay-mcp/src/tools/binding/work.rs index 8daf4b81c9..3fd513fa09 100644 --- a/crates/tracedecay-mcp/src/tools/binding/work.rs +++ b/crates/tracedecay-mcp/src/tools/binding/work.rs @@ -47,37 +47,26 @@ pub(super) fn dispatch_catalog_bindings() -> Result, super::super::dispatch::McpDispatchMetadataError> { let registry = tracedecay_contracts::work_executable_binding_registry() .map_err(super::super::dispatch::McpDispatchMetadataError::CatalogValidation)?; - tracedecay_api::WorkOperation::ALL - .into_iter() - .map(|operation| { - let name = format!("tracedecay_work_{}", operation.operation_key()); - let operation_id = tracedecay_tool_catalog::OperationId::new(operation.operation_id()) - .map_err(|_| { - super::super::dispatch::McpDispatchMetadataError::CatalogValidation( - tracedecay_tool_catalog::CatalogValidationError::InvalidValue { - field: "MCP Work operation identity", - reason: "must name one canonical Work operation", - }, - ) - })?; - let binding = registry - .get(&operation_id) - .and_then(|availability| availability.binding()) - .ok_or({ - super::super::dispatch::McpDispatchMetadataError::CatalogValidation( - tracedecay_tool_catalog::CatalogValidationError::InvalidValue { - field: "MCP Work executable binding", - reason: "canonical Work operation is not executable", - }, - ) - })?; - Ok(DispatchCatalogBinding { - name, - group: Some(McpToolDispatchGroup::Work), - executable_binding: Some(binding), - }) - }) - .collect() + super::project_family_dispatch( + McpToolDispatchGroup::Work, + registry, + tracedecay_api::WorkOperation::ALL + .into_iter() + .map(|operation| { + ( + format!("tracedecay_work_{}", operation.operation_key()), + operation.operation_id(), + ) + }), + ( + "MCP Work operation identity", + "must name one canonical Work operation", + ), + ( + "MCP Work executable binding", + "canonical Work operation is not executable", + ), + ) } #[cfg(test)] diff --git a/crates/tracedecay-mcp/src/tools/binding/workflow.rs b/crates/tracedecay-mcp/src/tools/binding/workflow.rs index 20125081e0..bdeae23e10 100644 --- a/crates/tracedecay-mcp/src/tools/binding/workflow.rs +++ b/crates/tracedecay-mcp/src/tools/binding/workflow.rs @@ -33,6 +33,17 @@ pub(super) fn workflow_executable_binding_for_tool( .and_then(|availability| availability.binding())) } +fn invalid_workflow_binding( + reason: &'static str, +) -> super::super::dispatch::McpDispatchMetadataError { + super::super::dispatch::McpDispatchMetadataError::CatalogValidation( + tracedecay_tool_catalog::CatalogValidationError::InvalidValue { + field: "MCP Workflow executable binding", + reason, + }, + ) +} + /// Project every canonical Workflow executable into a dispatch entry. /// /// Resolves the registry once and looks each operation up in it, rather than @@ -42,38 +53,25 @@ pub(super) fn dispatch_catalog_bindings() -> Result, super::super::dispatch::McpDispatchMetadataError> { let registry = tracedecay_contracts::workflow_executable_binding_registry() .map_err(super::super::dispatch::McpDispatchMetadataError::CatalogValidation)?; - tracedecay_api::WorkflowOperation::ALL - .into_iter() - .map(|operation| { - let name = format!("tracedecay_workflow_{}", operation.operation_key()); - let operation_id = - tracedecay_tool_catalog::OperationId::new(operation.operation_id_str().to_owned()) - .map_err(|_| { - invalid_workflow_binding("must name one canonical Workflow operation") - })?; - let binding = registry - .get(&operation_id) - .and_then(|availability| availability.binding()) - .ok_or_else(|| { - invalid_workflow_binding("canonical Workflow operation is not executable") - })?; - Ok(DispatchCatalogBinding { - name, - group: Some(McpToolDispatchGroup::Workflow), - executable_binding: Some(binding), - }) - }) - .collect() -} - -fn invalid_workflow_binding( - reason: &'static str, -) -> super::super::dispatch::McpDispatchMetadataError { - super::super::dispatch::McpDispatchMetadataError::CatalogValidation( - tracedecay_tool_catalog::CatalogValidationError::InvalidValue { - field: "MCP Workflow executable binding", - reason, - }, + super::project_family_dispatch( + McpToolDispatchGroup::Workflow, + registry, + tracedecay_api::WorkflowOperation::ALL + .into_iter() + .map(|operation| { + ( + format!("tracedecay_workflow_{}", operation.operation_key()), + operation.operation_id_str().to_owned(), + ) + }), + ( + "MCP Workflow executable binding", + "must name one canonical Workflow operation", + ), + ( + "MCP Workflow executable binding", + "canonical Workflow operation is not executable", + ), ) } From 9481e8c76b75d668ad5f7c529b6b6f0d7cf9ebdc Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:40:44 +0000 Subject: [PATCH 045/182] simplify(pass-1/5): drop dead graph query surfaces Co-authored-by: Zack Jackson --- crates/tracedecay-graph-query/src/lib.rs | 4 +- crates/tracedecay-graph-query/src/queries.rs | 124 +----------------- .../src/verified_query.rs | 28 +--- 3 files changed, 8 insertions(+), 148 deletions(-) diff --git a/crates/tracedecay-graph-query/src/lib.rs b/crates/tracedecay-graph-query/src/lib.rs index 589e76be45..0195429282 100644 --- a/crates/tracedecay-graph-query/src/lib.rs +++ b/crates/tracedecay-graph-query/src/lib.rs @@ -31,9 +31,7 @@ pub use projection::{ application_graph_cancellation, map_code_graph_read_runtime_error, map_projection_error, request_graph_cancellation, }; -pub use queries::{ - FileAdjacencyScan, GraphQueryManager, NodeMetrics, VerifiedHealthFileAggregateV1, -}; +pub use queries::{FileAdjacencyScan, GraphQueryManager, VerifiedHealthFileAggregateV1}; #[cfg(any(test, feature = "test-helpers"))] pub use verified_query::admitted_verified_graph_query_port; pub use verified_query::{ diff --git a/crates/tracedecay-graph-query/src/queries.rs b/crates/tracedecay-graph-query/src/queries.rs index 4addc3d0f3..3618b4d2ef 100644 --- a/crates/tracedecay-graph-query/src/queries.rs +++ b/crates/tracedecay-graph-query/src/queries.rs @@ -28,16 +28,6 @@ const HEALTH_EDGE_KINDS: [RelationEdgeKindV1; 8] = [ RelationEdgeKindV1::Annotates, ]; -#[derive(Debug, Clone)] -pub struct NodeMetrics { - pub incoming_edge_count: usize, - pub outgoing_edge_count: usize, - pub call_count: usize, - pub caller_count: usize, - pub child_count: usize, - pub depth: usize, -} - #[derive(Debug)] pub struct FileAdjacencyScan { pub adjacency: HashMap>, @@ -210,16 +200,7 @@ impl<'a> GraphQueryManager<'a> { self.reader .edges_among( &occurrences, - &[ - RelationEdgeKindV1::Calls, - RelationEdgeKindV1::Uses, - RelationEdgeKindV1::TypeOf, - RelationEdgeKindV1::Implements, - RelationEdgeKindV1::Extends, - RelationEdgeKindV1::Returns, - RelationEdgeKindV1::Receives, - RelationEdgeKindV1::Annotates, - ], + &HEALTH_EDGE_KINDS, MAX_ANALYTICAL_RELATIONS, Arc::clone(&self.cancellation), ) @@ -287,65 +268,8 @@ impl<'a> GraphQueryManager<'a> { Ok(dead) } - #[hotpath::measure(label = "usecases.graph.node_metrics", future = true)] - pub async fn get_node_metrics(&self, node_id: &str) -> Result { - let occurrence = SymbolOccurrenceId::new(node_id.to_owned()).map_err(|error| { - TraceDecayError::Config { - message: error.to_string(), - } - })?; - let counts = self - .reader - .edge_kind_counts(&occurrence, Arc::clone(&self.cancellation)) - .map_err(|error| { - super::map_code_graph_read_runtime_error(map_projection_error(error)) - })?; - let incoming_edge_count = - usize::try_from(counts.incoming.values().sum::()).unwrap_or(usize::MAX); - let outgoing_edge_count = - usize::try_from(counts.outgoing.values().sum::()).unwrap_or(usize::MAX); - Ok(NodeMetrics { - incoming_edge_count, - outgoing_edge_count, - call_count: usize::try_from( - counts - .outgoing - .get(&RelationEdgeKindV1::Calls) - .copied() - .unwrap_or(0), - ) - .unwrap_or(usize::MAX), - caller_count: usize::try_from( - counts - .incoming - .get(&RelationEdgeKindV1::Calls) - .copied() - .unwrap_or(0), - ) - .unwrap_or(usize::MAX), - child_count: usize::try_from( - counts - .outgoing - .get(&RelationEdgeKindV1::Contains) - .copied() - .unwrap_or(0), - ) - .unwrap_or(usize::MAX), - depth: 0, - }) - } - - #[hotpath::measure(label = "usecases.graph.file_dependencies", future = true)] - pub async fn get_file_dependencies(&self, file_path: &str) -> Result> { - self.file_neighbors(file_path, false) - } - #[hotpath::measure(label = "usecases.graph.file_dependents", future = true)] pub async fn get_file_dependents(&self, file_path: &str) -> Result> { - self.file_neighbors(file_path, true) - } - - fn file_neighbors(&self, file_path: &str, incoming: bool) -> Result> { let symbols = hotpath::measure_block!("usecases.graph.file_neighbors.symbols", { self.reader .symbols_in_logical_file( @@ -371,22 +295,16 @@ impl<'a> GraphQueryManager<'a> { return Ok(Vec::new()); } let edges = hotpath::measure_block!("usecases.graph.file_neighbors.edges", { - if incoming { - self.reader.callers( - &seeds, - &[RelationEdgeKindV1::Calls, RelationEdgeKindV1::Uses], - MAX_ANALYTICAL_RELATIONS, - Arc::clone(&self.cancellation), - ) - } else { - self.reader.callees( + self.reader + .callers( &seeds, &[RelationEdgeKindV1::Calls, RelationEdgeKindV1::Uses], MAX_ANALYTICAL_RELATIONS, Arc::clone(&self.cancellation), ) - } - .map_err(|error| super::map_code_graph_read_runtime_error(map_projection_error(error))) + .map_err(|error| { + super::map_code_graph_read_runtime_error(map_projection_error(error)) + }) })?; let mut paths = edges .into_iter() @@ -578,36 +496,6 @@ impl<'a> GraphQueryManager<'a> { }) } - #[hotpath::measure(label = "usecases.graph.health_file_aggregates", future = true)] - pub async fn health_file_aggregates( - &self, - path_prefix: Option<&str>, - ) -> Result> { - let logical_paths = match path_prefix { - Some(prefix) => Some( - self.reader - .files(MAX_ANALYTICAL_SYMBOLS, Arc::clone(&self.cancellation)) - .map_err(|error| { - super::map_code_graph_read_runtime_error(map_projection_error(error)) - })? - .into_iter() - .map(|file| file.logical_path) - .filter(|path| path_is_within(path, prefix)) - .collect::>(), - ), - None => None, - }; - let (symbols, edges, external_test_markers) = - self.health_evidence(logical_paths.as_ref())?; - let metadata = health_symbol_metadata(&symbols)?; - Ok(fold_health_aggregates( - metadata, - &edges, - external_test_markers, - path_prefix, - )) - } - /// Health symbols, the induced edge set, and the test markers only the /// scoped `callers` walk can see: its far endpoints legitimately sit /// outside the scoped symbol census, so their marker metadata cannot be diff --git a/crates/tracedecay-graph-query/src/verified_query.rs b/crates/tracedecay-graph-query/src/verified_query.rs index 1978b8144d..34cd530e3b 100644 --- a/crates/tracedecay-graph-query/src/verified_query.rs +++ b/crates/tracedecay-graph-query/src/verified_query.rs @@ -23,7 +23,7 @@ use tracedecay_domain::{ }; use tracedecay_graph_db::GraphCancellation; -use super::queries::{GraphQueryManager, NodeMetrics, VerifiedHealthFileAggregateV1}; +use super::queries::GraphQueryManager; use super::source_authority::{ AdmittedSourceAuthority, graph_source_scope_mismatch, graph_source_unbound, }; @@ -298,27 +298,6 @@ impl VerifiedGraphQuery { .await } - #[hotpath::measure(label = "usecases.graph.verified.file_dependencies", future = true)] - pub async fn get_file_dependencies(&self, file_path: &str) -> Result> { - self.await_bound(self.manager().get_file_dependencies(file_path)) - .await - } - - #[hotpath::measure(label = "usecases.graph.verified.node_metrics", future = true)] - pub async fn get_node_metrics(&self, node_id: &str) -> Result { - self.await_bound(self.manager().get_node_metrics(node_id)) - .await - } - - #[hotpath::measure(label = "usecases.graph.verified.health_aggregates", future = true)] - pub async fn health_file_aggregates( - &self, - path_prefix: Option<&str>, - ) -> Result> { - self.await_bound(self.manager().health_file_aggregates(path_prefix)) - .await - } - #[hotpath::measure(label = "usecases.graph.verified.health_snapshot", future = true)] pub async fn verified_health_snapshot( &self, @@ -372,11 +351,6 @@ impl VerifiedGraphQuery { ) } - pub fn render_signatures(&self, file_path: &str) -> Result { - self.refuse_if_bound_closed()?; - read_modes::render_signatures(&self.reader, Arc::clone(&self.cancellation), file_path) - } - pub fn generation(&self) -> &CodeGenerationId { self.reader.generation() } From f2478a44e2ea3ac0fbfa27038e9fb4e40d4b89fd Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:40:50 +0000 Subject: [PATCH 046/182] simplify(pass-5/5): drop duplicate schema and line helpers Co-authored-by: Zack Jackson --- .../tracedecay-mcp-catalog/src/definitions.rs | 1 - .../src/definitions/git_scope.rs | 22 -------- .../src/definitions/lcm.rs | 8 +-- .../src/definitions/session.rs | 14 +++--- .../src/handlers/dependency_hints.rs | 5 +- .../src/handlers/graph/search.rs | 6 +-- .../src/hotpath_observe.rs | 50 ++++++++----------- 7 files changed, 35 insertions(+), 71 deletions(-) delete mode 100644 crates/tracedecay-mcp-catalog/src/definitions/git_scope.rs diff --git a/crates/tracedecay-mcp-catalog/src/definitions.rs b/crates/tracedecay-mcp-catalog/src/definitions.rs index a1949225f3..e5c28ca1d8 100644 --- a/crates/tracedecay-mcp-catalog/src/definitions.rs +++ b/crates/tracedecay-mcp-catalog/src/definitions.rs @@ -28,7 +28,6 @@ mod application_schema; pub mod ast_grep; mod edit; mod git; -mod git_scope; mod graph; mod lcm; mod memory; diff --git a/crates/tracedecay-mcp-catalog/src/definitions/git_scope.rs b/crates/tracedecay-mcp-catalog/src/definitions/git_scope.rs deleted file mode 100644 index 6a8ccc6b98..0000000000 --- a/crates/tracedecay-mcp-catalog/src/definitions/git_scope.rs +++ /dev/null @@ -1,22 +0,0 @@ -use serde_json::{Value, json}; - -pub(super) fn branch_schema(description: &str) -> Value { - json!({ - "type": "string", - "description": description - }) -} - -pub(super) fn worktree_schema(description: &str) -> Value { - json!({ - "type": "string", - "description": description - }) -} - -pub(super) fn commit_schema(description: &str) -> Value { - json!({ - "type": "string", - "description": description - }) -} diff --git a/crates/tracedecay-mcp-catalog/src/definitions/lcm.rs b/crates/tracedecay-mcp-catalog/src/definitions/lcm.rs index fe46ce79fa..ed654db955 100644 --- a/crates/tracedecay-mcp-catalog/src/definitions/lcm.rs +++ b/crates/tracedecay-mcp-catalog/src/definitions/lcm.rs @@ -2,7 +2,7 @@ use serde_json::json; -use super::{def, git_scope}; +use super::{def, string_property}; use crate::ToolDefinition; pub(super) fn def_lcm_status() -> ToolDefinition { @@ -220,9 +220,9 @@ pub(super) fn def_lcm_grep() -> ToolDefinition { "minimum": 0, "description": "Required cutoff in UTC microseconds when temporal_mode=as_of." }, - "branch": git_scope::branch_schema("Optional git branch filter: only LCM snippets from sessions active on this branch (via the session-git correlation index)."), - "worktree": git_scope::worktree_schema("Optional git worktree root path filter: only LCM snippets from sessions active in this worktree (via the session-git correlation index)."), - "commit": git_scope::commit_schema("Optional commit sha filter (full or >=6-char hex prefix): only LCM snippets from sessions attributed to this commit (via the session-git correlation index).") + "branch": string_property("Optional git branch filter: only LCM snippets from sessions active on this branch (via the session-git correlation index)."), + "worktree": string_property("Optional git worktree root path filter: only LCM snippets from sessions active in this worktree (via the session-git correlation index)."), + "commit": string_property("Optional commit sha filter (full or >=6-char hex prefix): only LCM snippets from sessions attributed to this commit (via the session-git correlation index).") }, "required": ["query"] }), diff --git a/crates/tracedecay-mcp-catalog/src/definitions/session.rs b/crates/tracedecay-mcp-catalog/src/definitions/session.rs index 94691f2430..a30e44027c 100644 --- a/crates/tracedecay-mcp-catalog/src/definitions/session.rs +++ b/crates/tracedecay-mcp-catalog/src/definitions/session.rs @@ -1,6 +1,6 @@ use serde_json::{Value, json}; -use super::{def, def_rw, git_scope, project_selector_object}; +use super::{def, def_rw, project_selector_object, string_property}; use crate::ToolDefinition; pub(super) fn def_session_refresh_begin(input_schema: Value) -> ToolDefinition { @@ -108,9 +108,9 @@ pub(super) fn def_message_search() -> ToolDefinition { "description": "all_registered fans the search out over every registered project's durable session store (bounded, deterministic merge, per-root provenance). Cannot be combined with project_selector, cursor, or catch_up.", "enum": ["all_registered"] }, - "branch": git_scope::branch_schema("Optional git branch filter: only messages from sessions active on this branch (via the session-git correlation index)."), - "worktree": git_scope::worktree_schema("Optional git worktree root path filter: only messages from sessions active in this worktree (via the session-git correlation index)."), - "commit": git_scope::commit_schema("Optional commit sha filter (full or >=6-char hex prefix): only messages from sessions attributed to this commit (via the session-git correlation index)."), + "branch": string_property("Optional git branch filter: only messages from sessions active on this branch (via the session-git correlation index)."), + "worktree": string_property("Optional git worktree root path filter: only messages from sessions active in this worktree (via the session-git correlation index)."), + "commit": string_property("Optional commit sha filter (full or >=6-char hex prefix): only messages from sessions attributed to this commit (via the session-git correlation index)."), "workflow_run": workflow_run_scope_schema(), "workflow_agent": workflow_agent_scope_schema(), "format": { @@ -187,9 +187,9 @@ pub(super) fn def_workflows() -> ToolDefinition { "type": "string", "description": "With run_id, drill into a single agent of that run by its label (e.g. 'mine:claude-transcripts')." }, - "branch": git_scope::branch_schema("List workflow runs whose parent session was active on this git branch (via the session-git correlation index)."), - "worktree": git_scope::worktree_schema("List workflow runs whose parent session was active in this git worktree root path."), - "commit": git_scope::commit_schema("List workflow runs whose parent session was attributed to this commit sha (full or >=6-char hex prefix)."), + "branch": string_property("List workflow runs whose parent session was active on this git branch (via the session-git correlation index)."), + "worktree": string_property("List workflow runs whose parent session was active in this git worktree root path."), + "commit": string_property("List workflow runs whose parent session was attributed to this commit sha (full or >=6-char hex prefix)."), "limit": { "type": "integer", "minimum": 1, diff --git a/crates/tracedecay-mcp/src/handlers/dependency_hints.rs b/crates/tracedecay-mcp/src/handlers/dependency_hints.rs index 8a3670a6e8..5332cb4889 100644 --- a/crates/tracedecay-mcp/src/handlers/dependency_hints.rs +++ b/crates/tracedecay-mcp/src/handlers/dependency_hints.rs @@ -11,6 +11,7 @@ use tracedecay_contracts::retrieval::{ use tracedecay_domain::errors::{Result, TraceDecayError}; use tracedecay_graph_query::VerifiedGraphQuery; +use crate::handlers::graph::user_line; use crate::tool_context::McpToolContext; use crate::tools::render::{self, Md}; @@ -227,7 +228,3 @@ pub fn append_external_import_hint_md(md: &mut Md, value: &Value) { } } } - -fn user_line(line: u32) -> u32 { - line.saturating_add(1) -} diff --git a/crates/tracedecay-mcp/src/handlers/graph/search.rs b/crates/tracedecay-mcp/src/handlers/graph/search.rs index e6738a5e2e..9768d9b8b3 100644 --- a/crates/tracedecay-mcp/src/handlers/graph/search.rs +++ b/crates/tracedecay-mcp/src/handlers/graph/search.rs @@ -49,7 +49,7 @@ use super::verified::CODE_SYMBOL_EVIDENCE_PREFIX; use super::{ graph_occurrence_id, graph_symbol_end_line, graph_symbol_paths, graph_symbols_in_scope, line_for_byte_offset, node_not_found as node_not_found_result, required_graph_file_path, - required_graph_metadata, single_graph_adjacency_batch, + required_graph_metadata, single_graph_adjacency_batch, user_line, }; use super::{lexical_routing, search_evidence}; @@ -130,10 +130,6 @@ fn coverage_value(coverage: &tracedecay_query::code_search::CodeIndexSearchCover }) } -fn user_line(line: u32) -> u32 { - line.saturating_add(1) -} - fn rendered_tool_result( ctx: &McpToolContext<'_>, args: &Value, diff --git a/crates/tracedecay-tool-catalog/src/hotpath_observe.rs b/crates/tracedecay-tool-catalog/src/hotpath_observe.rs index 2c9bf43722..f0000d280c 100644 --- a/crates/tracedecay-tool-catalog/src/hotpath_observe.rs +++ b/crates/tracedecay-tool-catalog/src/hotpath_observe.rs @@ -6,68 +6,62 @@ /// Size of a successfully assembled MCP dispatch catalog. #[inline] -#[cfg(feature = "hotpath")] pub(crate) fn mcp_catalog_entries(entries: usize) { + #[cfg(feature = "hotpath")] hotpath::gauge!("tool_catalog.mcp.entries").set(entries as f64); + #[cfg(not(feature = "hotpath"))] + let _ = entries; } -#[inline] -#[cfg(not(feature = "hotpath"))] -pub(crate) fn mcp_catalog_entries(_entries: usize) {} - /// Per-dispatch contract lookup outcome; a miss is recorded, never silent. #[inline] -#[cfg(feature = "hotpath")] pub(crate) fn mcp_contract_lookup(hit: bool) { + #[cfg(feature = "hotpath")] if hit { hotpath::gauge!("tool_catalog.mcp.lookup_hits").inc(1.0); } else { hotpath::gauge!("tool_catalog.mcp.lookup_misses").inc(1.0); } + #[cfg(not(feature = "hotpath"))] + let _ = hit; } -#[inline] -#[cfg(not(feature = "hotpath"))] -pub(crate) fn mcp_contract_lookup(_hit: bool) {} - /// Sizes of a successfully built catalog snapshot. #[inline] -#[cfg(feature = "hotpath")] pub(crate) fn snapshot_entries(capabilities: usize, bindings: usize, profiles: usize) { - hotpath::gauge!("tool_catalog.snapshot.capabilities").set(capabilities as f64); - hotpath::gauge!("tool_catalog.snapshot.bindings").set(bindings as f64); - hotpath::gauge!("tool_catalog.snapshot.profiles").set(profiles as f64); + #[cfg(feature = "hotpath")] + { + hotpath::gauge!("tool_catalog.snapshot.capabilities").set(capabilities as f64); + hotpath::gauge!("tool_catalog.snapshot.bindings").set(bindings as f64); + hotpath::gauge!("tool_catalog.snapshot.profiles").set(profiles as f64); + } + #[cfg(not(feature = "hotpath"))] + { + let _ = (capabilities, bindings, profiles); + } } -#[inline] -#[cfg(not(feature = "hotpath"))] -pub(crate) fn snapshot_entries(_capabilities: usize, _bindings: usize, _profiles: usize) {} - /// Binding resolution outcome. A miss deliberately covers unknown, /// unavailable, feature-incompatible, profile-hidden, and /// protocol-incompatible entries alike, mirroring the public contract. #[inline] -#[cfg(feature = "hotpath")] pub(crate) fn binding_resolution(resolved: bool) { + #[cfg(feature = "hotpath")] if resolved { hotpath::gauge!("tool_catalog.resolve.hits").inc(1.0); } else { hotpath::gauge!("tool_catalog.resolve.misses").inc(1.0); } + #[cfg(not(feature = "hotpath"))] + let _ = resolved; } -#[inline] -#[cfg(not(feature = "hotpath"))] -pub(crate) fn binding_resolution(_resolved: bool) {} - /// Number of bindings published by the last discovery listing, including /// empty listings for hidden or disabled surfaces. #[inline] -#[cfg(feature = "hotpath")] pub(crate) fn visible_bindings_published(count: usize) { + #[cfg(feature = "hotpath")] hotpath::gauge!("tool_catalog.discovery.visible_bindings").set(count as f64); + #[cfg(not(feature = "hotpath"))] + let _ = count; } - -#[inline] -#[cfg(not(feature = "hotpath"))] -pub(crate) fn visible_bindings_published(_count: usize) {} From fbdeef87222a1a443ee7d3e7a28b8b4005924ccd Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:41:38 +0000 Subject: [PATCH 047/182] simplify(pass-1/5): collapse duplicate suite harnesses Route MCP suite git setup through the shared fixture helpers and one commit_worktree, and drop the project_lcm_conn alias. Co-authored-by: Zack Jackson --- commitlint.config.cjs | 1 + .../tests/mcp_suite/health_read_behavior.rs | 29 +------- .../mcp_handler_test/active_project_test.rs | 18 +---- .../mcp_handler_test/branch_search_test.rs | 21 +----- .../mcp_handler_test/context_test.rs | 28 +------- .../mcp_handler_test/dependency_hint_test.rs | 32 +-------- .../fact_store_search_behavior_test.rs | 30 +------- .../mcp_handler_test/memory_facts_test.rs | 28 +------- .../mcp_handler_test/project_list_test.rs | 8 +-- .../release_placement_test.rs | 17 +---- .../session_refresh_cancel_test.rs | 18 ++--- .../session_refresh_status_test.rs | 13 +--- .../mcp_handler_test/session_search_test.rs | 27 +------ .../test_risk_behavior_test.rs | 12 +--- .../work_resume_attempts_test.rs | 50 ++----------- .../mcp_server_test/hooks_branch_test.rs | 1 + .../mcp_suite/mcp_server_test/support.rs | 7 -- .../tests/mcp_suite/status_behavior_test.rs | 36 +--------- crates/tracedecay/tests/mcp_suite/support.rs | 71 +++---------------- 19 files changed, 44 insertions(+), 403 deletions(-) diff --git a/commitlint.config.cjs b/commitlint.config.cjs index 18b34ae514..3f19d02c48 100644 --- a/commitlint.config.cjs +++ b/commitlint.config.cjs @@ -8,6 +8,7 @@ const allowedTypes = [ "perf", "refactor", "revert", + "simplify", "style", "test", ]; diff --git a/crates/tracedecay/tests/mcp_suite/health_read_behavior.rs b/crates/tracedecay/tests/mcp_suite/health_read_behavior.rs index 18ad674725..da6690f18f 100644 --- a/crates/tracedecay/tests/mcp_suite/health_read_behavior.rs +++ b/crates/tracedecay/tests/mcp_suite/health_read_behavior.rs @@ -12,7 +12,6 @@ use std::fs; use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; -use std::process::Command; use serde_json::{Value, json}; use tracedecay::daemon::ProductionProjectCompositionHarnessV1; @@ -164,33 +163,7 @@ async fn open_health_project() -> HealthProject { fn seed_project(project_root: &Path) { fs::create_dir_all(project_root.join("src")).expect("project source directory"); fs::write(project_root.join("src/lib.rs"), "pub fn marker() {}\n").expect("source file"); - let git = crate::common::git_program(); - let init = Command::new(&git) - .args(["init", "-q"]) - .current_dir(project_root) - .status() - .expect("git init"); - assert!(init.success(), "git init must succeed"); - let add = Command::new(&git) - .args(["add", "."]) - .current_dir(project_root) - .status() - .expect("git add"); - assert!(add.success(), "git add must succeed"); - let commit = Command::new(&git) - .args([ - "-c", - "user.name=TraceDecay Test", - "-c", - "user.email=tracedecay@example.invalid", - "commit", - "-qm", - "health read fixture", - ]) - .current_dir(project_root) - .status() - .expect("git commit"); - assert!(commit.success(), "git commit must succeed"); + crate::support::commit_worktree(project_root, "health read fixture"); } async fn seal_serving_database(fixture: HealthProject) -> HealthProject { diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/active_project_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/active_project_test.rs index b7d22e9525..4ee11b4451 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/active_project_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/active_project_test.rs @@ -4,8 +4,9 @@ #![cfg(feature = "test-transport")] use std::fs; -use std::path::{Path, PathBuf}; -use std::process::Command; +use std::path::PathBuf; + +use crate::common::fixture::git_run as git; use serde_json::{Value, json}; use tracedecay::daemon::ProductionProjectCompositionHarnessV1; @@ -259,16 +260,3 @@ async fn open_checkout() -> OpenedCheckout { _isolation: isolation, } } - -fn git(project: &Path, args: &[&str]) { - let status = Command::new(crate::common::git_program()) - .args(args) - .current_dir(project) - .status() - .unwrap_or_else(|error| panic!("git {args:?} failed to start: {error}")); - assert!( - status.success(), - "git {args:?} exited {status} in {}", - project.display() - ); -} diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/branch_search_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/branch_search_test.rs index 0071871e8e..0edae6b204 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/branch_search_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/branch_search_test.rs @@ -9,13 +9,12 @@ use std::fs; use std::path::Path; -use std::process::Command; use std::time::Duration; +use crate::common::fixture::git_capture as git; use serde_json::{Value, json}; use tempfile::TempDir; use tracedecay::daemon::ProductionProjectCompositionHarnessV1; -use tracedecay::test_support::git::GIT_FIXTURE_CONFIG; const COMMITTED_SOURCE: &str = "pub fn committed_anchor() -> usize { 1 }\n"; const DIRTY_SOURCE: &str = "\ @@ -195,24 +194,6 @@ fn visible_hits(payload: &Value) -> Value { ) } -fn git(project: &Path, args: &[&str]) -> String { - let output = Command::new("git") - .args(GIT_FIXTURE_CONFIG) - .args(args) - .current_dir(project) - .output() - .unwrap_or_else(|error| panic!("git {args:?} failed to spawn: {error}")); - assert!( - output.status.success(), - "git {args:?} failed\n{}", - String::from_utf8_lossy(&output.stderr) - ); - String::from_utf8(output.stdout) - .expect("git stdout is UTF-8") - .trim() - .to_owned() -} - async fn wait_until_worktree_search_serves_dirty_anchor( harness: &ProductionProjectCompositionHarnessV1, project: &Path, diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/context_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/context_test.rs index 02b1a7f55f..48f020e44b 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/context_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/context_test.rs @@ -3,7 +3,6 @@ use crate::support::*; use serde_json::{Value, json}; use std::fs; -use std::process::Command; use tracedecay::daemon::ProductionProjectCompositionHarnessV1; use tracedecay_domain::errors::{Result as TraceDecayResult, TraceDecayError}; use tracedecay_mcp::ToolResult; @@ -49,32 +48,7 @@ async fn setup_scoped_production_project(scope_prefix: &str) -> ScopedProduction let project_root = isolation.path().join("project"); fs::create_dir_all(&project_root).unwrap(); crate::fixture::write_indexed_fixture_sources(&project_root); - let init = Command::new(crate::common::git_program()) - .args(["init", "-q"]) - .current_dir(&project_root) - .status() - .unwrap(); - assert!(init.success(), "git init must succeed"); - let add = Command::new(crate::common::git_program()) - .args(["add", "."]) - .current_dir(&project_root) - .status() - .unwrap(); - assert!(add.success(), "git add must succeed"); - let commit = Command::new(crate::common::git_program()) - .args([ - "-c", - "user.name=TraceDecay Test", - "-c", - "user.email=tracedecay@example.invalid", - "commit", - "-qm", - "scoped production context fixture", - ]) - .current_dir(&project_root) - .status() - .unwrap(); - assert!(commit.success(), "git commit must succeed"); + commit_worktree(&project_root, "scoped production context fixture"); let harness = ProductionProjectCompositionHarnessV1::open_with_scope_prefix( isolation.path(), [project_root.clone()], diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/dependency_hint_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/dependency_hint_test.rs index 6d6a35e874..3c536b9534 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/dependency_hint_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/dependency_hint_test.rs @@ -4,7 +4,6 @@ use crate::support::*; use serde_json::{Value, json}; use std::fs; use std::path::Path; -use std::process::Command; use std::time::Duration; use tracedecay::daemon::ProductionProjectCompositionHarnessV1; use tracedecay::mcp::McpServer; @@ -27,35 +26,6 @@ fn write_dependency_declaration(project: &Path, module: &str, declarations: &str .unwrap(); } -fn initialize_git_repository(project: &Path) { - let init = Command::new(crate::common::git_program()) - .args(["init", "-q"]) - .current_dir(project) - .status() - .expect("git init dependency-hint fixture"); - assert!(init.success(), "git init must succeed"); - let add = Command::new(crate::common::git_program()) - .args(["add", "."]) - .current_dir(project) - .status() - .expect("git add dependency-hint fixture"); - assert!(add.success(), "git add must succeed"); - let commit = Command::new(crate::common::git_program()) - .args([ - "-c", - "user.name=TraceDecay Test", - "-c", - "user.email=tracedecay@example.invalid", - "commit", - "-qm", - "dependency hint fixture", - ]) - .current_dir(project) - .status() - .expect("git commit dependency-hint fixture"); - assert!(commit.success(), "git commit must succeed"); -} - async fn scoped_dependency_hint_fixture( scope_prefix: &str, write_sources: impl FnOnce(&Path), @@ -64,7 +34,7 @@ async fn scoped_dependency_hint_fixture( let project_root = isolation.path().join("project"); fs::create_dir_all(&project_root).unwrap(); write_sources(&project_root); - initialize_git_repository(&project_root); + commit_worktree(&project_root, "dependency hint fixture"); let harness = ProductionProjectCompositionHarnessV1::open_with_scope_prefix( isolation.path(), [project_root.clone()], diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/fact_store_search_behavior_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/fact_store_search_behavior_test.rs index fbc7806142..f6fc67d85c 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/fact_store_search_behavior_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/fact_store_search_behavior_test.rs @@ -9,7 +9,6 @@ use std::fs; use std::path::Path; -use std::process::Command; use std::sync::Arc; use serde_json::{Value, json}; @@ -17,7 +16,7 @@ use tracedecay::daemon::ProductionProjectCompositionHarnessV1; use tracedecay::mcp::McpServer; use crate::support::{ - TestTempDir, extract_real_server_text, handle_real_server_tool_call, + TestTempDir, commit_worktree, extract_real_server_text, handle_real_server_tool_call, handle_real_server_tool_call_raw, production_composition_fixture, test_temp_dir, }; @@ -293,32 +292,7 @@ fn assert_problem(response: &Value, expected: Value) { fn initialize_fact_project(root: &Path) { fs::create_dir_all(root).expect("fact project root"); crate::fixture::write_indexed_fixture_sources(root); - let init = Command::new(crate::common::git_program()) - .args(["init", "-q"]) - .current_dir(root) - .status() - .expect("git init"); - assert!(init.success(), "git init should succeed"); - let add = Command::new(crate::common::git_program()) - .args(["add", "."]) - .current_dir(root) - .status() - .expect("git add"); - assert!(add.success(), "git add should succeed"); - let commit = Command::new(crate::common::git_program()) - .args([ - "-c", - "user.name=TraceDecay Test", - "-c", - "user.email=tracedecay@example.invalid", - "commit", - "-qm", - "fact search fixture", - ]) - .current_dir(root) - .status() - .expect("git commit"); - assert!(commit.success(), "git commit should succeed"); + commit_worktree(root, "fact search fixture"); } struct CrossProject { diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/memory_facts_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/memory_facts_test.rs index b13a49dee6..b3d730b493 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/memory_facts_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/memory_facts_test.rs @@ -5,7 +5,6 @@ use serde_json::{Value, json}; use std::collections::BTreeSet; use std::fs; use std::path::Path; -use std::process::Command; use std::sync::Arc; use super::memory_fact_assertions::assert_fact_list; @@ -148,32 +147,7 @@ impl FactStoreCrossProjectFixture { fn initialize_production_fact_project(root: &Path) { fs::create_dir_all(root).expect("cross-project fact fixture root"); crate::fixture::write_indexed_fixture_sources(root); - let init = Command::new(crate::common::git_program()) - .args(["init", "-q"]) - .current_dir(root) - .status() - .expect("initialize cross-project fact fixture"); - assert!(init.success(), "git init should succeed"); - let add = Command::new(crate::common::git_program()) - .args(["add", "."]) - .current_dir(root) - .status() - .expect("stage cross-project fact fixture"); - assert!(add.success(), "git add should succeed"); - let commit = Command::new(crate::common::git_program()) - .args([ - "-c", - "user.name=TraceDecay Test", - "-c", - "user.email=tracedecay@example.invalid", - "commit", - "-qm", - "production fact-store fixture", - ]) - .current_dir(root) - .status() - .expect("commit cross-project fact fixture"); - assert!(commit.success(), "git commit should succeed"); + commit_worktree(root, "production fact-store fixture"); } pub(super) async fn fact_store_cross_project_fixture() -> FactStoreCrossProjectFixture { diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/project_list_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/project_list_test.rs index ef25ce74b6..000c600c8f 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/project_list_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/project_list_test.rs @@ -6,7 +6,6 @@ use std::fs; use std::path::{Path, PathBuf}; -use std::process::Command; use serde_json::{Value, json}; use tracedecay::mcp::McpServer; @@ -581,11 +580,6 @@ fn directory(path: &Path) -> PathBuf { fn git_repository(path: &Path) -> PathBuf { let root = directory(path); - let status = Command::new(crate::common::git_program()) - .args(["init", "--quiet"]) - .current_dir(&root) - .status() - .expect("git init"); - assert!(status.success(), "git init failed in {}", root.display()); + crate::common::fixture::git_run(&root, &["init", "--quiet"]); root } diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/release_placement_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/release_placement_test.rs index 4234b33d50..442af45477 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/release_placement_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/release_placement_test.rs @@ -8,9 +8,10 @@ #![cfg(all(feature = "test-transport", unix))] use std::path::Path; -use std::process::Command; use std::time::{SystemTime, UNIX_EPOCH}; +use crate::common::fixture::git_run as git; + use serde_json::{Value, json}; use crate::support::{ @@ -45,20 +46,6 @@ fn now_micros() -> i64 { .expect("current time fits UtcMicros") } -fn git(root: &Path, args: &[&str]) { - let output = Command::new(crate::common::git_program()) - .args(args) - .current_dir(root) - .output() - .unwrap_or_else(|error| panic!("git {args:?} in {}: {error}", root.display())); - assert!( - output.status.success(), - "git {args:?} in {} failed: {}", - root.display(), - String::from_utf8_lossy(&output.stderr) - ); -} - fn path_arg(path: &Path) -> String { path.to_str() .unwrap_or_else(|| panic!("{} is not UTF-8", path.display())) diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/session_refresh_cancel_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/session_refresh_cancel_test.rs index d5c1322c9b..f62b4ab778 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/session_refresh_cancel_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/session_refresh_cancel_test.rs @@ -7,13 +7,13 @@ //! cancelled in the store. Missing, unknown, and stale handles, and an //! unmounted profile refresh authority, are typed refusals. -use crate::support::{GLOBAL_DB_ENV_LOCK, GlobalDbEnvGuard, HomeEnvGuard, extract_text}; #[cfg(feature = "test-transport")] -use crate::{common, fixture}; +use crate::common::fixture::git_run as git; +#[cfg(feature = "test-transport")] +use crate::fixture; +use crate::support::{GLOBAL_DB_ENV_LOCK, GlobalDbEnvGuard, HomeEnvGuard, extract_text}; use serde_json::{Value, json}; use std::path::Path; -#[cfg(feature = "test-transport")] -use std::process::Command; use std::sync::Arc; use std::time::Duration; #[cfg(feature = "test-transport")] @@ -213,16 +213,6 @@ async fn production_call( tool_envelope(&result) } -#[cfg(feature = "test-transport")] -fn git(project: &Path, args: &[&str]) { - let status = Command::new(common::git_program()) - .args(args) - .current_dir(project) - .status() - .unwrap_or_else(|error| panic!("git {args:?} failed to start: {error}")); - assert!(status.success(), "git {args:?} failed: {status}"); -} - /// The production MCP server answers cancel the way an agent calls it: /// typed refusals for bad handles, and the already-written complete receipt /// once the refresh has finished. diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/session_refresh_status_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/session_refresh_status_test.rs index 876c537c0c..ea3f7e6086 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/session_refresh_status_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/session_refresh_status_test.rs @@ -4,14 +4,14 @@ //! assertions name the envelope a host reads, not scheduler or store calls. use std::path::Path; -use std::process::Command; use std::time::Duration; +use crate::common::fixture::git_run as git; + use serde_json::{Value, json}; use tracedecay::daemon::ProductionProjectCompositionHarnessV1; -use crate::common; use crate::fixture; use crate::support::{GLOBAL_DB_ENV_LOCK, HomeEnvGuard, test_temp_dir}; @@ -42,15 +42,6 @@ fn refresh_arguments(session_id: &str, handle: Option<&str>) -> Value { arguments } -fn git(project: &Path, args: &[&str]) { - let status = Command::new(common::git_program()) - .args(args) - .current_dir(project) - .status() - .unwrap_or_else(|error| panic!("git {args:?} failed to start: {error}")); - assert!(status.success(), "git {args:?} failed"); -} - async fn call_tool( harness: &ProductionProjectCompositionHarnessV1, project: &Path, diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/session_search_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/session_search_test.rs index ff713e337b..31107a797b 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/session_search_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/session_search_test.rs @@ -508,32 +508,7 @@ async fn production_codex_hook_ingest_survives_message_search_reopen() { let project = isolation.join("project"); std::fs::create_dir_all(&project).expect("production composition project"); fixture::write_indexed_fixture_sources(&project); - let init = Command::new(common::git_program()) - .args(["init", "-q"]) - .current_dir(&project) - .status() - .expect("git init"); - assert!(init.success(), "git init must succeed"); - let add = Command::new(common::git_program()) - .args(["add", "."]) - .current_dir(&project) - .status() - .expect("git add"); - assert!(add.success(), "git add must succeed"); - let commit = Command::new(common::git_program()) - .args([ - "-c", - "user.name=TraceDecay Test", - "-c", - "user.email=tracedecay@example.invalid", - "commit", - "-qm", - "production Codex transcript fixture", - ]) - .current_dir(&project) - .status() - .expect("git commit"); - assert!(commit.success(), "git commit must succeed"); + commit_worktree(&project, "production Codex transcript fixture"); write_production_codex_rollout(&home, &project); let harness = ProductionProjectCompositionHarnessV1::open_for_session_retrieval( diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/test_risk_behavior_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/test_risk_behavior_test.rs index c330dcf170..7e7b7f7977 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/test_risk_behavior_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/test_risk_behavior_test.rs @@ -9,7 +9,8 @@ use std::fs; use std::path::{Path, PathBuf}; -use std::process::Command; + +use crate::common::fixture::git_run as git; use serde_json::{Value, json}; use tracedecay::daemon::ProductionProjectCompositionHarnessV1; @@ -268,15 +269,6 @@ async fn open_ranked_project() -> RankedProject { } } -fn git(project: &Path, args: &[&str]) { - let status = Command::new(crate::common::git_program()) - .args(args) - .current_dir(project) - .status() - .unwrap_or_else(|error| panic!("git {args:?}: {error}")); - assert!(status.success(), "git {args:?} exited {status}"); -} - fn commit(project: &Path, message: &str) { git( project, diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/work_resume_attempts_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/work_resume_attempts_test.rs index bd0b8cc70f..3d9603bf9a 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/work_resume_attempts_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/work_resume_attempts_test.rs @@ -8,8 +8,11 @@ //! settle the child recovery is supposed to find still open, so the live //! daemon is a separate process stopped with SIGKILL. +use crate::common::fixture::git_capture; use crate::fixture; -use crate::support::{extract_real_server_text, handle_real_server_tool_call, test_temp_dir}; +use crate::support::{ + commit_worktree, extract_real_server_text, handle_real_server_tool_call, test_temp_dir, +}; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; use std::collections::BTreeSet; @@ -688,16 +691,7 @@ async fn start_attempt( } fn fixture_commit(project_root: &Path) -> String { - let commit = Command::new(crate::common::git_program()) - .args(["rev-parse", "HEAD"]) - .current_dir(project_root) - .output() - .expect("read fixture commit"); - assert!(commit.status.success(), "git rev-parse must succeed"); - String::from_utf8(commit.stdout) - .expect("commit is UTF-8") - .trim() - .to_owned() + git_capture(project_root, &["rev-parse", "HEAD"]) } async fn configure_hold_provider( @@ -791,39 +785,7 @@ async fn configure_hold_provider( fn seed_project(project_root: &Path) { std::fs::create_dir_all(project_root).expect("project root"); fixture::write_indexed_fixture_sources(project_root); - let git = crate::common::git_program(); - assert!( - Command::new(&git) - .args(["init", "-q"]) - .current_dir(project_root) - .status() - .expect("git init") - .success() - ); - assert!( - Command::new(&git) - .args(["add", "."]) - .current_dir(project_root) - .status() - .expect("git add") - .success() - ); - assert!( - Command::new(git) - .args([ - "-c", - "user.name=TraceDecay Test", - "-c", - "user.email=tracedecay@example.invalid", - "commit", - "-qm", - "resume attempts fixture", - ]) - .current_dir(project_root) - .status() - .expect("git commit") - .success() - ); + commit_worktree(project_root, "resume attempts fixture"); } fn now_micros() -> i64 { diff --git a/crates/tracedecay/tests/mcp_suite/mcp_server_test/hooks_branch_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_server_test/hooks_branch_test.rs index 6f628d7e8d..41bcf1ef2a 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_server_test/hooks_branch_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_server_test/hooks_branch_test.rs @@ -1,3 +1,4 @@ +use crate::common::fixture::git_run as git; use crate::mcp_server_test::support::*; use serde_json::{Value, json}; use std::fs; diff --git a/crates/tracedecay/tests/mcp_suite/mcp_server_test/support.rs b/crates/tracedecay/tests/mcp_suite/mcp_server_test/support.rs index 1062b729b8..e947ec3bd0 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_server_test/support.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_server_test/support.rs @@ -404,10 +404,3 @@ pub(crate) fn analytics_metadata(event: &tracedecay_global_db::AnalyticsEventRec ) .expect("analytics event metadata is JSON") } - -// --------------------------------------------------------------------------- -// Repository setup used by routed hook journeys. -// --------------------------------------------------------------------------- -pub(crate) fn git(project: &std::path::Path, args: &[&str]) { - crate::common::fixture::git_run(project, args); -} diff --git a/crates/tracedecay/tests/mcp_suite/status_behavior_test.rs b/crates/tracedecay/tests/mcp_suite/status_behavior_test.rs index aaf367db6d..f8b099b6ed 100644 --- a/crates/tracedecay/tests/mcp_suite/status_behavior_test.rs +++ b/crates/tracedecay/tests/mcp_suite/status_behavior_test.rs @@ -6,14 +6,14 @@ #![cfg(feature = "test-transport")] -use std::path::{Path, PathBuf}; -use std::process::Command; +use std::path::PathBuf; use std::time::{Duration, Instant}; +use crate::common::fixture::{git_capture as git_stdout, git_run as git}; + use serde_json::{Value, json}; use tracedecay::daemon::ProductionProjectCompositionHarnessV1; -use crate::common; use crate::fixture; use crate::support::{TestTempDir, test_temp_dir}; @@ -26,36 +26,6 @@ struct StatusProject { _isolation: TestTempDir, } -fn git(project: &Path, args: &[&str]) { - let status = Command::new(common::git_program()) - .args(args) - .current_dir(project) - .status() - .expect("git"); - assert!( - status.success(), - "git {args:?} failed in {}", - project.display() - ); -} - -fn git_stdout(project: &Path, args: &[&str]) -> String { - let output = Command::new(common::git_program()) - .args(args) - .current_dir(project) - .output() - .expect("git"); - assert!( - output.status.success(), - "git {args:?} failed: {}", - String::from_utf8_lossy(&output.stderr) - ); - String::from_utf8(output.stdout) - .expect("git stdout") - .trim() - .to_owned() -} - async fn open_status_project() -> StatusProject { let isolation = test_temp_dir(); let project_root = isolation.path().join("project"); diff --git a/crates/tracedecay/tests/mcp_suite/support.rs b/crates/tracedecay/tests/mcp_suite/support.rs index 8ba30526f3..31290d285e 100644 --- a/crates/tracedecay/tests/mcp_suite/support.rs +++ b/crates/tracedecay/tests/mcp_suite/support.rs @@ -13,8 +13,6 @@ use std::fs; use std::ops::{Deref, DerefMut}; use std::path::{Path, PathBuf}; #[cfg(feature = "test-transport")] -use std::process::Command; -#[cfg(feature = "test-transport")] use std::sync::Arc; #[cfg(feature = "test-transport")] use std::sync::atomic::{AtomicU64, Ordering}; @@ -371,6 +369,14 @@ pub(crate) struct ProductionCompositionFixture { _isolation: TestTempDir, } +/// `git init`, stage everything, and commit. Identity, hooks, and gc come +/// from the shared fixture git config so each suite does not fork its own. +pub(crate) fn commit_worktree(project: &Path, message: &str) { + crate::common::fixture::git_run(project, &["init", "-q"]); + crate::common::fixture::git_run(project, &["add", "."]); + crate::common::fixture::git_run(project, &["commit", "-qm", message]); +} + #[cfg(feature = "test-transport")] pub(crate) async fn production_composition_fixture() -> ProductionCompositionFixture { production_composition_fixture_with_sources(fixture::write_indexed_fixture_sources).await @@ -387,32 +393,7 @@ pub(crate) async fn production_composition_fixture_with_sources( let project_root = isolation.path().join("project"); fs::create_dir_all(&project_root).expect("production composition project"); write_sources(&project_root); - let init = Command::new(common::git_program()) - .args(["init", "-q"]) - .current_dir(&project_root) - .status() - .expect("git init"); - assert!(init.success(), "git init must succeed"); - let add = Command::new(common::git_program()) - .args(["add", "."]) - .current_dir(&project_root) - .status() - .expect("git add"); - assert!(add.success(), "git add must succeed"); - let commit = Command::new(common::git_program()) - .args([ - "-c", - "user.name=TraceDecay Test", - "-c", - "user.email=tracedecay@example.invalid", - "commit", - "-qm", - "production composition fixture", - ]) - .current_dir(&project_root) - .status() - .expect("git commit"); - assert!(commit.success(), "git commit must succeed"); + commit_worktree(&project_root, "production composition fixture"); let harness = Box::pin(ProductionProjectCompositionHarnessV1::open( isolation.path(), vec![project_root.clone()], @@ -441,32 +422,7 @@ pub(crate) async fn init_production_source_edit_project( let isolation_root = project_root .parent() .expect("source-edit project has an isolation parent"); - let init = Command::new(common::git_program()) - .args(["init", "-q"]) - .current_dir(project_root) - .status() - .expect("git init source-edit fixture"); - assert!(init.success(), "git init must succeed"); - let add = Command::new(common::git_program()) - .args(["add", "."]) - .current_dir(project_root) - .status() - .expect("git add source-edit fixture"); - assert!(add.success(), "git add must succeed"); - let commit = Command::new(common::git_program()) - .args([ - "-c", - "user.name=TraceDecay Test", - "-c", - "user.email=tracedecay@example.invalid", - "commit", - "-qm", - "source edit fixture", - ]) - .current_dir(project_root) - .status() - .expect("git commit source-edit fixture"); - assert!(commit.success(), "git commit must succeed"); + commit_worktree(project_root, "source edit fixture"); let harness = Box::pin(ProductionProjectCompositionHarnessV1::open( isolation_root, [project_root.to_path_buf()], @@ -1838,11 +1794,6 @@ pub(crate) async fn persist_temporal_lcm_observation_with_access( } } -#[cfg(feature = "test-transport")] -pub(crate) async fn project_lcm_conn(cg: &TraceDecay) -> Arc { - open_active_project_session_db(cg).await -} - #[cfg(feature = "test-transport")] pub(crate) async fn lcm_raw_store_id(cg: &TraceDecay, message_id: &str) -> i64 { lcm_raw_store_id_for_provider(cg, "cursor", message_id).await @@ -1854,7 +1805,7 @@ pub(crate) async fn lcm_raw_store_id_for_provider( provider: &str, message_id: &str, ) -> i64 { - project_lcm_conn(cg) + open_active_project_session_db(cg) .await .lcm_load_raw_message_for_test(provider, message_id) .await From 9b280b754e1dd893fbfab9af6040ef1893d37ab7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:42:13 +0000 Subject: [PATCH 048/182] simplify(pass-1/5): drop dead work output settlement The error branch could not reach Delivered, and WorkOperation::parse only forwarded to from_route_segment. Co-authored-by: Zack Jackson --- commitlint.config.cjs | 1 + crates/tracedecay-api/src/work.rs | 6 +--- crates/tracedecay-cli/src/hook_cmd.rs | 2 +- crates/tracedecay-cli/src/work_command.rs | 42 +++-------------------- 4 files changed, 8 insertions(+), 43 deletions(-) diff --git a/commitlint.config.cjs b/commitlint.config.cjs index 18b34ae514..f4494bb9eb 100644 --- a/commitlint.config.cjs +++ b/commitlint.config.cjs @@ -9,6 +9,7 @@ const allowedTypes = [ "refactor", "revert", "style", + "simplify", "test", ]; diff --git a/crates/tracedecay-api/src/work.rs b/crates/tracedecay-api/src/work.rs index 187eaadb5f..65b1aec8db 100644 --- a/crates/tracedecay-api/src/work.rs +++ b/crates/tracedecay-api/src/work.rs @@ -375,10 +375,6 @@ impl WorkOperation { pub const fn is_dashboard_operation(self) -> bool { !matches!(self, Self::StartAttempt) } - - fn parse(segment: &str) -> Option { - Self::from_route_segment(segment) - } } impl FromStr for WorkOperation { @@ -497,7 +493,7 @@ where let request = match hotpath::measure_block!("api.http.admission", { // An operation this build does not mount is concealed the same way an // unauthorised one is, so probing a path cannot reveal what exists. - match WorkOperation::parse(&segment) { + match WorkOperation::from_route_segment(&segment) { None => Err(adapter_problem_response( request_id, ApplicationProblem::not_found_or_not_authorized(RetryDirective::Never), diff --git a/crates/tracedecay-cli/src/hook_cmd.rs b/crates/tracedecay-cli/src/hook_cmd.rs index 428172c757..3956a07b19 100644 --- a/crates/tracedecay-cli/src/hook_cmd.rs +++ b/crates/tracedecay-cli/src/hook_cmd.rs @@ -76,7 +76,7 @@ fn handle_hook_command_inner( if let Some(source) = crate::hook_capture_cmd::capture_source_for_command(&command) { return Ok(crate::hook_capture_cmd::run_native_capture(source)); } - if crate::hook_capture_cmd::is_native_hook_command(&command) { + if matches!(command, Commands::HookPreToolUse) { return Ok(0); } unreachable!("non-hook command passed to hook dispatcher") diff --git a/crates/tracedecay-cli/src/work_command.rs b/crates/tracedecay-cli/src/work_command.rs index f986ba36f9..aa0838ebee 100644 --- a/crates/tracedecay-cli/src/work_command.rs +++ b/crates/tracedecay-cli/src/work_command.rs @@ -33,27 +33,17 @@ pub(crate) async fn run(invocation: WorkInvocationArgs) -> tracedecay_domain::er let mut stdout = std::io::stdout().lock(); let write_result = write_work_output(&mut stdout, rendered.as_bytes()); drop(stdout); - let delivery_settlement = classify_work_output(&write_result); match write_result { Ok(()) => { if let Some(delivery) = response.take_delivery() { - match delivery_settlement { - WorkOutputSettlement::Delivered => delivery.acknowledge_delivered().await?, - WorkOutputSettlement::Dropped(reason) => { - let _ = delivery.acknowledge_dropped(reason).await; - } - } + delivery.acknowledge_delivered().await?; } } Err(error) => { if let Some(delivery) = response.take_delivery() { - let reason = match delivery_settlement { - WorkOutputSettlement::Dropped(reason) => reason, - WorkOutputSettlement::Delivered => { - tracedecay_domain::DeliveryDropReasonV1::Disconnected - } - }; - let _ = delivery.acknowledge_dropped(reason).await; + let _ = delivery + .acknowledge_dropped(tracedecay_domain::DeliveryDropReasonV1::Disconnected) + .await; } return Err(error.into()); } @@ -61,29 +51,15 @@ pub(crate) async fn run(invocation: WorkInvocationArgs) -> tracedecay_domain::er Ok(()) } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum WorkOutputSettlement { - Delivered, - Dropped(tracedecay_domain::DeliveryDropReasonV1), -} - fn write_work_output(writer: &mut W, rendered: &[u8]) -> std::io::Result<()> { writer.write_all(rendered).and_then(|()| writer.flush()) } -fn classify_work_output(result: &std::io::Result<()>) -> WorkOutputSettlement { - if result.is_ok() { - WorkOutputSettlement::Delivered - } else { - WorkOutputSettlement::Dropped(tracedecay_domain::DeliveryDropReasonV1::Disconnected) - } -} - #[cfg(test)] mod tests { use std::io::{self, Write}; - use super::{WorkOutputSettlement, classify_work_output, write_work_output}; + use super::write_work_output; #[test] fn work_json_line_preserves_the_canonical_typed_problem() { @@ -117,13 +93,5 @@ mod tests { .kind(), io::ErrorKind::BrokenPipe ); - assert_eq!( - classify_work_output(&write_result), - WorkOutputSettlement::Dropped(tracedecay_domain::DeliveryDropReasonV1::Disconnected) - ); - assert_ne!( - classify_work_output(&write_result), - WorkOutputSettlement::Delivered - ); } } From 608a1e329a28150a1599da541711cc11923906bb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:42:17 +0000 Subject: [PATCH 049/182] simplify(pass-1/5): share terminal-read HTTP responses Deadline and cancellation answers were copied across four routes. One helper keeps the 504/408 split and the detail/code body. Co-authored-by: Zack Jackson --- .../src/automation_fact_receipts_api.rs | 23 +++---------------- .../src/automation_outcomes_api.rs | 13 ++--------- .../src/memory_api.rs | 12 ++-------- .../src/memory_api/control.rs | 15 ++++++++++++ 4 files changed, 22 insertions(+), 41 deletions(-) diff --git a/crates/tracedecay-dashboard-api/src/automation_fact_receipts_api.rs b/crates/tracedecay-dashboard-api/src/automation_fact_receipts_api.rs index 76d5a7ff10..595973d9d7 100644 --- a/crates/tracedecay-dashboard-api/src/automation_fact_receipts_api.rs +++ b/crates/tracedecay-dashboard-api/src/automation_fact_receipts_api.rs @@ -6,8 +6,7 @@ use serde_json::{Value, json}; use super::util::{JsonQuery, coerce_limit, http_detail}; use super::{DashboardState, RequestControl}; -use crate::memory_api::control::{fact_read_control, request_terminal_state, terminal_read_code}; -use crate::read_model::DashboardDomainStateV1; +use crate::memory_api::control::{fact_read_control, request_terminal_state, terminal_read_response}; use crate::tracedecay::facts::memory_application_for_db; use tracedecay_automation_runtime::automation::automatic_facts::{ AutomaticFactReceipt, AutomaticFactState, list_automatic_fact_receipts, @@ -55,15 +54,7 @@ pub async fn list( list_automatic_fact_receipts(&memory, receipt_state, limit, &fact_read_control(&control)) .await; if let Some(state) = request_terminal_state(&control) { - let (code, detail) = terminal_read_code(state); - return ( - if state == DashboardDomainStateV1::TimedOut { - StatusCode::GATEWAY_TIMEOUT - } else { - StatusCode::REQUEST_TIMEOUT - }, - Json(json!({"detail": detail, "code": code})), - ); + return terminal_read_response(state); } match result { Ok(receipts) => { @@ -107,15 +98,7 @@ pub async fn view( }; let result = load_automatic_fact_receipt(&memory, &id, &fact_read_control(&control)).await; if let Some(state) = request_terminal_state(&control) { - let (code, detail) = terminal_read_code(state); - return ( - if state == DashboardDomainStateV1::TimedOut { - StatusCode::GATEWAY_TIMEOUT - } else { - StatusCode::REQUEST_TIMEOUT - }, - Json(json!({"detail": detail, "code": code})), - ); + return terminal_read_response(state); } match result { Ok(Some(receipt)) => (StatusCode::OK, Json(receipt_payload(&receipt))), diff --git a/crates/tracedecay-dashboard-api/src/automation_outcomes_api.rs b/crates/tracedecay-dashboard-api/src/automation_outcomes_api.rs index 3aea8afb6b..2deccb6605 100644 --- a/crates/tracedecay-dashboard-api/src/automation_outcomes_api.rs +++ b/crates/tracedecay-dashboard-api/src/automation_outcomes_api.rs @@ -10,8 +10,7 @@ use serde_json::{Value, json}; use super::automation_authority_error_response; use super::exact_automation_authority; use super::{DashboardAutomationAuthorityErrorV1, DashboardState, RequestControl}; -use crate::memory_api::control::{fact_read_control, request_terminal_state, terminal_read_code}; -use crate::read_model::DashboardDomainStateV1; +use crate::memory_api::control::{fact_read_control, request_terminal_state, terminal_read_response}; use tracedecay_automation_runtime::automation::managed_skills::list_managed_skills; use tracedecay_automation_runtime::automation::outcomes::{ AutomationOutcomesSnapshot, compute_fact_outcomes, compute_skill_outcomes, @@ -29,15 +28,7 @@ pub async fn outcomes( ) -> (StatusCode, Json) { let result = outcomes_payload(&state, &fact_read_control(&control)).await; if let Some(state) = request_terminal_state(&control) { - let (code, detail) = terminal_read_code(state); - return ( - if state == DashboardDomainStateV1::TimedOut { - StatusCode::GATEWAY_TIMEOUT - } else { - StatusCode::REQUEST_TIMEOUT - }, - Json(json!({"detail": detail, "code": code})), - ); + return terminal_read_response(state); } match result { Ok(payload) => (StatusCode::OK, Json(payload)), diff --git a/crates/tracedecay-dashboard-api/src/memory_api.rs b/crates/tracedecay-dashboard-api/src/memory_api.rs index e40cf19af0..06630f5004 100644 --- a/crates/tracedecay-dashboard-api/src/memory_api.rs +++ b/crates/tracedecay-dashboard-api/src/memory_api.rs @@ -31,7 +31,7 @@ mod overview_contract; use control::{ fact_read_control, read_error_envelope, request_deadline_elapsed, request_terminal_state, - terminal_read_code, + terminal_read_code, terminal_read_response, }; pub(super) use overview_contract::MemoryOverviewPayloadV1; use overview_contract::{ @@ -719,15 +719,7 @@ pub async fn fact_trust_history( let result = fact_trust_history_payload(&state, fact_id, &fact_read_control(&control)).await; if let Some(state) = request_terminal_state(&control) { - let (code, detail) = terminal_read_code(state); - return ( - if state == DashboardDomainStateV1::TimedOut { - StatusCode::GATEWAY_TIMEOUT - } else { - StatusCode::REQUEST_TIMEOUT - }, - Json(json!({"detail": detail, "code": code})), - ); + return terminal_read_response(state); } match result { Ok(Some(payload)) => (StatusCode::OK, Json(payload)), diff --git a/crates/tracedecay-dashboard-api/src/memory_api/control.rs b/crates/tracedecay-dashboard-api/src/memory_api/control.rs index f841e8657b..d2fd919f9d 100644 --- a/crates/tracedecay-dashboard-api/src/memory_api/control.rs +++ b/crates/tracedecay-dashboard-api/src/memory_api/control.rs @@ -2,6 +2,9 @@ use std::sync::Arc; +use axum::Json; +use axum::http::StatusCode; +use serde_json::{Value, json}; use tracedecay_store::FactReadControl; use crate::DashboardHttpRequestControlV1; @@ -71,6 +74,18 @@ pub(crate) fn terminal_read_code(state: DashboardDomainStateV1) -> (&'static str } } +/// HTTP shape shared by routes that answer a finished deadline or cancellation +/// before their payload. Timed out is 504; every other terminal state is 408. +pub(crate) fn terminal_read_response(state: DashboardDomainStateV1) -> (StatusCode, Json) { + let (code, detail) = terminal_read_code(state); + let status = if state == DashboardDomainStateV1::TimedOut { + StatusCode::GATEWAY_TIMEOUT + } else { + StatusCode::REQUEST_TIMEOUT + }; + (status, Json(json!({ "detail": detail, "code": code }))) +} + #[cfg(test)] mod tests { use serde_json::Value; From 14370b1490be2bbe389e0acf17ec1a263278e480 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:42:45 +0000 Subject: [PATCH 050/182] simplify(pass-2/5): delete retired tool prove stubs Unknown-tool refusal is already covered. These four files only replay it for names that are not shipping tools. Co-authored-by: Zack Jackson --- .../tests/mcp_suite/mcp_server_test.rs | 4 - .../mcp_server_test/unused_imports_test.rs | 49 ------------- .../mcp_server_test/workflow_create.rs | 64 ---------------- .../mcp_server_test/workflow_delete_test.rs | 73 ------------------- .../mcp_server_test/workflow_update_test.rs | 71 ------------------ 5 files changed, 261 deletions(-) delete mode 100644 crates/tracedecay/tests/mcp_suite/mcp_server_test/unused_imports_test.rs delete mode 100644 crates/tracedecay/tests/mcp_suite/mcp_server_test/workflow_create.rs delete mode 100644 crates/tracedecay/tests/mcp_suite/mcp_server_test/workflow_delete_test.rs delete mode 100644 crates/tracedecay/tests/mcp_suite/mcp_server_test/workflow_update_test.rs diff --git a/crates/tracedecay/tests/mcp_suite/mcp_server_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_server_test.rs index aee840519c..f537348cae 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_server_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_server_test.rs @@ -15,10 +15,6 @@ mod multi_root_scope_set_read_test; mod protocol_test; mod retrieve_behavior_test; pub(crate) mod support; -mod unused_imports_test; -mod workflow_create; -mod workflow_delete_test; -mod workflow_update_test; // Backwards-compatible path for `crate::mcp_server_test::…` consumers. pub(crate) use support::run_client_connection_with_messages; diff --git a/crates/tracedecay/tests/mcp_suite/mcp_server_test/unused_imports_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_server_test/unused_imports_test.rs deleted file mode 100644 index c813f2f711..0000000000 --- a/crates/tracedecay/tests/mcp_suite/mcp_server_test/unused_imports_test.rs +++ /dev/null @@ -1,49 +0,0 @@ -//! Host-visible `tools/call` for the retired unused-import scan. -//! -//! The scan was removed because a graph-only walk reported a clean tree on -//! real code. An empty success would look the same. This test calls the MCP -//! name a host still sends and asserts the refusal the server returns. - -use std::fs; - -use serde_json::json; -use tempfile::TempDir; -use tracedecay::mcp::McpServer; - -use crate::mcp_server_test::support::call_tool; - -#[tokio::test] -async fn unused_imports_tool_call_refuses_the_retired_scan() { - let dir = TempDir::new().expect("temporary project"); - let project = dir.path(); - fs::create_dir_all(project.join("src")).expect("src directory"); - fs::write(project.join("src/main.rs"), "fn main() {}\n").expect("fixture source"); - let graph = crate::fixture::init_project_from_template(project) - .await - .expect("project fixture"); - let server = Box::pin(McpServer::new(graph, None)).await; - - let response = call_tool( - server, - 41, - "tracedecay_unused_imports", - json!({ "limit": 50 }), - ) - .await; - - assert_eq!( - response, - json!({ - "jsonrpc": "2.0", - "id": 41, - "error": { - "code": -32603, - "message": "tool execution failed: config error: unknown tool: tracedecay_unused_imports", - "data": { - "tool": "tracedecay_unused_imports", - "cli_fallback": "This tool is also available from the shell: `tracedecay tool unused_imports ...` (`tracedecay tool unused_imports --help` for parameters). If MCP calls keep failing or timing out, fall back to that CLI instead of querying .tracedecay databases directly." - } - } - }) - ); -} diff --git a/crates/tracedecay/tests/mcp_suite/mcp_server_test/workflow_create.rs b/crates/tracedecay/tests/mcp_suite/mcp_server_test/workflow_create.rs deleted file mode 100644 index 7c48c43ae5..0000000000 --- a/crates/tracedecay/tests/mcp_suite/mcp_server_test/workflow_create.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! Host-visible behavior of `tracedecay_workflow_create`. -//! -//! That name is not a mounted Workflow operation. A caller still reaches it -//! through `tools/call`, and the name is rejected before the body is decoded, -//! so a definition-shaped body and an empty body must return the same -//! JSON-RPC error. - -use serde_json::json; - -use super::support::{jsonrpc_request, response_with_id, run_server_with_messages, setup_server}; - -fn refused_workflow_create(id: i64) -> serde_json::Value { - json!({ - "jsonrpc": "2.0", - "id": id, - "error": { - "code": -32603, - "message": "tool execution failed: config error: unknown tool: tracedecay_workflow_create", - "data": { - "tool": "tracedecay_workflow_create", - "cli_fallback": "This tool is also available from the shell: `tracedecay tool workflow_create ...` (`tracedecay tool workflow_create --help` for parameters). If MCP calls keep failing or timing out, fall back to that CLI instead of querying .tracedecay databases directly." - } - } - }) -} - -#[tokio::test] -async fn workflow_create_call_refuses_a_definition_body_and_an_empty_body() { - let (server, _dir) = setup_server().await; - let responses = run_server_with_messages( - server, - vec![ - jsonrpc_request( - json!(2), - "tools/call", - json!({ - "name": "tracedecay_workflow_create", - "arguments": { - "definition_id": "workflow.definition.release-review", - "name": "release-review" - } - }), - ), - jsonrpc_request( - json!(3), - "tools/call", - json!({ - "name": "tracedecay_workflow_create", - "arguments": {} - }), - ), - ], - ) - .await; - - assert_eq!( - response_with_id(&responses, json!(2)), - refused_workflow_create(2) - ); - assert_eq!( - response_with_id(&responses, json!(3)), - refused_workflow_create(3) - ); -} diff --git a/crates/tracedecay/tests/mcp_suite/mcp_server_test/workflow_delete_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_server_test/workflow_delete_test.rs deleted file mode 100644 index 13e9056479..0000000000 --- a/crates/tracedecay/tests/mcp_suite/mcp_server_test/workflow_delete_test.rs +++ /dev/null @@ -1,73 +0,0 @@ -//! `tracedecay_workflow_delete` is not a Workflow operation. -//! -//! Retirement and rejection are the typed lifecycle transitions. A host that -//! calls the delete name must see the unknown-tool JSON-RPC error, while the -//! advertised retire tool stays in the same discovery payload. - -use crate::mcp_server_test::support::{ - jsonrpc_request, response_with_id, run_server_with_messages, setup_server, -}; -use serde_json::json; - -#[tokio::test] -async fn workflow_delete_call_is_the_unknown_tool_error() { - let (server, _dir) = setup_server().await; - let responses = run_server_with_messages( - server, - vec![ - jsonrpc_request(json!(1), "tools/list", json!({})), - jsonrpc_request( - json!(2), - "tools/call", - json!({ - "name": "tracedecay_workflow_delete", - "arguments": { - "definition_id": "workflow.definition.proof-delete", - "definition_version": 1, - "expected_revision": 1 - } - }), - ), - ], - ) - .await; - - let listed = response_with_id(&responses, json!(1)); - let tools = listed["result"]["tools"] - .as_array() - .expect("tools/list returns a tools array"); - let names = tools - .iter() - .filter_map(|tool| tool["name"].as_str()) - .collect::>(); - let workflow_names = names - .iter() - .copied() - .filter(|name| name.starts_with("tracedecay_workflow_")) - .collect::>(); - assert!( - workflow_names.contains(&"tracedecay_workflow_retire_definition"), - "retire stays the typed non-delete transition; workflow tools: {workflow_names:?}" - ); - assert!( - !names.contains(&"tracedecay_workflow_delete"), - "delete must not be advertised" - ); - - let called = response_with_id(&responses, json!(2)); - assert_eq!( - called, - json!({ - "jsonrpc": "2.0", - "id": 2, - "error": { - "code": -32603, - "message": "tool execution failed: config error: unknown tool: tracedecay_workflow_delete", - "data": { - "tool": "tracedecay_workflow_delete", - "cli_fallback": "This tool is also available from the shell: `tracedecay tool workflow_delete ...` (`tracedecay tool workflow_delete --help` for parameters). If MCP calls keep failing or timing out, fall back to that CLI instead of querying .tracedecay databases directly." - } - } - }) - ); -} diff --git a/crates/tracedecay/tests/mcp_suite/mcp_server_test/workflow_update_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_server_test/workflow_update_test.rs deleted file mode 100644 index 9653089548..0000000000 --- a/crates/tracedecay/tests/mcp_suite/mcp_server_test/workflow_update_test.rs +++ /dev/null @@ -1,71 +0,0 @@ -//! Host-visible behavior of calling `tracedecay_workflow_update`. -//! -//! Workflow definitions are immutable: a new version is registered, never -//! patched in place. This name is outside that closed operation set, so the -//! host sees the catalog refusal rather than a Workflow problem envelope or -//! an empty success. - -use serde_json::json; - -use crate::mcp_server_test::support::{ - jsonrpc_request, response_with_id, run_server_with_messages, setup_server, -}; - -const TOOL_NAME: &str = "tracedecay_workflow_update"; - -#[tokio::test] -async fn calling_workflow_update_is_refused_as_an_unknown_tool() { - let (server, _project) = setup_server().await; - let responses = run_server_with_messages( - server, - vec![ - jsonrpc_request(json!(1), "tools/list", json!({})), - jsonrpc_request( - json!(2), - "tools/call", - json!({ - "name": TOOL_NAME, - "arguments": { - "definition_id": "wfdef_checkout", - "expected_revision": 3, - "summary": "retry payment capture after the adapter timeout" - } - }), - ), - ], - ) - .await; - - let listed = response_with_id(&responses, json!(1)); - let advertised: Vec<&str> = listed["result"]["tools"] - .as_array() - .expect("tools/list returns the advertised catalog") - .iter() - .filter_map(|tool| tool["name"].as_str()) - .collect(); - assert!( - advertised.contains(&"tracedecay_workflow_list_definitions"), - "discovery must still advertise the closed Workflow family: {advertised:?}" - ); - assert!( - !advertised.contains(&TOOL_NAME), - "{TOOL_NAME} must stay off the advertised catalog: {advertised:?}" - ); - - let called = response_with_id(&responses, json!(2)); - assert_eq!( - called, - json!({ - "jsonrpc": "2.0", - "id": 2, - "error": { - "code": -32603, - "message": "tool execution failed: config error: unknown tool: tracedecay_workflow_update", - "data": { - "cli_fallback": "This tool is also available from the shell: `tracedecay tool workflow_update ...` (`tracedecay tool workflow_update --help` for parameters). If MCP calls keep failing or timing out, fall back to that CLI instead of querying .tracedecay databases directly.", - "tool": "tracedecay_workflow_update" - } - } - }) - ); -} From 128e2e67126ded8e6acfeeeb7d103b5f13712078 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:43:05 +0000 Subject: [PATCH 051/182] simplify(pass-1/5): drop unused hook event constructors Nothing constructs Cursor afterFileEdit, workspaceOpen, or Kiro postToolUse through DaemonHookEvent. Hosts still send those events as JSON, and the remaining constructors stay. Co-authored-by: Zack Jackson --- commitlint.config.cjs | 1 + crates/tracedecay-hooks/src/core_events.rs | 18 ------------------ 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/commitlint.config.cjs b/commitlint.config.cjs index 18b34ae514..3f19d02c48 100644 --- a/commitlint.config.cjs +++ b/commitlint.config.cjs @@ -8,6 +8,7 @@ const allowedTypes = [ "perf", "refactor", "revert", + "simplify", "style", "test", ]; diff --git a/crates/tracedecay-hooks/src/core_events.rs b/crates/tracedecay-hooks/src/core_events.rs index 9c9ed310c4..87e27ce7e3 100644 --- a/crates/tracedecay-hooks/src/core_events.rs +++ b/crates/tracedecay-hooks/src/core_events.rs @@ -90,10 +90,6 @@ impl DaemonHookEvent { self } - pub fn cursor_after_file_edit(rel_paths: Vec) -> Self { - Self::new(HookAgent::Cursor, "afterFileEdit", rel_paths, None, None) - } - pub fn cursor_after_shell_execution(cwd: PathBuf) -> Self { Self::new( HookAgent::Cursor, @@ -104,16 +100,6 @@ impl DaemonHookEvent { ) } - pub fn cursor_workspace_open(cwd: PathBuf) -> Self { - Self::new( - HookAgent::Cursor, - "workspaceOpen", - Vec::new(), - None, - Some(cwd), - ) - } - /// A provider session started: let the daemon own branch tracking and /// index refresh for the session's actual working directory. pub fn session_start(agent: HookAgent, cwd: PathBuf) -> Self { @@ -130,8 +116,4 @@ impl DaemonHookEvent { pub fn post_tool_use_shell(agent: HookAgent, cwd: PathBuf) -> Self { Self::new(agent, "postToolUseShell", Vec::new(), None, Some(cwd)) } - - pub fn kiro_post_tool_use(rel_paths: Vec, cwd: Option) -> Self { - Self::new(HookAgent::Kiro, "postToolUse", rel_paths, None, cwd) - } } From 98a0fec5ec0d7d179b7746793c68717eef6a05a7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:43:14 +0000 Subject: [PATCH 052/182] simplify(pass-1/5): share lexical row scoring Co-authored-by: Zack Jackson --- .../src/retrieval/lexical/projection.rs | 125 +++++++++++ .../lexical/projection/artifact/reader.rs | 173 ++------------- .../retrieval/lexical/projection/in_memory.rs | 202 +++--------------- 3 files changed, 169 insertions(+), 331 deletions(-) diff --git a/crates/tracedecay-query/src/retrieval/lexical/projection.rs b/crates/tracedecay-query/src/retrieval/lexical/projection.rs index 433b2dee35..e283e0d61e 100644 --- a/crates/tracedecay-query/src/retrieval/lexical/projection.rs +++ b/crates/tracedecay-query/src/retrieval/lexical/projection.rs @@ -761,6 +761,131 @@ fn add_score(scores: &mut BTreeMap, field: LexicalFieldV1, .or_insert(score); } +/// Shared exact/fuzzy/phrase/proximity scoring for the in-memory projection +/// and the artifact reader. Callers supply term frequencies and BM25 inputs; +/// the loop, fuzzy discount, phrase boost, and echo penalty stay one place. +fn score_lexical_row( + row: &impl LexicalFieldTextV1, + exact_terms: &[ExactTechnicalTermV1], + prepared: &PreparedLexicalQueryV1<'_>, + fuzzy: &FuzzyExpansionsV1, + phrase_document_frequencies: &BTreeMap, + mut term_frequency: impl FnMut(LexicalFieldV1, &str) -> usize, + mut document_frequency: impl FnMut(LexicalFieldV1, &str) -> usize, + mut bm25: impl FnMut(LexicalFieldV1, usize, usize) -> u64, +) -> LexicalRowScoreV1 { + let mut field_scores = BTreeMap::new(); + let mut matched_whole_terms = BTreeSet::new(); + let mut matched_subtokens = BTreeSet::new(); + let mut matched_phrases = BTreeSet::new(); + let mut matched_proximities = BTreeSet::new(); + let mut spelling_variants = BTreeSet::new(); + let mut matched_kinds = BTreeSet::new(); + let mut typo_recovery_applied = false; + for field in row.field_lengths().keys().copied() { + if field != LexicalFieldV1::Subtoken { + for (query_term, normalized) in &prepared.whole_terms { + let exact_tf = term_frequency(field, normalized); + if exact_tf > 0 { + add_score( + &mut field_scores, + field, + bm25(field, exact_tf, document_frequency(field, normalized)), + ); + matched_whole_terms.insert((*query_term).to_owned()); + collect_term_kinds(exact_terms, normalized, &mut matched_kinds); + } + if let Some(expansions) = fuzzy.by_query.get(*query_term) { + for expansion in expansions { + let fuzzy_tf = term_frequency(field, expansion); + if fuzzy_tf == 0 { + continue; + } + let score = bm25(field, fuzzy_tf, document_frequency(field, expansion)) + .saturating_mul(FUZZY_SCORE_MILLIS) + / 1_000; + add_score(&mut field_scores, field, score); + matched_whole_terms.insert((*query_term).to_owned()); + spelling_variants.insert(LexicalSpellingVariantV1 { + query: (*query_term).to_owned(), + alternative: expansion.clone(), + }); + typo_recovery_applied = true; + collect_term_kinds(exact_terms, expansion, &mut matched_kinds); + } + } + } + } else { + for (subtoken, normalized) in &prepared.subtokens { + let tf = term_frequency(field, normalized); + if tf > 0 { + add_score( + &mut field_scores, + field, + bm25(field, tf, document_frequency(field, normalized)), + ); + matched_subtokens.insert((*subtoken).to_owned()); + } + } + } + } + for (phrase, normalized) in &prepared.phrases { + for field in row.field_lengths().keys().copied() { + let Some(text) = normalized_field_text(row, field) else { + continue; + }; + let tf = substring_count(&text, normalized); + if tf == 0 { + continue; + } + let score = bm25( + field, + tf, + phrase_document_frequencies + .get(normalized) + .copied() + .unwrap_or_default(), + ) + .saturating_mul(PHRASE_SCORE_MILLIS) + / 1_000; + add_score(&mut field_scores, field, score); + matched_phrases.insert((*phrase).to_owned()); + } + } + for proximity in &prepared.proximities { + for field in row.field_lengths().keys().copied() { + let Some(text) = normalized_field_text(row, field) else { + continue; + }; + let tf = proximity_count(&text, &proximity.terms, proximity.original.maximum_gap); + if tf == 0 { + continue; + } + let score = bm25(field, tf, 1).saturating_mul(PHRASE_SCORE_MILLIS) / 1_000; + add_score(&mut field_scores, field, score); + matched_proximities.insert(proximity.original.clone()); + } + } + let echo_penalty_applied = + !prepared.echo_query.is_empty() && prepared.echo_query == row.normalized_text().trim(); + if echo_penalty_applied { + for score in field_scores.values_mut() { + *score = score.saturating_mul(ECHO_SCORE_MILLIS) / 1_000; + } + } + LexicalRowScoreV1 { + field_scores: field_scores.into_iter().collect(), + matched_whole_terms: matched_whole_terms.into_iter().collect(), + matched_subtokens: matched_subtokens.into_iter().collect(), + matched_phrases: matched_phrases.into_iter().collect(), + matched_proximities: matched_proximities.into_iter().collect(), + spelling_variants: spelling_variants.into_iter().collect(), + matched_kinds: matched_kinds.into_iter().collect(), + typo_recovery_applied, + echo_penalty_applied, + } +} + fn field_weight_millis(field: LexicalFieldV1) -> u64 { match field { LexicalFieldV1::SymbolName => 4_000, diff --git a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/reader.rs b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/reader.rs index af80a42cbe..575aad0e2e 100644 --- a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/reader.rs +++ b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/reader.rs @@ -64,16 +64,15 @@ use crate::retrieval::ports::{ }; use super::super::{ - ECHO_SCORE_MILLIS, ExactMatchRowViewV1, FUZZY_SCORE_MILLIS, FuzzyExpansionsV1, - FuzzyQueryGroupV1, LexicalFieldTextV1, LexicalRowScoreV1, LiteralProofCacheV1, - PHRASE_SCORE_MILLIS, PreparedLexicalQueryV1, add_score, bm25_score_micros, collect_term_kinds, + ExactMatchRowViewV1, FuzzyExpansionsV1, FuzzyQueryGroupV1, LexicalFieldTextV1, + LexicalRowScoreV1, LiteralProofCacheV1, PreparedLexicalQueryV1, bm25_score_micros, exact_matches, field_weight_millis, fuzzy_distance_bound, matches_phrase, normalize_lexical, - normalized_field_text, proximity_count, retrieval_anchor, substring_count, + retrieval_anchor, score_lexical_row, }; use crate::retrieval::lexical::{ LexicalFieldFilterV1, LexicalFieldV1, LexicalLaneEvidence, LexicalLaneRequest, - LexicalSpellingVariantV1, MAX_FUZZY_TERM_EXPANSIONS_V1, MAX_LEXICAL_QUERY_TERM_BYTES_V1, - admit_candidate_sources, candidate_admission_outcome, field_admitted, + MAX_FUZZY_TERM_EXPANSIONS_V1, MAX_LEXICAL_QUERY_TERM_BYTES_V1, admit_candidate_sources, + candidate_admission_outcome, field_admitted, }; impl LexicalFieldTextV1 for ArtifactRowV1 { @@ -2166,7 +2165,7 @@ impl<'a> ArtifactQueryV1<'a> { &phrase_frequencies, &stats, &frequencies, - )?; + ); let Some(ranking) = admitted_score_micros(&score, &request.field_filters)? else { return Ok(()); }; @@ -2785,157 +2784,23 @@ impl<'a> ArtifactQueryV1<'a> { phrase_frequencies: &BTreeMap, stats: &LexicalStatsCacheV1, frequencies: &LexicalTermFrequenciesV1, - ) -> Result { + ) -> LexicalRowScoreV1 { crate::hotpath_metrics::measure_frequent("query.lane.lexical.score_row", || { - self.score_row_inner(row, prepared, fuzzy, phrase_frequencies, stats, frequencies) - }) - } - - fn score_row_inner( - &self, - row: &ArtifactRowV1, - prepared: &PreparedLexicalQueryV1<'_>, - fuzzy: &FuzzyExpansionsV1, - phrase_frequencies: &BTreeMap, - stats: &LexicalStatsCacheV1, - frequencies: &LexicalTermFrequenciesV1, - ) -> Result { - let mut field_scores = BTreeMap::new(); - let mut matched_whole_terms = BTreeSet::new(); - let mut matched_subtokens = BTreeSet::new(); - let mut matched_phrases = BTreeSet::new(); - let mut matched_proximities = BTreeSet::new(); - let mut spelling_variants = BTreeSet::new(); - let mut matched_kinds = BTreeSet::new(); - let mut typo_recovery_applied = false; - for field in row.field_lengths.keys() { - if *field != LexicalFieldV1::Subtoken { - for (query_term, normalized) in &prepared.whole_terms { - let exact_tf = term_frequency(frequencies, *field, normalized); - if exact_tf > 0 { - add_score( - &mut field_scores, - *field, - self.term_score(*field, normalized, exact_tf, row, stats), - ); - matched_whole_terms.insert((*query_term).to_owned()); - collect_term_kinds(&row.exact_terms, normalized, &mut matched_kinds); - } - if let Some(expansions) = fuzzy.by_query.get(*query_term) { - for expansion in expansions { - let tf = term_frequency(frequencies, *field, expansion); - if tf == 0 { - continue; - } - let score = self - .term_score(*field, expansion, tf, row, stats) - .saturating_mul(FUZZY_SCORE_MILLIS) - / 1_000; - add_score(&mut field_scores, *field, score); - matched_whole_terms.insert((*query_term).to_owned()); - spelling_variants.insert(LexicalSpellingVariantV1 { - query: (*query_term).to_owned(), - alternative: expansion.clone(), - }); - typo_recovery_applied = true; - collect_term_kinds(&row.exact_terms, expansion, &mut matched_kinds); - } - } - } - } else { - for (subtoken, normalized) in &prepared.subtokens { - let tf = term_frequency(frequencies, *field, normalized); - if tf > 0 { - add_score( - &mut field_scores, - *field, - self.term_score(*field, normalized, tf, row, stats), - ); - matched_subtokens.insert((*subtoken).to_owned()); - } - } - } - } - for (phrase, normalized) in &prepared.phrases { - for field in row.field_lengths.keys() { - let Some(text) = normalized_field_text(row, *field) else { - continue; - }; - let tf = substring_count(&text, normalized); - if tf == 0 { - continue; - } - let score = self - .term_score_with_df( - *field, - tf, - row, - phrase_frequencies - .get(normalized) - .copied() - .unwrap_or_default(), - stats, - ) - .saturating_mul(PHRASE_SCORE_MILLIS) - / 1_000; - add_score(&mut field_scores, *field, score); - matched_phrases.insert((*phrase).to_owned()); - } - } - for proximity in &prepared.proximities { - for field in row.field_lengths.keys() { - let Some(text) = normalized_field_text(row, *field) else { - continue; - }; - let tf = proximity_count(&text, &proximity.terms, proximity.original.maximum_gap); - if tf == 0 { - continue; - } - let score = self - .term_score_with_df(*field, tf, row, 1, stats) - .saturating_mul(PHRASE_SCORE_MILLIS) - / 1_000; - add_score(&mut field_scores, *field, score); - matched_proximities.insert(proximity.original.clone()); - } - } - let echo_penalty_applied = - !prepared.echo_query.is_empty() && prepared.echo_query == row.normalized_text.trim(); - if echo_penalty_applied { - for score in field_scores.values_mut() { - *score = score.saturating_mul(ECHO_SCORE_MILLIS) / 1_000; - } - } - Ok(LexicalRowScoreV1 { - field_scores: field_scores.into_iter().collect(), - matched_whole_terms: matched_whole_terms.into_iter().collect(), - matched_subtokens: matched_subtokens.into_iter().collect(), - matched_phrases: matched_phrases.into_iter().collect(), - matched_proximities: matched_proximities.into_iter().collect(), - spelling_variants: spelling_variants.into_iter().collect(), - matched_kinds: matched_kinds.into_iter().collect(), - typo_recovery_applied, - echo_penalty_applied, + score_lexical_row( + row, + &row.exact_terms, + prepared, + fuzzy, + phrase_frequencies, + |field, term| term_frequency(frequencies, field, term), + |field, term| stats.document_frequency(field, term), + |field, term_frequency, document_frequency| { + self.term_score_with_df(field, term_frequency, row, document_frequency, stats) + }, + ) }) } - fn term_score( - &self, - field: LexicalFieldV1, - term: &str, - term_frequency: usize, - row: &ArtifactRowV1, - stats: &LexicalStatsCacheV1, - ) -> u64 { - self.term_score_with_df( - field, - term_frequency, - row, - stats.document_frequency(field, term), - stats, - ) - } - fn term_score_with_df( &self, field: LexicalFieldV1, diff --git a/crates/tracedecay-query/src/retrieval/lexical/projection/in_memory.rs b/crates/tracedecay-query/src/retrieval/lexical/projection/in_memory.rs index cdb752f968..1a0f75ac44 100644 --- a/crates/tracedecay-query/src/retrieval/lexical/projection/in_memory.rs +++ b/crates/tracedecay-query/src/retrieval/lexical/projection/in_memory.rs @@ -19,17 +19,15 @@ use tracedecay_domain::{ }; use super::super::{ - LexicalFieldV1, LexicalLaneEvidence, LexicalLaneRequest, LexicalSpellingVariantV1, - MAX_FUZZY_TERM_EXPANSIONS_V1, admit_candidate_sources, candidate_admission_outcome, + LexicalFieldV1, LexicalLaneEvidence, LexicalLaneRequest, MAX_FUZZY_TERM_EXPANSIONS_V1, + admit_candidate_sources, candidate_admission_outcome, }; use super::{ - CodeLexicalProjectionMetadataV1, ECHO_SCORE_MILLIS, ExactMatchRowViewV1, FUZZY_SCORE_MILLIS, - FuzzyExpansionsV1, FuzzyQueryGroupV1, LexicalRowScoreV1, LiteralProofCacheV1, - PHRASE_SCORE_MILLIS, PreparedLexicalQueryV1, ProjectedChunkV1, add_score, bm25_score_micros, - canonical_projected_exact_term, collect_term_kinds, exact_field_for_kind, exact_matches, + CodeLexicalProjectionMetadataV1, ExactMatchRowViewV1, FuzzyExpansionsV1, FuzzyQueryGroupV1, + LexicalRowScoreV1, LiteralProofCacheV1, PreparedLexicalQueryV1, ProjectedChunkV1, + bm25_score_micros, canonical_projected_exact_term, exact_field_for_kind, exact_matches, field_weight_millis, fuzzy_distance_bound, matches_phrase, normalize_lexical, - normalized_field_text, normalized_search_text, proximity_count, retrieval_anchor, - substring_count, + normalized_search_text, retrieval_anchor, score_lexical_row, }; use crate::retrieval::exact::{ExactAdmissionAuthority, ExactLaneEvidence, ExactLaneRequest}; use crate::retrieval::ports::{ @@ -1000,176 +998,26 @@ impl CodeLexicalProjectionAdapterV1 { phrase_document_frequencies: &BTreeMap, ) -> LexicalRowScoreV1 { crate::hotpath_metrics::measure_frequent("query.lane.lexical.score_row", || { - self.score_row_inner(document, row, prepared, fuzzy, phrase_document_frequencies) - }) - } - - fn score_row_inner( - &self, - document: u32, - row: &ProjectedChunkV1, - prepared: &PreparedLexicalQueryV1<'_>, - fuzzy: &FuzzyExpansionsV1, - phrase_document_frequencies: &BTreeMap, - ) -> LexicalRowScoreV1 { - let mut field_scores: BTreeMap = BTreeMap::new(); - let mut matched_whole_terms = BTreeSet::new(); - let mut matched_subtokens = BTreeSet::new(); - let mut matched_phrases = BTreeSet::new(); - let mut matched_proximities = BTreeSet::new(); - let mut spelling_variants = BTreeSet::new(); - let mut matched_kinds = BTreeSet::new(); - let mut typo_recovery_applied = false; - for field in row.field_lengths.keys() { - if *field != LexicalFieldV1::Subtoken { - for (query_term, normalized_query) in &prepared.whole_terms { - let exact_tf = self - .postings - .term_frequency(*field, normalized_query, document); - if exact_tf > 0 { - add_score( - &mut field_scores, - *field, - self.term_score(*field, normalized_query, exact_tf, row), - ); - matched_whole_terms.insert((*query_term).to_owned()); - collect_term_kinds(&row.exact_terms, normalized_query, &mut matched_kinds); - } - if let Some(expansions) = fuzzy.by_query.get(*query_term) { - for expansion in expansions { - let fuzzy_tf = - self.postings.term_frequency(*field, expansion, document); - if fuzzy_tf == 0 { - continue; - } - let score = self - .term_score(*field, expansion, fuzzy_tf, row) - .saturating_mul(FUZZY_SCORE_MILLIS) - / 1_000; - add_score(&mut field_scores, *field, score); - matched_whole_terms.insert((*query_term).to_owned()); - spelling_variants.insert(LexicalSpellingVariantV1 { - query: (*query_term).to_owned(), - alternative: expansion.clone(), - }); - typo_recovery_applied = true; - collect_term_kinds(&row.exact_terms, expansion, &mut matched_kinds); - } - } - } - } - if *field == LexicalFieldV1::Subtoken { - for (subtoken, normalized) in &prepared.subtokens { - let tf = self.postings.term_frequency(*field, normalized, document); - if tf > 0 { - add_score( - &mut field_scores, - *field, - self.term_score(*field, normalized, tf, row), - ); - matched_subtokens.insert((*subtoken).to_owned()); - } - } - } - } - for (phrase, normalized) in &prepared.phrases { - for field in row.field_lengths.keys() { - let Some(text) = normalized_field_text(row, *field) else { - continue; - }; - let tf = substring_count(&text, normalized); - if tf == 0 { - continue; - } - let score = self - .phrase_score( - *field, - tf, - row, - phrase_document_frequencies - .get(normalized) - .copied() - .unwrap_or_default(), + score_lexical_row( + row, + &row.exact_terms, + prepared, + fuzzy, + phrase_document_frequencies, + |field, term| self.postings.term_frequency(field, term, document), + |field, term| self.postings.document_frequency(field, term), + |field, term_frequency, document_frequency| { + bm25_score_micros( + self.rows.len(), + document_frequency, + term_frequency, + row.field_lengths.get(&field).copied().unwrap_or(0).max(1), + self.postings.average_field_length(field), + field_weight_millis(field), ) - .saturating_mul(PHRASE_SCORE_MILLIS) - / 1_000; - add_score(&mut field_scores, *field, score); - matched_phrases.insert((*phrase).to_owned()); - } - } - for proximity in &prepared.proximities { - for field in row.field_lengths.keys() { - let Some(text) = normalized_field_text(row, *field) else { - continue; - }; - let tf = proximity_count(&text, &proximity.terms, proximity.original.maximum_gap); - if tf == 0 { - continue; - } - let score = self - .phrase_score(*field, tf, row, 1) - .saturating_mul(PHRASE_SCORE_MILLIS) - / 1_000; - add_score(&mut field_scores, *field, score); - matched_proximities.insert(proximity.original.clone()); - } - } - let echo_penalty_applied = - !prepared.echo_query.is_empty() && prepared.echo_query == row.normalized_text.trim(); - if echo_penalty_applied { - for score in field_scores.values_mut() { - *score = score.saturating_mul(ECHO_SCORE_MILLIS) / 1_000; - } - } - LexicalRowScoreV1 { - field_scores: field_scores.into_iter().collect(), - matched_whole_terms: matched_whole_terms.into_iter().collect(), - matched_subtokens: matched_subtokens.into_iter().collect(), - matched_phrases: matched_phrases.into_iter().collect(), - matched_proximities: matched_proximities.into_iter().collect(), - spelling_variants: spelling_variants.into_iter().collect(), - matched_kinds: matched_kinds.into_iter().collect(), - typo_recovery_applied, - echo_penalty_applied, - } - } - - fn term_score( - &self, - field: LexicalFieldV1, - term: &str, - term_frequency: usize, - row: &ProjectedChunkV1, - ) -> u64 { - let document_frequency = self.postings.document_frequency(field, term); - let document_length = row.field_lengths.get(&field).copied().unwrap_or(0).max(1); - let average_length = self.postings.average_field_length(field); - bm25_score_micros( - self.rows.len(), - document_frequency, - term_frequency, - document_length, - average_length, - field_weight_millis(field), - ) - } - - fn phrase_score( - &self, - field: LexicalFieldV1, - term_frequency: usize, - row: &ProjectedChunkV1, - document_frequency: usize, - ) -> u64 { - let document_length = row.field_lengths.get(&field).copied().unwrap_or(0).max(1); - bm25_score_micros( - self.rows.len(), - document_frequency, - term_frequency, - document_length, - self.postings.average_field_length(field), - field_weight_millis(field), - ) + }, + ) + }) } fn candidate( From 9a67fc4ae44ab1d700ea55612a04443480d5fba8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:43:35 +0000 Subject: [PATCH 053/182] simplify(pass-2/5): project one capture through the batch path The single-outcome projector repeated the batch helper. One capture now calls that helper with a one-element window. Co-authored-by: Zack Jackson --- crates/tracedecay-host-admission/src/lib.rs | 41 +++------------------ 1 file changed, 6 insertions(+), 35 deletions(-) diff --git a/crates/tracedecay-host-admission/src/lib.rs b/crates/tracedecay-host-admission/src/lib.rs index bee8a4d8f9..2a66016b17 100644 --- a/crates/tracedecay-host-admission/src/lib.rs +++ b/crates/tracedecay-host-admission/src/lib.rs @@ -553,12 +553,15 @@ impl<'a> HostAdmissionFacade<'a> { ) .await .map_err(|error| classify_error(&error))?; - project_captured_outcome( + let mut projected = project_captured_outcomes( database, self.authorities.repository_provenance.as_ref(), - outcome, + vec![outcome], ) - .await + .await?; + projected.pop().ok_or_else(|| { + HostAdmissionOutcome::retained_unavailable("external_source_commit_failed") + }) } /// Sanitize then persist a bounded window through one store-owned batch. @@ -984,38 +987,6 @@ fn classify_external_source_error( } } -async fn project_captured_outcome( - database: &RegisteredGlobalDb, - repository_provenance: Option<&RepositoryProvenanceAdmissionContext>, - outcome: CaptureObservationOutcome, -) -> Result { - let CaptureObservationOutcome::Persisted { - outcome: persisted, .. - } = &outcome - else { - return Ok(outcome); - }; - let projection = - tracedecay_session_memory::external_source_store::RuntimeExternalSourceStore::new( - database.runtime_client(), - ) - .capture_host_observation(persisted.receipt()) - .await - .map_err(classify_external_source_error)?; - publish_canonical_git_evidence( - database, - repository_provenance, - std::slice::from_ref(&outcome), - ) - .await?; - let outcome = if let tracedecay_session_memory::external_source_store::RuntimeSourceCaptureOutcomeV1::ProjectionPending(receipt) = projection { - accepted_for_external_source_replay(outcome, receipt)? - } else { - outcome - }; - Ok(outcome) -} - async fn project_captured_outcomes( database: &RegisteredGlobalDb, repository_provenance: Option<&RepositoryProvenanceAdmissionContext>, From 270055441409bf37a6e00ceb20cabbd43dacf971 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:43:45 +0000 Subject: [PATCH 054/182] simplify(pass-2/5): share daemon handshake and catalog lookup Identical (None, false, false) handshakes and the duplicated workflow binding read now go through one helper. Co-authored-by: Zack Jackson --- crates/tracedecay-cli/src/application_cli.rs | 11 ++- crates/tracedecay-cli/src/commands.rs | 2 +- crates/tracedecay-cli/src/commands/daemon.rs | 2 +- crates/tracedecay-cli/src/commands/index.rs | 40 ++++---- .../tracedecay-cli/src/commands/settings.rs | 7 +- crates/tracedecay-cli/src/lsp_cmd.rs | 3 +- crates/tracedecay-cli/src/tool_command.rs | 10 +- crates/tracedecay-cli/src/work_cli.rs | 59 ++++++------ crates/tracedecay-cli/src/workflow_cli.rs | 95 ++++++++----------- 9 files changed, 110 insertions(+), 119 deletions(-) diff --git a/crates/tracedecay-cli/src/application_cli.rs b/crates/tracedecay-cli/src/application_cli.rs index b37a555fa8..4d1508bff4 100644 --- a/crates/tracedecay-cli/src/application_cli.rs +++ b/crates/tracedecay-cli/src/application_cli.rs @@ -6,7 +6,8 @@ use std::path::Path; use serde::de::DeserializeOwned; use serde_json::Value; use tracedecay_contracts::{ - ApplicationProblem, ApplicationResult, LegalAction, RetryDirective, SafeDiagnostic, + ApplicationProblem, ApplicationProblemEnvelope, ApplicationResult, LegalAction, + ResultContractRef, RetryDirective, SafeDiagnostic, }; use tracedecay_daemon_protocol::DaemonInvocationProblem; use tracedecay_domain::errors::{Result, TraceDecayError}; @@ -78,6 +79,14 @@ impl ApplicationKind { } } +pub(crate) fn problem_envelope( + result_contract: ResultContractRef, + request_id: tracedecay_contracts::RequestId, + problem: ApplicationProblem, +) -> Result { + ApplicationProblemEnvelope::new(result_contract, request_id, problem).map_err(config_error) +} + pub(crate) fn read_request(path: &Path, kind: ApplicationKind) -> Result { let payload = if path == Path::new("-") { let mut payload = String::new(); diff --git a/crates/tracedecay-cli/src/commands.rs b/crates/tracedecay-cli/src/commands.rs index d95a552df1..4b52bdc26e 100644 --- a/crates/tracedecay-cli/src/commands.rs +++ b/crates/tracedecay-cli/src/commands.rs @@ -11,7 +11,7 @@ mod storage; pub(crate) use bench::handle_bench; pub(crate) use branch::handle_branch_action; pub(crate) use daemon::{ - daemon_tool_json, daemon_tool_json_until, recover_truncated_mcp_result, + client_handshake, daemon_tool_json, daemon_tool_json_until, recover_truncated_mcp_result, reject_truncation_envelope, retained_effect_payload, retained_tool_payload, }; pub use gain::handle_gain; diff --git a/crates/tracedecay-cli/src/commands/daemon.rs b/crates/tracedecay-cli/src/commands/daemon.rs index 2113fcb1e3..0f738d691c 100644 --- a/crates/tracedecay-cli/src/commands/daemon.rs +++ b/crates/tracedecay-cli/src/commands/daemon.rs @@ -7,7 +7,7 @@ use tracedecay_contracts::{ApplicationEnvelope, ApplicationOutcome, ApplicationP /// boundary so a slow CLI invocation can attribute time to client identity /// resolution separately from the daemon round-trip itself. #[hotpath::measure(label = "cli.daemon.handshake")] -fn client_handshake( +pub(crate) fn client_handshake( project_path: Option<&std::path::Path>, ) -> tracedecay_domain::errors::Result { tracedecay::daemon::handshake_for_current_client( diff --git a/crates/tracedecay-cli/src/commands/index.rs b/crates/tracedecay-cli/src/commands/index.rs index 3b99586b84..de5bb20442 100644 --- a/crates/tracedecay-cli/src/commands/index.rs +++ b/crates/tracedecay-cli/src/commands/index.rs @@ -177,11 +177,11 @@ async fn brokered_init( include_folders: &[String], handshake: &tracedecay_daemon_protocol::DaemonHandshake, ) -> tracedecay_domain::errors::Result<()> { - if !skip_folders.is_empty() || !include_folders.is_empty() { - return Err(tracedecay_domain::errors::TraceDecayError::Config { - message: "brokered init does not yet support --skip-folders/--include-folders; configure tracedecay.toml first".to_string(), - }); - } + reject_brokered_folder_options( + skip_folders, + include_folders, + "brokered init does not yet support --skip-folders/--include-folders; configure tracedecay.toml first", + )?; // Init deliberately triggers a cold project open behind this single // status call. The default warming-retry grace is far tighter than a cold // open can take on a debug build or slow shared runner, which surfaced as @@ -246,6 +246,19 @@ async fn brokered_init( Ok(()) } +fn reject_brokered_folder_options( + skip_folders: &[String], + include_folders: &[String], + message: &'static str, +) -> tracedecay_domain::errors::Result<()> { + if skip_folders.is_empty() && include_folders.is_empty() { + return Ok(()); + } + Err(tracedecay_domain::errors::TraceDecayError::Config { + message: message.to_owned(), + }) +} + fn admin_sync_status(envelope: &serde_json::Value) -> Option { let text = envelope .get("content")? @@ -555,21 +568,16 @@ pub(crate) async fn handle_sync( doctor: bool, verbose: bool, ) -> tracedecay_domain::errors::Result<()> { - if !skip_folders.is_empty() || !include_folders.is_empty() { - return Err(tracedecay_domain::errors::TraceDecayError::Config { - message: "brokered sync does not yet support --skip-folders/--include-folders; update tracedecay.toml first".to_string(), - }); - } + reject_brokered_folder_options( + &skip_folders, + &include_folders, + "brokered sync does not yet support --skip-folders/--include-folders; update tracedecay.toml first", + )?; let resolved = super::scope::resolve_project_scope( tracedecay_configuration::resolve_path_with_discovery(path), ) .await?; - let handshake = tracedecay::daemon::handshake_for_current_client( - Some(resolved.project_path.clone()), - None, - false, - false, - )?; + let handshake = super::daemon::client_handshake(Some(&resolved.project_path))?; let result = tracedecay::daemon::call_default_tool( &handshake, "tracedecay_admin_sync", diff --git a/crates/tracedecay-cli/src/commands/settings.rs b/crates/tracedecay-cli/src/commands/settings.rs index a4e6bf767f..9e7e4dce39 100644 --- a/crates/tracedecay-cli/src/commands/settings.rs +++ b/crates/tracedecay-cli/src/commands/settings.rs @@ -104,12 +104,7 @@ async fn invoke_configuration_surface( let cancellation = CancellationSignal::active(format!("cancellation.cli.{}", request_id.as_str())) .map_err(|error| configuration_error(error.to_string()))?; - let handshake = tracedecay::daemon::handshake_for_current_client( - Some(project_path.to_path_buf()), - None, - false, - false, - )?; + let handshake = super::daemon::client_handshake(Some(project_path))?; let client = tracedecay_daemon_identity::invocation_client_for_current(handshake)?; loop { let result = crate::cli::dispatch::resolve_cli_application_surface( diff --git a/crates/tracedecay-cli/src/lsp_cmd.rs b/crates/tracedecay-cli/src/lsp_cmd.rs index a50534622b..dbce29db39 100644 --- a/crates/tracedecay-cli/src/lsp_cmd.rs +++ b/crates/tracedecay-cli/src/lsp_cmd.rs @@ -59,8 +59,7 @@ async fn run_stdio_bridge(project_root: Option) -> tracedecay_domain::e .map(|binding| binding.project_root.clone()) }) .ok_or_else(|| bridge_config_error("LSP initialize did not identify a workspace root"))?; - let handshake = - tracedecay::daemon::handshake_for_current_client(Some(project_root), None, false, false)?; + let handshake = crate::commands::client_handshake(Some(&project_root))?; let invocation = tracedecay_daemon_identity::invocation_client_for_current(handshake)?; let (deadline, cancellation) = lsp_request_control().map_err(lsp_invocation_error)?; let mut session = DaemonLspSessionClient::open( diff --git a/crates/tracedecay-cli/src/tool_command.rs b/crates/tracedecay-cli/src/tool_command.rs index e9a28e8add..981ea66494 100644 --- a/crates/tracedecay-cli/src/tool_command.rs +++ b/crates/tracedecay-cli/src/tool_command.rs @@ -402,13 +402,9 @@ fn dispatch_cli_application_surface_inner( let request = match parse_application_surface_request(operation, tool_args.clone()) { Ok(request) => request, Err(error) => { - if let Ok(handshake) = tracedecay::daemon::handshake_for_current_client( - project.clone(), - None, - false, - false, - ) && let Ok(client) = - tracedecay_daemon_identity::invocation_client_for_current(handshake) + if let Ok(handshake) = crate::commands::client_handshake(project.as_deref()) + && let Ok(client) = + tracedecay_daemon_identity::invocation_client_for_current(handshake) { observe_surface_argument_rejection( Some(&client), diff --git a/crates/tracedecay-cli/src/work_cli.rs b/crates/tracedecay-cli/src/work_cli.rs index cdd3dc967e..7e7611f091 100644 --- a/crates/tracedecay-cli/src/work_cli.rs +++ b/crates/tracedecay-cli/src/work_cli.rs @@ -13,9 +13,9 @@ use tracedecay_api::WorkOperation; use tracedecay_contracts::{ AcceptWorkProposalRequestV1, AdjudicateWorkLeakCommandV1, AdmitWorkExecutionRequestV1, AdmitWorkPlacementCommand, AdmitWorkSynthesisCommand, ApplicationEnvelope, ApplicationOutcome, - ApplicationProblem, ApplicationProblemEnvelope, ApplicationResult, CancelWorkAttemptCommand, - CancellationSignal, CreateWorkTaskRequestV1, Deadline, ExecutionTopologyMetricsRequestV1, - GenerateProposalRequest, PauseWorkRunCommand, PrepareWorkDuplicateAdjudicationRequestV1, + ApplicationProblem, ApplicationResult, CancelWorkAttemptCommand, CancellationSignal, + CreateWorkTaskRequestV1, Deadline, ExecutionTopologyMetricsRequestV1, GenerateProposalRequest, + PauseWorkRunCommand, PrepareWorkDuplicateAdjudicationRequestV1, PrepareWorkProductMutationRequestV1, ReleaseWorkPlacementCommand, ResultContractRef, ResumeWorkAttemptsCommand, ResumeWorkRunCommand, RetryWorkAttemptCommandV1, ReviewWorkProposalRequestV1, SafeDiagnostic, StartWorkAttemptCommand, @@ -364,11 +364,13 @@ pub async fn invoke_work_cli_with_delivery( let invocation = match decode_work_invocation(operation, body) { Ok(invocation) => invocation, Err(_) => { - return Ok(WorkCliResponse::without_delivery(Err(work_problem( - result_contract, - request_id, - WORK.invalid_request(), - )?))); + return Ok(WorkCliResponse::without_delivery(Err( + crate::application_cli::problem_envelope( + result_contract, + request_id, + WORK.invalid_request(), + )?, + ))); } }; let request = DaemonInvocationRequest::work_application( @@ -378,8 +380,7 @@ pub async fn invoke_work_cli_with_delivery( deadline.clone(), cancellation.context(), ); - let handshake = - tracedecay::daemon::handshake_for_current_client(Some(project_root), None, false, false)?; + let handshake = crate::commands::client_handshake(Some(&project_root))?; let client = tracedecay_daemon_identity::invocation_client_for_current(handshake)?; let result = match client .invoke_controlled_with_delivery( @@ -392,11 +393,13 @@ pub async fn invoke_work_cli_with_delivery( { Ok(result) => result, Err(error) => { - return Ok(WorkCliResponse::without_delivery(Err(work_problem( - result_contract, - request_id, - error.into_application_problem(), - )?))); + return Ok(WorkCliResponse::without_delivery(Err( + crate::application_cli::problem_envelope( + result_contract, + request_id, + error.into_application_problem(), + )?, + ))); } }; let (response, delivery) = result.into_parts(); @@ -419,15 +422,17 @@ pub async fn invoke_work_cli_with_delivery( outcome: erase_work_outcome(outcome)?, }) } - DaemonInvocationOutcome::ApplicationProblem { problem } => { - Err(work_problem(result_contract, request_id.clone(), problem)?) + DaemonInvocationOutcome::ApplicationProblem { problem } => Err( + crate::application_cli::problem_envelope(result_contract, request_id.clone(), problem)?, + ), + DaemonInvocationOutcome::Problem { problem } => { + Err(crate::application_cli::problem_envelope( + result_contract, + request_id.clone(), + WORK.daemon_problem(problem), + )?) } - DaemonInvocationOutcome::Problem { problem } => Err(work_problem( - result_contract, - request_id.clone(), - WORK.daemon_problem(problem), - )?), - _ => Err(work_problem( + _ => Err(crate::application_cli::problem_envelope( result_contract, request_id.clone(), ApplicationProblem::unavailable(SafeDiagnostic { @@ -517,14 +522,6 @@ fn erase_work_outcome(outcome: WorkApplicationOutcomeV1) -> Result Result { - ApplicationProblemEnvelope::new(result_contract, request_id, problem).map_err(config_error) -} - fn decode(body: Value) -> Result where T: serde::de::DeserializeOwned, diff --git a/crates/tracedecay-cli/src/workflow_cli.rs b/crates/tracedecay-cli/src/workflow_cli.rs index 1c5b6c9125..2541f72cfc 100644 --- a/crates/tracedecay-cli/src/workflow_cli.rs +++ b/crates/tracedecay-cli/src/workflow_cli.rs @@ -9,13 +9,13 @@ use std::path::PathBuf; use serde_json::Value; use tracedecay_api::WorkflowOperation; use tracedecay_contracts::{ - ApplicationEnvelope, ApplicationOutcome, ApplicationProblem, ApplicationProblemEnvelope, - ApplicationResult, CancellationSignal, Deadline, ResultContractRef, SafeDiagnostic, - TaskHandoffIssueRequest, TaskHandoffRedeemRequest, WorkflowDefinitionActivateRequest, - WorkflowDefinitionDiffRequest, WorkflowDefinitionGetRequest, WorkflowDefinitionHistoryRequest, - WorkflowDefinitionListRequest, WorkflowDefinitionRegisterRequest, - WorkflowDefinitionRejectRequest, WorkflowDefinitionRetireRequest, - WorkflowDefinitionValidateRequest, workflow_executable_binding_registry, + ApplicationEnvelope, ApplicationOutcome, ApplicationProblem, ApplicationResult, + CancellationSignal, Deadline, ResultContractRef, SafeDiagnostic, TaskHandoffIssueRequest, + TaskHandoffRedeemRequest, WorkflowDefinitionActivateRequest, WorkflowDefinitionDiffRequest, + WorkflowDefinitionGetRequest, WorkflowDefinitionHistoryRequest, WorkflowDefinitionListRequest, + WorkflowDefinitionRegisterRequest, WorkflowDefinitionRejectRequest, + WorkflowDefinitionRetireRequest, WorkflowDefinitionValidateRequest, + workflow_executable_binding_registry, }; use tracedecay_domain::UtcMicros; use tracedecay_tool_catalog::OperationId; @@ -30,29 +30,7 @@ use tracedecay_domain::errors::{Result, TraceDecayError}; use crate::application_cli::{WORKFLOW, config_error}; -fn workflow_cli_deadline(operation: WorkflowOperation, observed_at: UtcMicros) -> Result { - let operation_id = - OperationId::new(operation.operation_id_str().to_owned()).map_err(config_error)?; - let registry = workflow_executable_binding_registry().map_err(config_error)?; - let binding = registry - .get(&operation_id) - .and_then(|availability| availability.binding()) - .ok_or_else(|| TraceDecayError::Config { - message: format!( - "Workflow operation {} is not advertised by this build", - operation_id.as_str() - ), - })?; - let maximum_micros = i64::try_from( - std::time::Duration::from_millis(binding.deadline().maximum_millis()).as_micros(), - ) - .map_err(|_| TraceDecayError::Config { - message: "The canonical Workflow deadline exceeds the domain clock".to_owned(), - })?; - Deadline::new(UtcMicros(observed_at.0.saturating_add(maximum_micros))).map_err(config_error) -} - -fn workflow_result_contract(operation: WorkflowOperation) -> Result { +fn workflow_catalog(operation: WorkflowOperation) -> Result<(ResultContractRef, u64)> { let operation_id = OperationId::new(operation.operation_id_str().to_owned()).map_err(config_error)?; let registry = workflow_executable_binding_registry().map_err(config_error)?; @@ -67,11 +45,27 @@ fn workflow_result_contract(operation: WorkflowOperation) -> Result Result { + let (_, maximum_millis) = workflow_catalog(operation)?; + deadline_from_maximum_millis(maximum_millis, observed_at) +} + +fn deadline_from_maximum_millis(maximum_millis: u64, observed_at: UtcMicros) -> Result { + let maximum_micros = i64::try_from( + std::time::Duration::from_millis(maximum_millis).as_micros(), + ) + .map_err(|_| TraceDecayError::Config { + message: "The canonical Workflow deadline exceeds the domain clock".to_owned(), + })?; + Deadline::new(UtcMicros(observed_at.0.saturating_add(maximum_micros))).map_err(config_error) +} + fn decode_workflow_invocation( operation: WorkflowOperation, body: Value, @@ -185,20 +179,20 @@ pub async fn invoke_workflow_cli( operation: WorkflowOperation, body: Value, ) -> Result> { - let result_contract = workflow_result_contract(operation)?; + let (result_contract, maximum_millis) = workflow_catalog(operation)?; let request_id = mint_global_request_id(GlobalRequestSurface::Cli).map_err(|_| TraceDecayError::Config { message: "could not allocate a Workflow CLI request id".to_owned(), })?; let observed_at = invocation_now_micros(); - let deadline = workflow_cli_deadline(operation, observed_at)?; + let deadline = deadline_from_maximum_millis(maximum_millis, observed_at)?; let cancellation = CancellationSignal::active(format!("cancellation.cli.{}", request_id.as_str())) .map_err(config_error)?; let invocation = match decode_workflow_invocation(operation, body) { Ok(invocation) => invocation, Err(_) => { - return Ok(Err(workflow_problem( + return Ok(Err(crate::application_cli::problem_envelope( result_contract, request_id, WORKFLOW.invalid_request(), @@ -212,8 +206,7 @@ pub async fn invoke_workflow_cli( deadline.clone(), cancellation.context(), ); - let handshake = - tracedecay::daemon::handshake_for_current_client(Some(project_root), None, false, false)?; + let handshake = crate::commands::client_handshake(Some(&project_root))?; let response = match tracedecay_daemon_identity::invocation_client_for_current(handshake)? .invoke_controlled( request, @@ -225,7 +218,7 @@ pub async fn invoke_workflow_cli( { Ok(response) => response, Err(error) => { - return Ok(Err(workflow_problem( + return Ok(Err(crate::application_cli::problem_envelope( result_contract, request_id, error.into_application_problem(), @@ -243,15 +236,17 @@ pub async fn invoke_workflow_cli( outcome: erase_workflow_outcome(outcome)?, })) } - DaemonInvocationOutcome::ApplicationProblem { problem } => { - Ok(Err(workflow_problem(result_contract, request_id, problem)?)) + DaemonInvocationOutcome::ApplicationProblem { problem } => Ok(Err( + crate::application_cli::problem_envelope(result_contract, request_id, problem)?, + )), + DaemonInvocationOutcome::Problem { problem } => { + Ok(Err(crate::application_cli::problem_envelope( + result_contract, + request_id, + WORKFLOW.daemon_problem(problem), + )?)) } - DaemonInvocationOutcome::Problem { problem } => Ok(Err(workflow_problem( - result_contract, - request_id, - WORKFLOW.daemon_problem(problem), - )?)), - _ => Ok(Err(workflow_problem( + _ => Ok(Err(crate::application_cli::problem_envelope( result_contract, request_id, ApplicationProblem::unavailable(SafeDiagnostic { @@ -286,14 +281,6 @@ fn erase_workflow_outcome( serde_json::from_value(outcome).map_err(Into::into) } -fn workflow_problem( - result_contract: ResultContractRef, - request_id: tracedecay_contracts::RequestId, - problem: ApplicationProblem, -) -> Result { - ApplicationProblemEnvelope::new(result_contract, request_id, problem).map_err(config_error) -} - fn decode(body: Value) -> Result where T: serde::de::DeserializeOwned, From 2a90762ead7afbe9286a1ba42998a4c76e046a8b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:43:49 +0000 Subject: [PATCH 055/182] simplify(pass-2/5): collapse detail error helpers Local bad-request, not-found, and 500 constructors all built the same FastAPI detail body. They now go through util::json_error. Co-authored-by: Zack Jackson --- .../src/automation_fact_receipts_api.rs | 4 +- .../src/automation_jobs_api.rs | 31 ++++++++------- .../src/automation_outcomes_api.rs | 4 +- .../src/automation_run_api.rs | 38 +++++++++---------- .../src/automation_skills_api.rs | 16 ++------ .../src/code_diagnostics_api.rs | 20 ++++------ .../src/explorer_api.rs | 31 ++++++--------- 7 files changed, 64 insertions(+), 80 deletions(-) diff --git a/crates/tracedecay-dashboard-api/src/automation_fact_receipts_api.rs b/crates/tracedecay-dashboard-api/src/automation_fact_receipts_api.rs index 595973d9d7..d468605226 100644 --- a/crates/tracedecay-dashboard-api/src/automation_fact_receipts_api.rs +++ b/crates/tracedecay-dashboard-api/src/automation_fact_receipts_api.rs @@ -6,7 +6,9 @@ use serde_json::{Value, json}; use super::util::{JsonQuery, coerce_limit, http_detail}; use super::{DashboardState, RequestControl}; -use crate::memory_api::control::{fact_read_control, request_terminal_state, terminal_read_response}; +use crate::memory_api::control::{ + fact_read_control, request_terminal_state, terminal_read_response, +}; use crate::tracedecay::facts::memory_application_for_db; use tracedecay_automation_runtime::automation::automatic_facts::{ AutomaticFactReceipt, AutomaticFactState, list_automatic_fact_receipts, diff --git a/crates/tracedecay-dashboard-api/src/automation_jobs_api.rs b/crates/tracedecay-dashboard-api/src/automation_jobs_api.rs index 4122e13d65..86fedba618 100644 --- a/crates/tracedecay-dashboard-api/src/automation_jobs_api.rs +++ b/crates/tracedecay-dashboard-api/src/automation_jobs_api.rs @@ -16,7 +16,7 @@ use serde::Deserialize; use serde_json::{Value, json}; use super::DashboardState; -use super::util::{JsonError, http_detail, internal_error}; +use super::util::{JsonError, internal_error, json_error}; use super::{ DashboardAutomationRunRequestV1, DashboardHttpRequestControlV1, automation_authority_error_response, exact_automation_authority, @@ -97,7 +97,7 @@ pub async fn list(State(state): State) -> ApiResult { #[hotpath::measure(label = "dashboard_api.jobs.create", future = true)] pub async fn create(State(state): State, Json(body): Json) -> ApiResult { let body = serde_json::from_value::(body) - .map_err(|err| bad_request(&format!("invalid job: {err}")))?; + .map_err(|err| json_error(StatusCode::BAD_REQUEST, &format!("invalid job: {err}")))?; let now = current_timestamp(); let job = AutomationJob { id: match body.id { @@ -117,7 +117,7 @@ pub async fn create(State(state): State, Json(body): Json updated_at: now, extra: BTreeMap::new(), }; - validate_job(&job).map_err(|err| bad_request(&err.to_string()))?; + validate_job(&job).map_err(|err| json_error(StatusCode::BAD_REQUEST, &err.to_string()))?; let job_for_write = job.clone(); let result = super::automation_run_service::execute_dashboard_automation_write( &state, @@ -139,7 +139,10 @@ pub async fn create(State(state): State, Json(body): Json .await .map_err(|err| internal_error(&err))?; if result["conflict"] == true { - return Err(bad_request(&format!("job '{}' already exists", job.id))); + return Err(json_error( + StatusCode::BAD_REQUEST, + &format!("job '{}' already exists", job.id), + )); } Ok(Json(json!({ "job": job }))) } @@ -159,8 +162,12 @@ pub async fn update( AxumPath(job_id): AxumPath, Json(body): Json, ) -> ApiResult { - let patch = serde_json::from_value::(body) - .map_err(|err| bad_request(&format!("invalid job patch: {err}")))?; + let patch = serde_json::from_value::(body).map_err(|err| { + json_error( + StatusCode::BAD_REQUEST, + &format!("invalid job patch: {err}"), + ) + })?; let job_id_for_write = job_id.clone(); let result = super::automation_run_service::execute_dashboard_automation_write( &state, @@ -219,7 +226,7 @@ pub async fn update( return Err(not_found(&job_id)); } if let Some(message) = result["validation_error"].as_str() { - return Err(bad_request(message)); + return Err(json_error(StatusCode::BAD_REQUEST, message)); } let updated = serde_json::from_value::(result["job"].clone()) .map_err(|err| internal_error(&err))?; @@ -293,7 +300,7 @@ async fn load_job_or_404( state: &DashboardState, job_id: &str, ) -> std::result::Result { - validate_job_id(job_id).map_err(|err| bad_request(&err.to_string()))?; + validate_job_id(job_id).map_err(|err| json_error(StatusCode::BAD_REQUEST, &err.to_string()))?; match find_job(&state.dashboard_root, job_id).await { Ok(Some(job)) => Ok(job), Ok(None) => Err(not_found(job_id)), @@ -331,13 +338,9 @@ fn generated_job_id(name: &str) -> String { format!("{slug}-{}", micros_now() % 1_000_000) } -fn bad_request(message: &str) -> JsonError { - (StatusCode::BAD_REQUEST, Json(http_detail(message))) -} - fn not_found(job_id: &str) -> JsonError { - ( + json_error( StatusCode::NOT_FOUND, - Json(http_detail(&format!("automation job '{job_id}' not found"))), + format!("automation job '{job_id}' not found"), ) } diff --git a/crates/tracedecay-dashboard-api/src/automation_outcomes_api.rs b/crates/tracedecay-dashboard-api/src/automation_outcomes_api.rs index 2deccb6605..c436016006 100644 --- a/crates/tracedecay-dashboard-api/src/automation_outcomes_api.rs +++ b/crates/tracedecay-dashboard-api/src/automation_outcomes_api.rs @@ -10,7 +10,9 @@ use serde_json::{Value, json}; use super::automation_authority_error_response; use super::exact_automation_authority; use super::{DashboardAutomationAuthorityErrorV1, DashboardState, RequestControl}; -use crate::memory_api::control::{fact_read_control, request_terminal_state, terminal_read_response}; +use crate::memory_api::control::{ + fact_read_control, request_terminal_state, terminal_read_response, +}; use tracedecay_automation_runtime::automation::managed_skills::list_managed_skills; use tracedecay_automation_runtime::automation::outcomes::{ AutomationOutcomesSnapshot, compute_fact_outcomes, compute_skill_outcomes, diff --git a/crates/tracedecay-dashboard-api/src/automation_run_api.rs b/crates/tracedecay-dashboard-api/src/automation_run_api.rs index 46e539a785..c50e2f4f32 100644 --- a/crates/tracedecay-dashboard-api/src/automation_run_api.rs +++ b/crates/tracedecay-dashboard-api/src/automation_run_api.rs @@ -5,7 +5,7 @@ use serde::Deserialize; use serde_json::{Value, json}; use super::DashboardState; -use super::util::http_detail; +use super::util::{internal_error, json_error}; use tracedecay_automation_runtime::automation::run_ledger::{ AutomationRunArtifact, AutomationRunArtifactKind, AutomationRunLedgerRecord, find_run_record, read_published_artifact_chain, read_run_artifact_payload, @@ -57,7 +57,7 @@ pub async fn run_list( })), ) } - Err(err) => internal_error(&format!("Failed to read automation run ledger: {err}")), + Err(err) => internal_error(format!("Failed to read automation run ledger: {err}")), } } @@ -129,8 +129,11 @@ pub async fn artifact_list( })), ) } - Ok(None) => not_found(&format!("automation run '{run_id}' not found")), - Err(err) => internal_error(&format!("Failed to load automation run artifacts: {err}")), + Ok(None) => json_error( + StatusCode::NOT_FOUND, + format!("automation run '{run_id}' not found"), + ), + Err(err) => internal_error(format!("Failed to load automation run artifacts: {err}")), } } @@ -142,16 +145,20 @@ pub async fn artifact_payload( let record = match find_run_record(&state.dashboard_root, &run_id).await { Ok(Some(record)) => record, Ok(None) => { - return not_found(&format!("automation run '{run_id}' not found")); + return json_error( + StatusCode::NOT_FOUND, + format!("automation run '{run_id}' not found"), + ); } Err(err) => { - return internal_error(&format!("Failed to load automation run artifact: {err}")); + return internal_error(format!("Failed to load automation run artifact: {err}")); } }; let Some(artifact) = find_artifact(&record.artifacts, &kind) else { - return not_found(&format!( - "automation run artifact '{kind}' not found for run '{run_id}'" - )); + return json_error( + StatusCode::NOT_FOUND, + format!("automation run artifact '{kind}' not found for run '{run_id}'"), + ); }; // Heavy per-run payloads (proposed/applied ops, validation reports) are // read and parsed here; this span scales with artifact size while the @@ -171,21 +178,10 @@ pub async fn artifact_payload( "error": "", })), ), - Err(err) => internal_error(&format!("Failed to read automation run artifact: {err}")), + Err(err) => internal_error(format!("Failed to read automation run artifact: {err}")), } } -fn not_found(message: &str) -> (StatusCode, Json) { - (StatusCode::NOT_FOUND, Json(http_detail(message))) -} - -fn internal_error(message: &str) -> (StatusCode, Json) { - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(http_detail(message)), - ) -} - fn find_artifact<'a>( artifacts: &'a [AutomationRunArtifact], kind: &str, diff --git a/crates/tracedecay-dashboard-api/src/automation_skills_api.rs b/crates/tracedecay-dashboard-api/src/automation_skills_api.rs index 82b9cf0a5d..4c0ad477db 100644 --- a/crates/tracedecay-dashboard-api/src/automation_skills_api.rs +++ b/crates/tracedecay-dashboard-api/src/automation_skills_api.rs @@ -6,7 +6,7 @@ use axum::http::StatusCode; use serde::Deserialize; use serde_json::{Value, json}; -use super::util::{JsonError, http_detail, internal_error}; +use super::util::{JsonError, internal_error, json_error}; use super::{ DashboardManagedSkillCommandOutcomeV1, DashboardManagedSkillCommandV1, DashboardState, automation_authority_error_response, exact_automation_authority, @@ -215,10 +215,6 @@ fn profile_root(state: &DashboardState) -> std::result::Result<&std::path::Path, Ok(automation_authority(state)?.profile_root()) } -fn bad_request(err: &impl ToString) -> JsonError { - (StatusCode::BAD_REQUEST, Json(http_detail(&err.to_string()))) -} - fn bad_request_or_internal(err: &impl ToString) -> JsonError { client_error_or_internal(err, false, true) } @@ -234,11 +230,11 @@ fn client_error_or_internal( ) -> JsonError { let message = err.to_string(); if allow_not_found && is_not_found(&message) { - not_found(&message) + json_error(StatusCode::NOT_FOUND, message) } else if allow_bad_request && is_bad_request(&message) { - bad_request(&message) + json_error(StatusCode::BAD_REQUEST, message) } else { - internal_error(&message) + internal_error(message) } } @@ -246,10 +242,6 @@ fn is_not_found(message: &str) -> bool { message.contains("No such file") || message.contains("not found") } -fn not_found(message: &str) -> JsonError { - (StatusCode::NOT_FOUND, Json(http_detail(message))) -} - fn is_bad_request(message: &str) -> bool { message.contains("unsafe") || message.contains("cannot be empty") diff --git a/crates/tracedecay-dashboard-api/src/code_diagnostics_api.rs b/crates/tracedecay-dashboard-api/src/code_diagnostics_api.rs index f85a7a82a6..2316abaef9 100644 --- a/crates/tracedecay-dashboard-api/src/code_diagnostics_api.rs +++ b/crates/tracedecay-dashboard-api/src/code_diagnostics_api.rs @@ -6,7 +6,7 @@ use axum::http::StatusCode; use serde::{Deserialize, Deserializer}; use serde_json::{Value, json}; -use super::util::{JsonError, http_detail, internal_error}; +use super::util::{JsonError, http_detail, internal_error, json_error}; use super::{DashboardHttpRequestControlV1, DashboardState, RequestControl}; use crate::application::dashboard_diagnostics::{ DashboardDiagnosticsAuthorityV1, DashboardDiagnosticsErrorV1, settings_revision, @@ -70,7 +70,10 @@ pub async fn patch_settings( Json(patch): Json, ) -> ApiResult { let patch = serde_json::from_value::(patch).map_err(|error| { - bad_request(&format!("invalid code diagnostics settings patch: {error}")) + json_error( + StatusCode::BAD_REQUEST, + format!("invalid code diagnostics settings patch: {error}"), + ) })?; let request = diagnostics_request(&control)?; let snapshot = authority(&state)? @@ -181,19 +184,12 @@ where }) } -fn bad_request(error: &impl ToString) -> JsonError { - ( - StatusCode::BAD_REQUEST, - Json(json!({ - "detail": error.to_string(), - })), - ) -} - fn authority_error(error: DashboardDiagnosticsErrorV1) -> JsonError { match &error { DashboardDiagnosticsErrorV1::AdapterUnavailable { .. } - | DashboardDiagnosticsErrorV1::LanguageDisabled { .. } => bad_request(&error), + | DashboardDiagnosticsErrorV1::LanguageDisabled { .. } => { + json_error(StatusCode::BAD_REQUEST, error.to_string()) + } DashboardDiagnosticsErrorV1::RevisionConflict { expected, actual } => ( StatusCode::CONFLICT, Json(json!({ diff --git a/crates/tracedecay-dashboard-api/src/explorer_api.rs b/crates/tracedecay-dashboard-api/src/explorer_api.rs index 6ca69b561f..5cda18704a 100644 --- a/crates/tracedecay-dashboard-api/src/explorer_api.rs +++ b/crates/tracedecay-dashboard-api/src/explorer_api.rs @@ -31,6 +31,7 @@ use super::read_model::{ DashboardCoverageV1, DashboardDomainStateV1, DashboardEnvelopeV1, DashboardFreshnessV1, DashboardLegalActionKindV1, DashboardLegalActionRefV1, now_micros, scope_from_state, }; +use super::util::json_error; use super::{DashboardHttpRequestControlV1, DashboardState, RequestControl, graph_service}; use crate::request_identity::{GlobalOpaqueIdentityKind, mint_global_opaque_id}; use tracedecay_session_memory::context::CancellationToken; @@ -269,27 +270,19 @@ fn new_run_id() -> Option { } fn bad_request(message: impl Into) -> Response { - ( - StatusCode::BAD_REQUEST, - Json(json!({"detail": message.into()})), - ) - .into_response() + json_error(StatusCode::BAD_REQUEST, message).into_response() } fn not_found(run_id: &str) -> Response { - ( + json_error( StatusCode::NOT_FOUND, - Json(json!({"detail": format!("explorer query run not found: {run_id}")})), + format!("explorer query run not found: {run_id}"), ) - .into_response() + .into_response() } fn internal_error(message: impl Into) -> Response { - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(json!({"detail": message.into()})), - ) - .into_response() + json_error(StatusCode::INTERNAL_SERVER_ERROR, message).into_response() } fn validate_query(request: &mut ExplorerQueryRequestV1) -> Result<(), &'static str> { @@ -423,11 +416,11 @@ pub async fn create_query( return bad_request(message); } let Some(owner) = run_owner(&state) else { - return ( + return json_error( StatusCode::SERVICE_UNAVAILABLE, - Json(json!({"detail": "exact registered project scope is unavailable"})), + "exact registered project scope is unavailable", ) - .into_response(); + .into_response(); }; let Some(run_id) = new_run_id() else { return internal_error("could not allocate explorer query run identity"); @@ -491,11 +484,11 @@ pub async fn cancel_query( }; let mut run = stored.run.write().await; if run.state != ExplorerRunStateV1::Pending { - return ( + return json_error( StatusCode::CONFLICT, - Json(json!({"detail": format!("explorer query run is already terminal: {run_id}")})), + format!("explorer query run is already terminal: {run_id}"), ) - .into_response(); + .into_response(); } stored.cancellation.cancel(); mark_cancelled(&mut run); From 0c89da927057acac589353e8833531d0a563a8f0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:44:05 +0000 Subject: [PATCH 056/182] simplify(pass-3/5): one hint-count summary builder The value and typed hint summaries populated the same map twice. The MCP value is now the typed summary without the error field. Co-authored-by: Zack Jackson --- .../src/analytics_api.rs | 50 ++++--------------- 1 file changed, 10 insertions(+), 40 deletions(-) diff --git a/crates/tracedecay-dashboard-api/src/analytics_api.rs b/crates/tracedecay-dashboard-api/src/analytics_api.rs index 3f9eebaa8e..4efc3bbb41 100644 --- a/crates/tracedecay-dashboard-api/src/analytics_api.rs +++ b/crates/tracedecay-dashboard-api/src/analytics_api.rs @@ -246,7 +246,7 @@ pub async fn overview( durable_events.as_deref(), Some(&project_id), ), - typed_usage_summary(state.lcm_db.as_deref(), durable_events.as_deref()), + usage_summary(state.lcm_db.as_deref(), durable_events.as_deref()), typed_diagnostics_summary(&state, durable_events.as_deref()), ); let usage = match usage { @@ -858,7 +858,7 @@ pub async fn usage( hotpath::future!( async move { let durable_events = durable_analytics_rows_for_state(&state).await; - match typed_usage_summary(state.lcm_db.as_deref(), durable_events.as_deref()).await { + match usage_summary(state.lcm_db.as_deref(), durable_events.as_deref()).await { Ok(payload) if !payload.available => Json(DashboardEnvelopeV1::unavailable( scope_from_state(&state), Some(payload), @@ -1094,33 +1094,13 @@ pub fn hint_summary_from_events(events: &[AnalyticsEventRecord]) -> AnalyticsHin } pub fn hint_summary_from_counts(counts: &[AnalyticsHintCounts]) -> Value { - let mut by_category: BTreeMap = HINT_CATEGORIES - .iter() - .map(|category| ((*category).to_string(), HintCounts::default())) - .collect(); - for row in counts { - by_category.insert( - row.category.clone(), - HintCounts { - emitted: row.emitted, - followed: row.followed, - ignored: row.ignored, - suppressed: row.suppressed, - }, - ); - } + let summary = typed_hint_summary_from_counts(counts); + // MCP callers omit `error`. The typed payload keeps it for dashboard + // envelopes, including the explicit null when the read succeeded. json!({ - "available": true, - "source": "analytics_events", - "by_category": by_category.into_iter().map(|(category, counts)| { - json!({ - "category": category, - "emitted": counts.emitted, - "followed": counts.followed, - "ignored": counts.ignored, - "suppressed": counts.suppressed, - }) - }).collect::>(), + "available": summary.available, + "source": summary.source, + "by_category": summary.by_category, }) } @@ -1356,19 +1336,9 @@ fn increment_usage_count(counts: &mut BTreeMap<(String, String), i64>, kind: &st .or_default() += 1; } -/// The contract form of the usage summary, shared by `GET .../usage` and the -/// `usage` member of the overview payload. +/// Shared by `GET .../usage` and the `usage` member of the overview payload. /// -/// Absent `source` / `event_count` stay `None` on the struct so serde writes -/// them as explicit nulls. The previous JSON literals omitted those keys and -/// had to round-trip through this type to keep that distinction. -async fn typed_usage_summary( - db: Option<&RegisteredGlobalDb>, - durable_events: Option<&[AnalyticsEventRecord]>, -) -> Result { - usage_summary(db, durable_events).await -} - +/// Absent `source` / `event_count` stay `None` so serde writes explicit nulls. async fn usage_summary( db: Option<&RegisteredGlobalDb>, durable_events: Option<&[AnalyticsEventRecord]>, From cf5fbe11f6a56261d8a44e4773b68d45abfcf4c7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:44:10 +0000 Subject: [PATCH 057/182] simplify(pass-1/5): drop dead user-session run path Nothing in the workspace called this entry or its only callee. Co-authored-by: Zack Jackson --- .../src/automation/runner.rs | 95 +------------------ 1 file changed, 3 insertions(+), 92 deletions(-) diff --git a/crates/tracedecay-automation-runtime/src/automation/runner.rs b/crates/tracedecay-automation-runtime/src/automation/runner.rs index 16ca32482f..52c9b8d25f 100644 --- a/crates/tracedecay-automation-runtime/src/automation/runner.rs +++ b/crates/tracedecay-automation-runtime/src/automation/runner.rs @@ -14,7 +14,6 @@ use super::backend::{ BackendRetryPolicy, run_agent_task_with_retry_report, }; use super::config::AutomationConfig; -use super::host_io::HostIo; use super::lifecycle::{ AgentRunFinalizer, AutomationCommittedReceipt, AutomationRunControl, AutomationRunError, AutomationRunLedgerPublication, AutomationRunResult, BackendTaskRun, SchedulerGate, @@ -51,15 +50,16 @@ use evidence::{ SkillWriterEvidenceOutcome, build_session_reflector_evidence, build_skill_writer_evidence, canonical_evidence_hash, }; -use retrieval::{production_user_automation_retrieval, unavailable_automation_retrieval}; +use retrieval::unavailable_automation_retrieval; use session_reflector::{ ProposedAgentOutput, SessionReflectorFinalization, build_session_reflector_prompt, finalize_session_reflector_success, validate_session_fact_candidates, }; use skill_writer::{ ProposedSkillOutput, SkillWriterFinalization, build_skill_writer_prompt, - finalize_skill_writer_success, run_user_skill_writer_with_backend_and_retrieval, + finalize_skill_writer_success, }; +pub(crate) use skill_writer::run_user_skill_writer_with_backend_and_retrieval; pub use super::lifecycle::{ AutomationRunSettlementGuard, RetainedAutomationRun, RetainedAutomationSettlementDisposition, @@ -138,17 +138,6 @@ fn profile_curation_authority( }) } -/// One callable projectless post-session review suitable for host hooks. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub struct UserSessionAutomationOptions { - #[serde(default)] - pub session_reflector: SessionReflectorAutomationOptions, - #[serde(default)] - pub memory_curator: MemoryCuratorAutomationOptions, - #[serde(default)] - pub skill_writer: SkillWriterAutomationOptions, -} - #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct UserSessionAutomationRun { pub session_reflector: SessionReflectorAutomationRun, @@ -171,84 +160,6 @@ struct CombinedReviewPublication<'a> { skill_guard: Option<&'a AutomationRunSettlementGuard>, } -#[hotpath::measure(future = true, label = "automation.run.user_session")] -pub async fn run_user_session_automation_with_backend( - host_io: HostIo, - profile_root: &std::path::Path, - session_registry: Arc, - config: &AutomationConfig, - configuration_revision_id: &ConfigurationRevisionId, - backend: &dyn AgentTaskBackend, - options: UserSessionAutomationOptions, - run_control: &AutomationRunControl, -) -> AutomationRunResult { - let _run = super::scheduler_metrics::RunningGuard::enter(); - let _duration = super::scheduler_metrics::DurationGuard::run(); - let retrieval = production_user_automation_retrieval(profile_root).await; - run_user_session_automation_with_backend_and_retrieval( - host_io, - profile_root, - session_registry, - config, - configuration_revision_id, - AutomationTaskIo { - backend, - retrieval: retrieval.as_ref(), - }, - options, - run_control, - ) - .await -} - -pub(crate) async fn run_user_session_automation_with_backend_and_retrieval( - host_io: HostIo, - profile_root: &std::path::Path, - session_registry: Arc, - config: &AutomationConfig, - configuration_revision_id: &ConfigurationRevisionId, - io: AutomationTaskIo<'_>, - options: UserSessionAutomationOptions, - run_control: &AutomationRunControl, -) -> AutomationRunResult { - let session_reflector = run_user_session_reflector_with_backend_and_retrieval( - profile_root, - Arc::clone(&session_registry), - config, - run_control, - configuration_revision_id, - io, - options.session_reflector, - ) - .await?; - let memory_curator = run_user_memory_curator_with_backend( - profile_root, - Arc::clone(&session_registry), - config, - configuration_revision_id, - io.backend, - options.memory_curator, - run_control, - ) - .await?; - let skill_writer = run_user_skill_writer_with_backend_and_retrieval( - host_io, - profile_root, - session_registry, - config, - configuration_revision_id, - io.backend, - io.retrieval, - options.skill_writer, - ) - .await?; - Ok(UserSessionAutomationRun { - session_reflector, - memory_curator, - skill_writer, - }) -} - /// Options for the scheduler-only combined reflector+skill pass. Manual /// (CLI/dashboard) runs stay per-task; this path exists so one backend call /// can serve both tasks when they are due in the same scheduler tick. From 85fb581a95dd5ab93964786e33b2988b4c1a57b9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:44:38 +0000 Subject: [PATCH 058/182] simplify(pass-3/5): flatten native hook capture nesting The capture decision returns early instead of matching four levels deep. Co-authored-by: Zack Jackson --- crates/tracedecay-cli/src/hook_capture_cmd.rs | 151 ++++++++++-------- 1 file changed, 87 insertions(+), 64 deletions(-) diff --git a/crates/tracedecay-cli/src/hook_capture_cmd.rs b/crates/tracedecay-cli/src/hook_capture_cmd.rs index 557188302a..8d238c9a1a 100644 --- a/crates/tracedecay-cli/src/hook_capture_cmd.rs +++ b/crates/tracedecay-cli/src/hook_capture_cmd.rs @@ -248,15 +248,91 @@ fn open_delivery_receipt_spool( ) } +struct PreparedNativeCapture { + outcome: NativeHookCaptureOutcomeV1, + delivery_writer: Option, + delivery_open_error: Option, + delivery_material: Option, + rejection: Option, +} + +impl PreparedNativeCapture { + fn plain(outcome: NativeHookCaptureOutcomeV1) -> Self { + Self { + outcome, + delivery_writer: None, + delivery_open_error: None, + delivery_material: None, + rejection: None, + } + } +} + +fn prepare_native_capture( + source: NativeHookCaptureSourceV1, + payload: &[u8], + working_directory: &std::io::Result, +) -> PreparedNativeCapture { + let Ok(project_root) = working_directory.as_ref() else { + return PreparedNativeCapture::plain(NativeHookCaptureOutcomeV1::Unavailable); + }; + let layout = match tracedecay_runtime_core::storage::resolve_enrolled_layout_for_current_profile( + project_root, + ) { + Ok(Some(layout)) => layout, + Ok(None) => return PreparedNativeCapture::plain(NativeHookCaptureOutcomeV1::Unbound), + Err(_) => return PreparedNativeCapture::plain(NativeHookCaptureOutcomeV1::Unavailable), + }; + let worktree_id = tracedecay_agent_hosts::hooks::hook_worktree_id_for_layout( + &tracedecay::hook_runtime(), + &layout, + ); + let (Some(now), Ok(worktree_id)) = (current_time(), worktree_id) else { + return PreparedNativeCapture::plain(NativeHookCaptureOutcomeV1::Unavailable); + }; + match tracedecay_agent_hosts::hooks::native_capture_material(source, payload, now) { + Ok(material) => { + let outcome = tracedecay_hooks::capture_native_event_for_replay( + &layout.data_root, + worktree_id, + source, + payload, + material, + now, + tracedecay_hooks::HOOK_SYNCHRONOUS_BUDGET, + ); + if outcome != NativeHookCaptureOutcomeV1::Captured { + return PreparedNativeCapture::plain(outcome); + } + let (delivery_writer, delivery_open_error) = + match open_delivery_receipt_spool(&layout.data_root, source.host()) { + Ok(writer) => (Some(writer), None), + Err(error) => (None, Some(error)), + }; + PreparedNativeCapture { + outcome, + delivery_writer, + delivery_open_error, + delivery_material: Some(material), + rejection: None, + } + } + Err( + tracedecay_hooks::NativeHookDecodeError::UnsupportedNativeEvent + | tracedecay_hooks::NativeHookDecodeError::UnsupportedNativeFamily, + ) => PreparedNativeCapture::plain(NativeHookCaptureOutcomeV1::Unsupported), + Err(error) => PreparedNativeCapture { + rejection: Some(error.to_string()), + ..PreparedNativeCapture::plain(NativeHookCaptureOutcomeV1::Rejected) + }, + } +} + pub(crate) fn run_native_capture(source: NativeHookCaptureSourceV1) -> i32 { let payload = match read_bounded_stdin() { Ok(payload) => payload, Err(()) => return refused("stdin was unreadable or exceeded the payload bound"), }; - let mut delivery_writer = None; - let mut delivery_open_error = None; - let mut delivery_material = None; - let mut rejection = None; let working_directory = std::env::current_dir(); // The invocation is analytics-visible whatever the capture outcome: an // unbound, unsupported, or rejected callback still proves the host fired @@ -268,62 +344,8 @@ pub(crate) fn run_native_capture(source: NativeHookCaptureSourceV1) -> i32 { None, &String::from_utf8_lossy(&payload), ); - let outcome = match working_directory { - Ok(project_root) => { - match tracedecay_runtime_core::storage::resolve_enrolled_layout_for_current_profile( - &project_root, - ) { - Ok(Some(layout)) => { - let worktree_id = tracedecay_agent_hosts::hooks::hook_worktree_id_for_layout( - &tracedecay::hook_runtime(), - &layout, - ); - match (current_time(), worktree_id) { - (Some(now), Ok(worktree_id)) => { - match tracedecay_agent_hosts::hooks::native_capture_material( - source, &payload, now, - ) { - Ok(material) => { - let outcome = tracedecay_hooks::capture_native_event_for_replay( - &layout.data_root, - worktree_id, - source, - &payload, - material, - now, - tracedecay_hooks::HOOK_SYNCHRONOUS_BUDGET, - ); - if outcome == NativeHookCaptureOutcomeV1::Captured { - match open_delivery_receipt_spool( - &layout.data_root, - source.host(), - ) { - Ok(writer) => delivery_writer = Some(writer), - Err(error) => delivery_open_error = Some(error), - } - delivery_material = Some(material); - } - outcome - } - Err( - tracedecay_hooks::NativeHookDecodeError::UnsupportedNativeEvent - | tracedecay_hooks::NativeHookDecodeError::UnsupportedNativeFamily, - ) => NativeHookCaptureOutcomeV1::Unsupported, - Err(error) => { - rejection = Some(error.to_string()); - NativeHookCaptureOutcomeV1::Rejected - } - } - } - _ => NativeHookCaptureOutcomeV1::Unavailable, - } - } - Ok(None) => NativeHookCaptureOutcomeV1::Unbound, - Err(_) => NativeHookCaptureOutcomeV1::Unavailable, - } - } - Err(_) => NativeHookCaptureOutcomeV1::Unavailable, - }; + let prepared = prepare_native_capture(source, &payload, &working_directory); + let outcome = prepared.outcome; let stdout = std::io::stdout(); let mut stdout = stdout.lock(); @@ -336,13 +358,14 @@ pub(crate) fn run_native_capture(source: NativeHookCaptureSourceV1) -> i32 { } drop(stdout); if outcome == NativeHookCaptureOutcomeV1::Captured { - let Some(writer) = delivery_writer else { - return refused(match delivery_open_error { + let Some(writer) = prepared.delivery_writer else { + return refused(match prepared.delivery_open_error { Some(error) => format!("native delivery receipt spool unavailable: {error}"), None => "native delivery receipt writer unavailable".to_string(), }); }; - let (Some(material), Some(delivered_at)) = (delivery_material, current_time()) else { + let (Some(material), Some(delivered_at)) = (prepared.delivery_material, current_time()) + else { return refused("native delivery receipt material unavailable"); }; let Some(settlement) = native_hook_delivery_settlement(source, material, delivered_at) @@ -366,7 +389,7 @@ pub(crate) fn run_native_capture(source: NativeHookCaptureSourceV1) -> i32 { | NativeHookCaptureOutcomeV1::Full | NativeHookCaptureOutcomeV1::ResetRequired | NativeHookCaptureOutcomeV1::Unavailable - | NativeHookCaptureOutcomeV1::AdmissionTimedOut => refused(match rejection { + | NativeHookCaptureOutcomeV1::AdmissionTimedOut => refused(match prepared.rejection { Some(reason) => format!("native capture did not land: {outcome:?} ({reason})"), None => format!("native capture did not land: {outcome:?}"), }), From 6874249f834707b47e3a701aff50a7d6632cfd6e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:44:50 +0000 Subject: [PATCH 059/182] simplify(pass-2/5): drop unused retained run entries These wrappers only substituted unavailable retrieval. Callers already pass the retained retrieval variant. Co-authored-by: Zack Jackson --- .../src/automation/runner.rs | 23 ------------------ .../automation/runner/session_reflector.rs | 24 ------------------- .../src/automation/runner/skill_writer.rs | 22 ----------------- 3 files changed, 69 deletions(-) diff --git a/crates/tracedecay-automation-runtime/src/automation/runner.rs b/crates/tracedecay-automation-runtime/src/automation/runner.rs index 52c9b8d25f..8c2cd953f5 100644 --- a/crates/tracedecay-automation-runtime/src/automation/runner.rs +++ b/crates/tracedecay-automation-runtime/src/automation/runner.rs @@ -76,13 +76,11 @@ pub use session_reflector::{ SessionReflectorAutomationRun, run_session_reflector_with_backend, run_session_reflector_with_backend_and_retrieval, run_session_reflector_with_backend_and_retrieval_for_retained_settlement, - run_session_reflector_with_backend_for_retained_settlement, }; pub use skill_writer::{ SkillWriterAutomationOptions, SkillWriterAutomationRun, run_skill_writer_with_backend, run_skill_writer_with_backend_and_retrieval, run_skill_writer_with_backend_and_retrieval_for_retained_settlement, - run_skill_writer_with_backend_for_retained_settlement, }; pub(crate) use user_evidence_preflight::run_user_session_reflector_with_backend_and_retrieval; @@ -374,27 +372,6 @@ pub async fn run_combined_review_with_backend_and_retrieval( .await } -pub async fn run_combined_review_with_backend_for_retained_settlement( - cg: &AutomationProjectContext, - config: &AutomationConfig, - configuration_revision_id: &ConfigurationRevisionId, - backend: &dyn AgentTaskBackend, - options: CombinedReviewAutomationOptions, - run_control: &AutomationRunControl, -) -> RetainedCombinedReviewRun { - let retrieval = unavailable_automation_retrieval("session_evidence_retrieval_unavailable"); - run_combined_review_with_backend_and_retrieval_for_retained_settlement( - cg, - config, - configuration_revision_id, - backend, - retrieval.as_ref(), - options, - run_control, - ) - .await -} - #[allow(clippy::too_many_arguments)] pub async fn run_combined_review_with_backend_and_retrieval_for_retained_settlement( cg: &AutomationProjectContext, diff --git a/crates/tracedecay-automation-runtime/src/automation/runner/session_reflector.rs b/crates/tracedecay-automation-runtime/src/automation/runner/session_reflector.rs index a0ea312214..1fff51cebe 100644 --- a/crates/tracedecay-automation-runtime/src/automation/runner/session_reflector.rs +++ b/crates/tracedecay-automation-runtime/src/automation/runner/session_reflector.rs @@ -1053,30 +1053,6 @@ pub async fn run_session_reflector_with_backend( .await } -/// Runs one already-admitted retained application effect without publishing -/// its ledger terminal ahead of outer settlement. The retained settlement -/// authority must bind and publish the returned exact record. -pub async fn run_session_reflector_with_backend_for_retained_settlement( - cg: &AutomationProjectContext, - config: &AutomationConfig, - run_control: &AutomationRunControl, - configuration_revision_id: &ConfigurationRevisionId, - backend: &dyn AgentTaskBackend, - options: SessionReflectorAutomationOptions, -) -> RetainedAutomationRun { - let retrieval = unavailable_automation_retrieval("session_evidence_retrieval_unavailable"); - run_session_reflector_with_backend_and_retrieval_for_retained_settlement( - cg, - config, - run_control, - configuration_revision_id, - backend, - retrieval.as_ref(), - options, - ) - .await -} - /// Retained-settlement variant that preserves the caller's canonical session /// retrieval authority instead of silently reopening the production route. #[allow(clippy::too_many_arguments)] diff --git a/crates/tracedecay-automation-runtime/src/automation/runner/skill_writer.rs b/crates/tracedecay-automation-runtime/src/automation/runner/skill_writer.rs index 6bc6eac453..6d9fc4316d 100644 --- a/crates/tracedecay-automation-runtime/src/automation/runner/skill_writer.rs +++ b/crates/tracedecay-automation-runtime/src/automation/runner/skill_writer.rs @@ -138,28 +138,6 @@ pub async fn run_skill_writer_with_backend( .await } -/// Runs one already-admitted retained application effect without publishing -/// its ledger terminal ahead of outer settlement. The retained settlement -/// authority must bind and publish the returned exact record. -pub async fn run_skill_writer_with_backend_for_retained_settlement( - cg: &AutomationProjectContext, - config: &AutomationConfig, - configuration_revision_id: &ConfigurationRevisionId, - backend: &dyn AgentTaskBackend, - options: SkillWriterAutomationOptions, -) -> RetainedAutomationRun { - let retrieval = unavailable_automation_retrieval("session_evidence_retrieval_unavailable"); - run_skill_writer_with_backend_and_retrieval_for_retained_settlement( - cg, - config, - configuration_revision_id, - backend, - retrieval.as_ref(), - options, - ) - .await -} - /// Retained-settlement variant that preserves the caller's canonical session /// retrieval authority instead of silently reopening the production route. pub async fn run_skill_writer_with_backend_and_retrieval_for_retained_settlement( From d89de94f6d5a370596a5df5d271d98dfd2f199d9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:45:07 +0000 Subject: [PATCH 060/182] simplify(pass-3/5): drop unused scheduler and ledger helpers The schedule validator only forwarded the contract parser, and the other three symbols had no callers. Co-authored-by: Zack Jackson --- .../src/automation/outcomes.rs | 10 ---------- .../src/automation/run_ledger.rs | 7 ------- .../src/automation/scheduler.rs | 5 ----- .../src/automation/skill_usage.rs | 16 ---------------- 4 files changed, 38 deletions(-) diff --git a/crates/tracedecay-automation-runtime/src/automation/outcomes.rs b/crates/tracedecay-automation-runtime/src/automation/outcomes.rs index 70c7808a84..bc53223803 100644 --- a/crates/tracedecay-automation-runtime/src/automation/outcomes.rs +++ b/crates/tracedecay-automation-runtime/src/automation/outcomes.rs @@ -296,16 +296,6 @@ pub async fn load_outcomes_snapshot(dashboard_root: &Path) -> Result Result<()> { - let lock = outcomes_snapshot_lock(dashboard_root); - let _guard = lock.lock().await; - save_outcomes_snapshot_unlocked(dashboard_root, snapshot).await -} - async fn save_outcomes_snapshot_unlocked( dashboard_root: &Path, snapshot: &AutomationOutcomesSnapshot, diff --git a/crates/tracedecay-automation-runtime/src/automation/run_ledger.rs b/crates/tracedecay-automation-runtime/src/automation/run_ledger.rs index 12ed92251c..c6897c6578 100644 --- a/crates/tracedecay-automation-runtime/src/automation/run_ledger.rs +++ b/crates/tracedecay-automation-runtime/src/automation/run_ledger.rs @@ -853,13 +853,6 @@ impl AutomationRunLedgerTaskSummary { self.latest_session_evidence_budget_exhausted .map(|index| &self.records[index]) } - - pub fn latest_scheduler_effectful_user_job_terminal( - &self, - ) -> Option<&AutomationRunLedgerRecord> { - self.latest_scheduler_effectful() - .filter(|record| record.task == AgentTaskKind::UserJob) - } } #[hotpath::measure(label = "automation_runtime.run_ledger.load_page", future = true)] diff --git a/crates/tracedecay-automation-runtime/src/automation/scheduler.rs b/crates/tracedecay-automation-runtime/src/automation/scheduler.rs index 668195c401..6c10f333c9 100644 --- a/crates/tracedecay-automation-runtime/src/automation/scheduler.rs +++ b/crates/tracedecay-automation-runtime/src/automation/scheduler.rs @@ -9,7 +9,6 @@ use cap_std::fs::Dir; use cap_std::fs::OpenOptionsExt; use cap_std::time::SystemClock; use serde::{Deserialize, Serialize}; -use tracedecay_automation::config::validate_schedule as validate_leaf_schedule; pub use tracedecay_automation::config::{AutomationSchedule, CronSchedule, parse_schedule}; use tracedecay_automation::evidence_budget::{ SessionEvidenceBudgetBackoff, SessionEvidenceBudgetExceeded, SessionEvidenceBudgetGate, @@ -749,10 +748,6 @@ pub fn stale_lock_secs(config: &AutomationConfig, task: AgentTaskKind) -> Option .or(Some(DEFAULT_STALE_LOCK_SECS)) } -pub fn validate_schedule(schedule: Option<&str>) -> Result<()> { - Ok(validate_leaf_schedule(schedule)?) -} - /// User jobs carry their own schedule/enabled state (see /// `automation::jobs`), so the fixed-task config lookup falls back to a /// disabled default that makes the fixed-task gates skip them. diff --git a/crates/tracedecay-automation-runtime/src/automation/skill_usage.rs b/crates/tracedecay-automation-runtime/src/automation/skill_usage.rs index 020746e0a0..d45d0f3a74 100644 --- a/crates/tracedecay-automation-runtime/src/automation/skill_usage.rs +++ b/crates/tracedecay-automation-runtime/src/automation/skill_usage.rs @@ -204,22 +204,6 @@ pub async fn load_skill_usage_ledger(profile_root: &Path) -> Result Result<()> { - // Split the snapshot. Do not rewrite an aggregate file, and do not delete - // a skill file that this snapshot does not mention. - for record in ledger.records.values() { - let owned = record.clone(); - let skill_id = owned.skill_id.clone(); - let first_seen_at = owned.first_seen_at; - store::update_record(profile_root, &skill_id, first_seen_at, move |slot| { - *slot = owned; - }) - .await?; - } - Ok(()) -} - pub async fn sync_skill_usage_metadata(profile_root: &Path, skill: &ManagedSkill) -> Result<()> { let skill = skill.clone(); let skill_id = skill.metadata.id.clone(); From abb3567ccac718318b80244885231a4d1f13cd8c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:45:12 +0000 Subject: [PATCH 061/182] simplify(pass-4/5): drop dead growth and receipt copies Growth ignored its size arguments. Receipt routes shared one opener. The unused skill payload wrapper and dead name fallback are gone. Co-authored-by: Zack Jackson --- .../src/automation_fact_receipts_api.rs | 62 +++++++++---------- .../src/automation_skills_api.rs | 6 +- .../src/graph_structure_api.rs | 10 +-- .../src/storage_telemetry_api.rs | 15 ++--- 4 files changed, 36 insertions(+), 57 deletions(-) diff --git a/crates/tracedecay-dashboard-api/src/automation_fact_receipts_api.rs b/crates/tracedecay-dashboard-api/src/automation_fact_receipts_api.rs index d468605226..d86fccd5cb 100644 --- a/crates/tracedecay-dashboard-api/src/automation_fact_receipts_api.rs +++ b/crates/tracedecay-dashboard-api/src/automation_fact_receipts_api.rs @@ -4,7 +4,7 @@ use axum::response::Json; use serde::Deserialize; use serde_json::{Value, json}; -use super::util::{JsonQuery, coerce_limit, http_detail}; +use super::util::{JsonQuery, coerce_limit, json_error}; use super::{DashboardState, RequestControl}; use crate::memory_api::control::{ fact_read_control, request_terminal_state, terminal_read_response, @@ -31,7 +31,7 @@ pub async fn list( let receipt_state = match params.state.as_deref() { Some(value) => match AutomaticFactState::parse(value) { Ok(state) => Some(state), - Err(err) => return (StatusCode::BAD_REQUEST, Json(http_detail(&err.to_string()))), + Err(err) => return json_error(StatusCode::BAD_REQUEST, err.to_string()), }, None => None, }; @@ -40,17 +40,9 @@ pub async fn list( 50, MAX_PROJECT_MEMORY_AUTOMATIC_FACT_RECEIPTS as i64, ) as usize; - let memory = match memory_application_for_db(state.memory_owner.clone(), state.mem_db.as_ref()) - { + let memory = match open_receipt_memory(&state) { Ok(memory) => memory, - Err(err) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(http_detail(&format!( - "Failed to initialize automatic fact receipt authority: {err}" - ))), - ); - } + Err(error) => return error, }; let result = list_automatic_fact_receipts(&memory, receipt_state, limit, &fact_read_control(&control)) @@ -71,11 +63,9 @@ pub async fn list( })), ) } - Err(err) => ( + Err(err) => json_error( StatusCode::INTERNAL_SERVER_ERROR, - Json(http_detail(&format!( - "Failed to load automatic fact receipts: {err}" - ))), + format!("Failed to load automatic fact receipts: {err}"), ), } } @@ -86,17 +76,9 @@ pub async fn view( RequestControl(control): RequestControl, AxumPath(id): AxumPath, ) -> (StatusCode, Json) { - let memory = match memory_application_for_db(state.memory_owner.clone(), state.mem_db.as_ref()) - { + let memory = match open_receipt_memory(&state) { Ok(memory) => memory, - Err(err) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(http_detail(&format!( - "Failed to initialize automatic fact receipt authority: {err}" - ))), - ); - } + Err(error) => return error, }; let result = load_automatic_fact_receipt(&memory, &id, &fact_read_control(&control)).await; if let Some(state) = request_terminal_state(&control) { @@ -104,21 +86,33 @@ pub async fn view( } match result { Ok(Some(receipt)) => (StatusCode::OK, Json(receipt_payload(&receipt))), - Ok(None) => ( + Ok(None) => json_error( StatusCode::NOT_FOUND, - Json(http_detail(&format!( - "automatic fact receipt not found: {id}" - ))), + format!("automatic fact receipt not found: {id}"), ), - Err(err) => ( + Err(err) => json_error( StatusCode::INTERNAL_SERVER_ERROR, - Json(http_detail(&format!( - "Failed to load automatic fact receipt: {err}" - ))), + format!("Failed to load automatic fact receipt: {err}"), ), } } +fn open_receipt_memory( + state: &DashboardState, +) -> std::result::Result< + tracedecay_session_memory::memory::MemoryApplication< + tracedecay_session_memory::fact_store::DatabaseFactStore<'_>, + >, + (StatusCode, Json), +> { + memory_application_for_db(state.memory_owner.clone(), state.mem_db.as_ref()).map_err(|err| { + json_error( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Failed to initialize automatic fact receipt authority: {err}"), + ) + }) +} + fn receipt_payload(receipt: &AutomaticFactReceipt) -> Value { json!({ "receipt": receipt, diff --git a/crates/tracedecay-dashboard-api/src/automation_skills_api.rs b/crates/tracedecay-dashboard-api/src/automation_skills_api.rs index 4c0ad477db..53bc094bfa 100644 --- a/crates/tracedecay-dashboard-api/src/automation_skills_api.rs +++ b/crates/tracedecay-dashboard-api/src/automation_skills_api.rs @@ -86,7 +86,7 @@ pub async fn view(State(state): State, Path(id): Path) - let skill = load_managed_skill(profile_root, &id) .await .map_err(|err| not_found_or_internal(&err))?; - skill_payload(profile_root, skill).await + skill_payload_with_deployment(profile_root, skill, None).await } #[hotpath::measure(label = "dashboard_api.skills.create", future = true)] @@ -164,10 +164,6 @@ async fn execute_skill_command( skill_payload_with_deployment(authority.profile_root(), skill, Some(deployment)).await } -async fn skill_payload(profile_root: &std::path::Path, skill: ManagedSkill) -> ApiResult { - skill_payload_with_deployment(profile_root, skill, None).await -} - async fn skill_payload_with_deployment( profile_root: &std::path::Path, skill: ManagedSkill, diff --git a/crates/tracedecay-dashboard-api/src/graph_structure_api.rs b/crates/tracedecay-dashboard-api/src/graph_structure_api.rs index 55e1b2811c..792f314eba 100644 --- a/crates/tracedecay-dashboard-api/src/graph_structure_api.rs +++ b/crates/tracedecay-dashboard-api/src/graph_structure_api.rs @@ -1109,14 +1109,8 @@ fn line_for_byte_offset(source: &[u8], byte_offset: u64) -> Option { } fn simple_symbol_name(qualified_name: &str) -> &str { - match qualified_name - .rsplit("::") - .next() - .and_then(|name| name.rsplit('.').next()) - { - Some(name) => name, - None => qualified_name, - } + let segment = qualified_name.rsplit("::").next().unwrap_or(qualified_name); + segment.rsplit('.').next().unwrap_or(segment) } fn is_callable_kind(kind: &str) -> bool { diff --git a/crates/tracedecay-dashboard-api/src/storage_telemetry_api.rs b/crates/tracedecay-dashboard-api/src/storage_telemetry_api.rs index 75488d3520..237c794fb1 100644 --- a/crates/tracedecay-dashboard-api/src/storage_telemetry_api.rs +++ b/crates/tracedecay-dashboard-api/src/storage_telemetry_api.rs @@ -554,7 +554,11 @@ fn telemetry_entry( ) }); let budget = budget_dimension(&sampled.store, sample, retention); - let growth = growth_dimension(total_bytes, free_bytes); + // Current bytes stay on the store-size read. A status sample has no + // execution-owned watermarks, so it cannot become a growth series. + let growth = StoreGrowthDimensionV1::Unknown { + reason: GROWTH_UNKNOWN_REASON.to_string(), + }; let table_growth = table_growth_dimension(TableGrowthTelemetryReadV1::Unsupported { store: StoreKeyV1::new(sanitize_store_key(&sampled.store)) .unwrap_or_else(|_| fallback_store_key()), @@ -612,15 +616,6 @@ fn budget_dimension( } } -/// Project the absence of execution-owned watermarks. Current bytes remain -/// available in the store-size read, but a status read cannot honestly turn -/// them into a growth series. -fn growth_dimension(_total_bytes: Option, _free_bytes: Option) -> StoreGrowthDimensionV1 { - StoreGrowthDimensionV1::Unknown { - reason: GROWTH_UNKNOWN_REASON.to_string(), - } -} - fn store_file_name(path: &str) -> String { std::path::Path::new(path) .file_name() From c4916393d41bce7815a5c98a97fbdff4d98f5f48 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:45:25 +0000 Subject: [PATCH 062/182] simplify(pass-4/5): collapse workflow and handoff route tables Path, segment, and catalog id are concatenations of one key and segment. Co-authored-by: Zack Jackson --- crates/tracedecay-api/src/handoff.rs | 41 +----- crates/tracedecay-api/src/lib.rs | 3 + .../tracedecay-api/src/mounted_operation.rs | 54 ++++++++ crates/tracedecay-api/src/workflow.rs | 123 +++--------------- 4 files changed, 84 insertions(+), 137 deletions(-) create mode 100644 crates/tracedecay-api/src/mounted_operation.rs diff --git a/crates/tracedecay-api/src/handoff.rs b/crates/tracedecay-api/src/handoff.rs index 1df582c2f8..6c64e7d366 100644 --- a/crates/tracedecay-api/src/handoff.rs +++ b/crates/tracedecay-api/src/handoff.rs @@ -52,40 +52,13 @@ impl HandoffOperation { matches!(self, Self::ListTaskHandoffs) } - pub const fn operation_id_str(self) -> &'static str { - match self { - Self::IssueTaskHandoff => "operation.handoff.issue_task_handoff", - Self::ListTaskHandoffs => "operation.handoff.list_task_handoffs", - Self::OpenInvestigationHandoff => "operation.handoff.open_investigation_handoff", - Self::OpenTaskHandoff => "operation.handoff.open_task_handoff", - } - } - - pub const fn route_segment(self) -> &'static str { - match self { - Self::IssueTaskHandoff => "issue-task", - Self::ListTaskHandoffs => "list-task", - Self::OpenInvestigationHandoff => "open-investigation", - Self::OpenTaskHandoff => "open-task", - } - } - - pub const fn route_path(self) -> &'static str { - match self { - Self::IssueTaskHandoff => "/handoff/issue-task", - Self::ListTaskHandoffs => "/handoff/list-task", - Self::OpenInvestigationHandoff => "/handoff/open-investigation", - Self::OpenTaskHandoff => "/handoff/open-task", - } - } - - pub const fn application_route_path(self) -> &'static str { - match self { - Self::IssueTaskHandoff => "/application/handoff/issue-task", - Self::ListTaskHandoffs => "/application/handoff/list-task", - Self::OpenInvestigationHandoff => "/application/handoff/open-investigation", - Self::OpenTaskHandoff => "/application/handoff/open-task", - } + mounted_operation_paths! { + id_prefix = "operation.handoff.", + route_prefix = "handoff", + IssueTaskHandoff: "issue_task_handoff", "issue-task"; + ListTaskHandoffs: "list_task_handoffs", "list-task"; + OpenInvestigationHandoff: "open_investigation_handoff", "open-investigation"; + OpenTaskHandoff: "open_task_handoff", "open-task"; } pub fn request_schema_name(self) -> Cow<'static, str> { diff --git a/crates/tracedecay-api/src/lib.rs b/crates/tracedecay-api/src/lib.rs index 87dcae89d2..35c74bfa18 100644 --- a/crates/tracedecay-api/src/lib.rs +++ b/crates/tracedecay-api/src/lib.rs @@ -11,6 +11,9 @@ //! #![forbid(unsafe_code)] +#[macro_use] +mod mounted_operation; + pub mod assets; pub mod configuration; pub mod doctor; diff --git a/crates/tracedecay-api/src/mounted_operation.rs b/crates/tracedecay-api/src/mounted_operation.rs new file mode 100644 index 0000000000..f5f91e5a9e --- /dev/null +++ b/crates/tracedecay-api/src/mounted_operation.rs @@ -0,0 +1,54 @@ +//! One row per mounted operation instead of a match per path projection. +//! +//! The catalog id and both route paths are concatenations of the same key and +//! segment, so a second handwritten match can only drift. + +macro_rules! mounted_operation_paths { + ( + with_key, + id_prefix = $id_prefix:literal, + route_prefix = $route_prefix:literal, + $($variant:ident: $key:literal, $segment:literal;)+ + ) => { + pub const fn operation_key(self) -> &'static str { + match self { + $(Self::$variant => $key,)+ + } + } + + mounted_operation_paths! { + id_prefix = $id_prefix, + route_prefix = $route_prefix, + $($variant: $key, $segment;)+ + } + }; + ( + id_prefix = $id_prefix:literal, + route_prefix = $route_prefix:literal, + $($variant:ident: $key:literal, $segment:literal;)+ + ) => { + pub const fn operation_id_str(self) -> &'static str { + match self { + $(Self::$variant => concat!($id_prefix, $key),)+ + } + } + + pub const fn route_segment(self) -> &'static str { + match self { + $(Self::$variant => $segment,)+ + } + } + + pub const fn route_path(self) -> &'static str { + match self { + $(Self::$variant => concat!("/", $route_prefix, "/", $segment),)+ + } + } + + pub const fn application_route_path(self) -> &'static str { + match self { + $(Self::$variant => concat!("/application/", $route_prefix, "/", $segment),)+ + } + } + }; +} diff --git a/crates/tracedecay-api/src/workflow.rs b/crates/tracedecay-api/src/workflow.rs index 85b4400163..f7e0437063 100644 --- a/crates/tracedecay-api/src/workflow.rs +++ b/crates/tracedecay-api/src/workflow.rs @@ -73,46 +73,26 @@ impl WorkflowOperation { Self::GetRun, ]; - pub const fn operation_id_str(self) -> &'static str { - match self { - Self::RegisterDefinition => "operation.workflow.register_definition", - Self::ActivateDefinition => "operation.workflow.activate_definition", - Self::RetireDefinition => "operation.workflow.retire_definition", - Self::RejectDefinition => "operation.workflow.reject_definition", - Self::ValidateDefinition => "operation.workflow.validate_definition", - Self::GetDefinition => "operation.workflow.get_definition", - Self::ListDefinitions => "operation.workflow.list_definitions", - Self::DefinitionHistory => "operation.workflow.definition_history", - Self::DiffDefinition => "operation.workflow.diff_definition", - Self::HandoffIssue => "operation.workflow.handoff_issue", - Self::HandoffRedeem => "operation.workflow.handoff_redeem", - Self::StartRun => "operation.workflow.start_run", - Self::PauseRun => "operation.workflow.pause_run", - Self::ResumeRun => "operation.workflow.resume_run", - Self::CancelRun => "operation.workflow.cancel_run", - Self::GetRun => "operation.workflow.get_run", - } - } - - pub const fn operation_key(self) -> &'static str { - match self { - Self::RegisterDefinition => "register_definition", - Self::ActivateDefinition => "activate_definition", - Self::RetireDefinition => "retire_definition", - Self::RejectDefinition => "reject_definition", - Self::ValidateDefinition => "validate_definition", - Self::GetDefinition => "get_definition", - Self::ListDefinitions => "list_definitions", - Self::DefinitionHistory => "definition_history", - Self::DiffDefinition => "diff_definition", - Self::HandoffIssue => "handoff_issue", - Self::HandoffRedeem => "handoff_redeem", - Self::StartRun => "start_run", - Self::PauseRun => "pause_run", - Self::ResumeRun => "resume_run", - Self::CancelRun => "cancel_run", - Self::GetRun => "get_run", - } + mounted_operation_paths! { + with_key, + id_prefix = "operation.workflow.", + route_prefix = "workflow", + RegisterDefinition: "register_definition", "register-definition"; + ActivateDefinition: "activate_definition", "activate-definition"; + RetireDefinition: "retire_definition", "retire-definition"; + RejectDefinition: "reject_definition", "reject-definition"; + ValidateDefinition: "validate_definition", "validate-definition"; + GetDefinition: "get_definition", "get-definition"; + ListDefinitions: "list_definitions", "list-definitions"; + DefinitionHistory: "definition_history", "definition-history"; + DiffDefinition: "diff_definition", "diff-definition"; + HandoffIssue: "handoff_issue", "handoff-issue"; + HandoffRedeem: "handoff_redeem", "handoff-redeem"; + StartRun: "start_run", "start-run"; + PauseRun: "pause_run", "pause-run"; + ResumeRun: "resume_run", "resume-run"; + CancelRun: "cancel_run", "cancel-run"; + GetRun: "get_run", "get-run"; } pub fn from_operation_key(key: &str) -> Option { @@ -137,69 +117,6 @@ impl WorkflowOperation { ) } - pub const fn route_segment(self) -> &'static str { - match self { - Self::RegisterDefinition => "register-definition", - Self::ActivateDefinition => "activate-definition", - Self::RetireDefinition => "retire-definition", - Self::RejectDefinition => "reject-definition", - Self::ValidateDefinition => "validate-definition", - Self::GetDefinition => "get-definition", - Self::ListDefinitions => "list-definitions", - Self::DefinitionHistory => "definition-history", - Self::DiffDefinition => "diff-definition", - Self::HandoffIssue => "handoff-issue", - Self::HandoffRedeem => "handoff-redeem", - Self::StartRun => "start-run", - Self::PauseRun => "pause-run", - Self::ResumeRun => "resume-run", - Self::CancelRun => "cancel-run", - Self::GetRun => "get-run", - } - } - - pub const fn route_path(self) -> &'static str { - match self { - Self::RegisterDefinition => "/workflow/register-definition", - Self::ActivateDefinition => "/workflow/activate-definition", - Self::RetireDefinition => "/workflow/retire-definition", - Self::RejectDefinition => "/workflow/reject-definition", - Self::ValidateDefinition => "/workflow/validate-definition", - Self::GetDefinition => "/workflow/get-definition", - Self::ListDefinitions => "/workflow/list-definitions", - Self::DefinitionHistory => "/workflow/definition-history", - Self::DiffDefinition => "/workflow/diff-definition", - Self::HandoffIssue => "/workflow/handoff-issue", - Self::HandoffRedeem => "/workflow/handoff-redeem", - Self::StartRun => "/workflow/start-run", - Self::PauseRun => "/workflow/pause-run", - Self::ResumeRun => "/workflow/resume-run", - Self::CancelRun => "/workflow/cancel-run", - Self::GetRun => "/workflow/get-run", - } - } - - pub const fn application_route_path(self) -> &'static str { - match self { - Self::RegisterDefinition => "/application/workflow/register-definition", - Self::ActivateDefinition => "/application/workflow/activate-definition", - Self::RetireDefinition => "/application/workflow/retire-definition", - Self::RejectDefinition => "/application/workflow/reject-definition", - Self::ValidateDefinition => "/application/workflow/validate-definition", - Self::GetDefinition => "/application/workflow/get-definition", - Self::ListDefinitions => "/application/workflow/list-definitions", - Self::DefinitionHistory => "/application/workflow/definition-history", - Self::DiffDefinition => "/application/workflow/diff-definition", - Self::HandoffIssue => "/application/workflow/handoff-issue", - Self::HandoffRedeem => "/application/workflow/handoff-redeem", - Self::StartRun => "/application/workflow/start-run", - Self::PauseRun => "/application/workflow/pause-run", - Self::ResumeRun => "/application/workflow/resume-run", - Self::CancelRun => "/application/workflow/cancel-run", - Self::GetRun => "/application/workflow/get-run", - } - } - pub fn request_schema_name(self) -> Cow<'static, str> { match self { Self::RegisterDefinition => schema_name::(), From e6e5aeebcbc1dfa7063be17098700c277ee430da Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:45:31 +0000 Subject: [PATCH 063/182] simplify(pass-2/5): flatten query helpers and narration Co-authored-by: Zack Jackson --- crates/tracedecay-graph-query/src/health.rs | 39 +------------------ crates/tracedecay-graph-query/src/lib.rs | 2 - .../src/verified_query.rs | 37 ++++-------------- .../tools/handlers/dispatch_test_support.rs | 8 ++-- 4 files changed, 14 insertions(+), 72 deletions(-) diff --git a/crates/tracedecay-graph-query/src/health.rs b/crates/tracedecay-graph-query/src/health.rs index 5e39895a23..f36c8f7152 100644 --- a/crates/tracedecay-graph-query/src/health.rs +++ b/crates/tracedecay-graph-query/src/health.rs @@ -118,10 +118,6 @@ pub async fn compute_verified_health_snapshot( }) } -// --------------------------------------------------------------------------- -// Task 2: Gini Coefficient -// --------------------------------------------------------------------------- - /// Computes the Gini coefficient for a slice of non-negative values. /// Returns 0.0 for empty slices, single-element slices, or all-zero slices. /// Result is in \[0.0, 1.0\] where 0.0 = perfect equality. @@ -167,10 +163,6 @@ pub fn gini_label(gini: f64) -> &'static str { } } -// --------------------------------------------------------------------------- -// Task 3: Tarjan's SCC / Acyclicity Score -// --------------------------------------------------------------------------- - /// Computes the acyclicity score for a directed graph. /// Uses Tarjan's SCC algorithm. Score = 1.0 - (`edges_in_nontrivial_SCCs` / `total_edges`). /// Returns (score, `number_of_edges_in_cycles`). @@ -213,10 +205,6 @@ pub fn acyclicity_score( (score, edges_in_cycles) } -// --------------------------------------------------------------------------- -// Task 4: Dependency Depth -// --------------------------------------------------------------------------- - /// A chain entry representing a file and the longest dependency chain reaching it. pub struct DepthChain { pub file: String, @@ -242,7 +230,6 @@ pub fn dependency_depth( adj: &HashMap, S1>, limit: usize, ) -> DepthResult { - // Collect all nodes let mut all_nodes: HashSet = adj.keys().cloned().collect(); for targets in adj.values() { all_nodes.extend(targets.iter().cloned()); @@ -264,7 +251,6 @@ pub fn dependency_depth( (file_count as f64).log2().ceil() as usize }; - // Step 1: Run Tarjan's SCC, map each node to its SCC index let sccs = tarjan_scc(adj); let mut node_to_scc: HashMap = HashMap::new(); for (idx, scc) in sccs.iter().enumerate() { @@ -273,7 +259,6 @@ pub fn dependency_depth( } } - // Step 2: Build DAG over SCC indices let scc_count = sccs.len(); let mut scc_adj: HashMap> = HashMap::new(); for (src, targets) in adj { @@ -286,7 +271,6 @@ pub fn dependency_depth( } } - // Step 3: Kahn's algorithm for topological sort let mut in_degree = vec![0usize; scc_count]; for targets in scc_adj.values() { for &tgt in targets { @@ -309,7 +293,6 @@ pub fn dependency_depth( } } - // Step 4: DP for longest path with predecessor tracking let mut dist = vec![0usize; scc_count]; let mut pred = vec![usize::MAX; scc_count]; @@ -324,7 +307,7 @@ pub fn dependency_depth( } } - // Step 5: Rank every SCC before applying the response limit. Reconstructing + // Rank every SCC before applying the response limit. Reconstructing // only the selected chains keeps small-limit callers proportional to their // requested result count. let max_depth = dist.iter().copied().max().unwrap_or(0); @@ -340,7 +323,6 @@ pub fn dependency_depth( for scc_idx in ranked_sccs { let depth = dist[scc_idx]; - // Reconstruct the chain by walking predecessors. let mut chain_sccs: Vec = Vec::new(); let mut cur = scc_idx; loop { @@ -353,7 +335,6 @@ pub fn dependency_depth( } chain_sccs.reverse(); - // Map SCC indices back to representative file names. let chain: Vec = chain_sccs.iter().map(|&si| sccs[si][0].clone()).collect(); let mut scc_files = sccs[scc_idx].clone(); @@ -464,10 +445,6 @@ pub fn depth_score(max_depth: usize, ideal_depth: usize) -> f64 { (ideal_depth as f64 / max_depth as f64).min(1.0) } -// --------------------------------------------------------------------------- -// Task 5: Modularity Score -// --------------------------------------------------------------------------- - /// Estimates modularity by removing hub nodes and counting connected components. /// Hub nodes = files with (fan\_in + fan\_out) > mean + 2\*stddev. /// Score = 1.0 - (1.0 / component\_count), clamped to \[0, 1\]. @@ -480,18 +457,12 @@ pub fn modularity_score( return (1.0, 0); } - // Collect all nodes let mut all_nodes: HashSet = adj.keys().cloned().collect(); for targets in adj.values() { all_nodes.extend(targets.iter().cloned()); } hotpath::gauge!("usecases.graph.modularity.nodes_total").inc(all_nodes.len() as u64); - if all_nodes.is_empty() { - return (1.0, 0); - } - - // Build undirected connectivity count per node (fan_in + fan_out) let mut connectivity: HashMap<&str, usize> = HashMap::new(); for node in &all_nodes { connectivity.insert(node.as_str(), 0); @@ -503,7 +474,6 @@ pub fn modularity_score( } } - // Compute mean and stddev let n = connectivity.len() as f64; let values: Vec = connectivity.values().map(|&v| v as f64).collect(); let mean = values.iter().sum::() / n; @@ -511,14 +481,12 @@ pub fn modularity_score( let stddev = variance.sqrt(); let threshold = mean + 2.0 * stddev; - // Identify hub nodes let hubs: HashSet<&str> = connectivity .iter() .filter(|&(_, &v)| v as f64 > threshold) .map(|(&k, _)| k) .collect(); - // Build undirected graph without hubs let non_hub_nodes: Vec<&str> = all_nodes .iter() .map(String::as_str) @@ -552,7 +520,6 @@ pub fn modularity_score( } } - // Count connected components via BFS let mut visited: HashSet<&str> = HashSet::new(); let mut components = 0; @@ -580,10 +547,6 @@ pub fn modularity_score( (score, components) } -// --------------------------------------------------------------------------- -// Task 6: Composite Health Score -// --------------------------------------------------------------------------- - /// All five health dimensions, each in \[0.0, 1.0\]. #[derive(Debug, Clone)] pub struct HealthDimensions { diff --git a/crates/tracedecay-graph-query/src/lib.rs b/crates/tracedecay-graph-query/src/lib.rs index 0195429282..d5e94e8fd7 100644 --- a/crates/tracedecay-graph-query/src/lib.rs +++ b/crates/tracedecay-graph-query/src/lib.rs @@ -32,8 +32,6 @@ pub use projection::{ request_graph_cancellation, }; pub use queries::{FileAdjacencyScan, GraphQueryManager, VerifiedHealthFileAggregateV1}; -#[cfg(any(test, feature = "test-helpers"))] -pub use verified_query::admitted_verified_graph_query_port; pub use verified_query::{ AdmittedVerifiedGraphQueryPort, VerifiedGraphQuery, VerifiedGraphQueryFuture, VerifiedGraphQueryPort, VerifiedGraphQueryRequest, diff --git a/crates/tracedecay-graph-query/src/verified_query.rs b/crates/tracedecay-graph-query/src/verified_query.rs index 34cd530e3b..7e9a610b60 100644 --- a/crates/tracedecay-graph-query/src/verified_query.rs +++ b/crates/tracedecay-graph-query/src/verified_query.rs @@ -115,15 +115,6 @@ impl VerifiedGraphQueryPort for AdmittedVerifiedGraphQueryPort { } } -#[cfg(any(test, feature = "test-helpers"))] -#[must_use] -pub fn admitted_verified_graph_query_port( - admission: Arc, - projection: Arc, -) -> Arc { - admitted_verified_graph_query_port_with_source(admission, projection, None) -} - #[must_use] pub fn admitted_verified_graph_query_port_with_source( admission: Arc, @@ -223,24 +214,14 @@ impl VerifiedGraphQuery { #[hotpath::skip] async fn await_bound(&self, future: impl Future>) -> Result { self.refuse_if_bound_closed()?; - match run_deadline_signal_interruptible( + let result = await_graph_port_wait( self.request_context.deadline(), &self.live_cancellation, future, ) - .await - { - Ok(result) => { - self.refuse_if_bound_closed()?; - result - } - Err(RequestInterruption::Cancelled) => Err(map_code_graph_read_runtime_error( - super::CodeGraphReadError::Cancelled, - )), - Err(RequestInterruption::DeadlineExceeded) => Err(map_code_graph_read_runtime_error( - super::CodeGraphReadError::TimedOut, - )), - } + .await?; + self.refuse_if_bound_closed()?; + result } pub fn project_root(&self) -> Result<&Path> { @@ -581,7 +562,7 @@ impl VerifiedGraphQuery { max_relations: usize, ) -> Result> { self.refuse_if_bound_closed()?; - let (symbols, scoped) = match logical_paths { + let symbols = match logical_paths { Some(requested) => { let mut symbols = Vec::new(); for path in requested { @@ -602,7 +583,7 @@ impl VerifiedGraphQuery { } symbols.append(&mut in_file); } - (symbols, true) + symbols } None => { let page = self.symbols_page(None, max_symbols)?; @@ -611,7 +592,7 @@ impl VerifiedGraphQuery { "verified test-attribution census exceeded its symbol budget", )); } - (page.symbols, false) + page.symbols } }; let mut paths = HashMap::new(); @@ -631,7 +612,7 @@ impl VerifiedGraphQuery { test_markers.insert(symbol.occurrence.clone()); } } - if scoped { + if logical_paths.is_some() { if test_markers.is_empty() { return Ok(HashSet::new()); } @@ -645,7 +626,6 @@ impl VerifiedGraphQuery { .into_iter() .flatten() .filter_map(|edge| paths.get(&edge.edge.to_occurrence).cloned()) - .filter(|path| logical_paths.is_none_or(|requested| requested.contains(path))) .collect()); } let occurrences = symbols @@ -661,7 +641,6 @@ impl VerifiedGraphQuery { .into_iter() .filter(|edge| test_markers.contains(&edge.from_occurrence)) .filter_map(|edge| paths.get(&edge.to_occurrence).cloned()) - .filter(|path| logical_paths.is_none_or(|requested| requested.contains(path))) .collect()) } } diff --git a/crates/tracedecay/src/mcp/tools/handlers/dispatch_test_support.rs b/crates/tracedecay/src/mcp/tools/handlers/dispatch_test_support.rs index 506ce3408c..082abf053a 100644 --- a/crates/tracedecay/src/mcp/tools/handlers/dispatch_test_support.rs +++ b/crates/tracedecay/src/mcp/tools/handlers/dispatch_test_support.rs @@ -255,8 +255,8 @@ pub(super) fn verified_graph_error_options<'a>( let mut options = verified_graph_options(cg, options); options.code_graph_projection_read_port = Some(Arc::new(FailingFixtureCodeGraphProjection { error })); - options.verified_graph_query_port = - Some(tracedecay_graph_query::admitted_verified_graph_query_port( + options.verified_graph_query_port = Some( + tracedecay_graph_query::admitted_verified_graph_query_port_with_source( options .code_graph_read_admission_port .clone() @@ -265,7 +265,9 @@ pub(super) fn verified_graph_error_options<'a>( .code_graph_projection_read_port .clone() .expect("graph fixture projection"), - )); + None, + ), + ); options } From 99694813edaca6192d67435f87e53dcac6e01f9f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:45:46 +0000 Subject: [PATCH 064/182] simplify(pass-3/5): flatten nested mcp test modules Inline graph analysis, protocol routing, dependency hints, and retrieve paging helpers into their parent modules. Co-authored-by: Zack Jackson --- .../mcp_handler_test/dependency_hint_test.rs | 214 ++- .../dependency_hint_test/lazy_cutover.rs | 213 --- .../mcp_handler_test/graph_analysis_test.rs | 1247 ++++++++++++++++- .../diff_context_behavior.rs | 262 ---- .../graph_analysis_test/gini.rs | 282 ---- .../graph_analysis_test/graph_readiness.rs | 78 -- .../graph_analysis_test/hotspots.rs | 356 ----- .../graph_analysis_test/recursion_behavior.rs | 282 ---- .../retrieve_truncation_support.rs | 54 - .../retrieve_truncation_test.rs | 53 +- .../mcp_server_test/protocol_test.rs | 186 ++- .../protocol_test/initialize_routing.rs | 188 --- 12 files changed, 1676 insertions(+), 1739 deletions(-) delete mode 100644 crates/tracedecay/tests/mcp_suite/mcp_handler_test/dependency_hint_test/lazy_cutover.rs delete mode 100644 crates/tracedecay/tests/mcp_suite/mcp_handler_test/graph_analysis_test/diff_context_behavior.rs delete mode 100644 crates/tracedecay/tests/mcp_suite/mcp_handler_test/graph_analysis_test/gini.rs delete mode 100644 crates/tracedecay/tests/mcp_suite/mcp_handler_test/graph_analysis_test/graph_readiness.rs delete mode 100644 crates/tracedecay/tests/mcp_suite/mcp_handler_test/graph_analysis_test/hotspots.rs delete mode 100644 crates/tracedecay/tests/mcp_suite/mcp_handler_test/graph_analysis_test/recursion_behavior.rs delete mode 100644 crates/tracedecay/tests/mcp_suite/mcp_handler_test/retrieve_truncation_support.rs delete mode 100644 crates/tracedecay/tests/mcp_suite/mcp_server_test/protocol_test/initialize_routing.rs diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/dependency_hint_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/dependency_hint_test.rs index 3c536b9534..78e33a796f 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/dependency_hint_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/dependency_hint_test.rs @@ -8,8 +8,6 @@ use std::time::Duration; use tracedecay::daemon::ProductionProjectCompositionHarnessV1; use tracedecay::mcp::McpServer; -mod lazy_cutover; - struct ScopedDependencyHintFixture { harness: ProductionProjectCompositionHarnessV1, project_root: std::path::PathBuf, @@ -269,3 +267,215 @@ export function IndexedAnchor() { return 1; } ); fixture.harness.shutdown().await; } + +const GENERATION_ADVANCED_REASON: &str = + "application.symbol-graph.ignored-dependency-generation-advanced"; + +enum DependencyImportStyle { + Named, + Default, +} + +async fn lazy_dependency_fixture( + symbol: &str, + import_style: DependencyImportStyle, +) -> ProductionCompositionFixture { + let (declaration, import) = match import_style { + DependencyImportStyle::Named => ( + format!("export interface {symbol} {{ value: string }}\n"), + format!("import type {{ {symbol} }} from \"pkg\";\n"), + ), + DependencyImportStyle::Default => ( + format!("export default interface {symbol} {{ value: string }}\n"), + format!("import type {symbol} from \"pkg\";\n"), + ), + }; + production_composition_fixture_with_sources(move |project| { + fs::create_dir_all(project.join("src")).unwrap(); + write_dependency_declaration(project, "pkg", &declaration); + fs::write( + project.join("src/app.ts"), + "export function GenerationAnchor() { return 1; }\n", + ) + .unwrap(); + fs::write(project.join("src/dependency-types.ts"), import).unwrap(); + }) + .await +} + +fn assert_generation_advanced_retry(response: &Value) { + assert!( + response["result"].is_null(), + "the generation-advancing call must not return a same-call symbol payload: {response}" + ); + assert_eq!( + response["error"]["data"]["reason_code"].as_str(), + Some(GENERATION_ADVANCED_REASON), + "lazy admission must expose the canonical usecase retry reason: {response}" + ); + assert_eq!(response["error"]["data"]["retryable"], true); +} + +fn code_generation(payload: &Value) -> &str { + payload["code_generation"] + .as_str() + .unwrap_or_else(|| panic!("search response code generation: {payload}")) +} + +async fn exact_symbol_payload(server: &McpServer, arguments: Value) -> Value { + let response = + handle_real_server_tool_call_raw(server, "tracedecay_find_exact_symbol", arguments).await; + assert!( + response["error"].is_null(), + "exact-symbol read must succeed: {response}" + ); + serde_json::from_str(extract_real_server_text(&response["result"])) + .expect("exact-symbol response JSON") +} + +#[tokio::test] +async fn exact_symbol_explicit_lazy_admission_advances_generation_then_retry_finds_symbol_once() { + let fixture = + lazy_dependency_fixture("ExactOnlyDependency", DependencyImportStyle::Named).await; + let server = fixture + .harness + .server(&fixture.project_root) + .expect("production project server"); + let before = + wait_for_search_payload(&server, json!({"query": "GenerationAnchor", "limit": 1})).await; + + let zero = + exact_symbol_payload(&server, json!({"name": "ExactOnlyDependency", "limit": 5})).await; + assert_eq!( + zero["count"], 0, + "the ignored dependency must be absent before explicit admission: {zero}" + ); + let after_zero = + wait_for_search_payload(&server, json!({"query": "GenerationAnchor", "limit": 1})).await; + assert_eq!(code_generation(&after_zero), code_generation(&before)); + + let arguments = json!({ + "name": "ExactOnlyDependency", + "limit": 5, + "lazy_index_ignored_dependencies": true + }); + let first = handle_real_server_tool_call_raw( + &server, + "tracedecay_find_exact_symbol", + arguments.clone(), + ) + .await; + assert_generation_advanced_retry(&first); + + let after = + wait_for_search_payload(&server, json!({"query": "GenerationAnchor", "limit": 1})).await; + assert_ne!( + code_generation(&after), + code_generation(&before), + "the scheduler must publish a new serving generation before requesting a retry" + ); + + let payload = exact_symbol_payload(&server, arguments).await; + assert_eq!(payload["count"], 1, "retry must find one symbol: {payload}"); + let matches = payload["matches"] + .as_array() + .expect("exact-symbol retry matches"); + assert_eq!(matches.len(), 1, "retry must find one symbol: {payload}"); + assert_eq!( + matches[0]["name"], "ExactOnlyDependency", + "retry must return the requested dependency symbol: {payload}" + ); + assert_eq!( + matches[0]["file"], "node_modules/pkg/index.d.ts", + "retry must return the ignored dependency declaration: {payload}" + ); + fixture.harness.shutdown().await; +} + +#[tokio::test] +async fn search_explicit_lazy_admission_advances_generation_then_retry_finds_dependency_chunk_once() +{ + let fixture = + lazy_dependency_fixture("SearchOnlyDependency", DependencyImportStyle::Default).await; + let server = fixture + .harness + .server(&fixture.project_root) + .expect("production project server"); + let before = + wait_for_search_payload(&server, json!({"query": "GenerationAnchor", "limit": 1})).await; + + let zero = search_payload(&server, json!({"query": "default", "limit": 5})).await; + assert_eq!( + zero["results"], + json!([]), + "the ignored dependency must be absent before explicit admission: {zero}" + ); + let after_zero = + wait_for_search_payload(&server, json!({"query": "GenerationAnchor", "limit": 1})).await; + assert_eq!(code_generation(&after_zero), code_generation(&before)); + + let arguments = json!({ + "query": "default", + "limit": 5, + "lazy_index_ignored_dependencies": true + }); + let first = search_payload(&server, arguments.clone()).await; + assert_eq!( + first["results"], + json!([]), + "generation-advancing admission must preserve the successful lexical result: {first}" + ); + assert_eq!( + code_generation(&first), + code_generation(&before), + "the current response remains bound to the generation that produced its lexical result" + ); + + let retry = wait_for_search_payload(&server, arguments.clone()).await; + assert_ne!( + code_generation(&retry), + code_generation(&before), + "the retry must bind the scheduler-published generation" + ); + let results = retry["results"].as_array().expect("search retry results"); + assert_eq!( + results.len(), + 1, + "retry must find one dependency chunk: {retry}" + ); + let result = &results[0]; + let anchor = result["candidate"]["anchor_id"] + .as_str() + .expect("search retry candidate anchor"); + assert!( + anchor.starts_with("code-chunk:"), + "the default keyword must bind the admitted dependency chunk: {retry}" + ); + let chunk_id = anchor + .strip_prefix("code-chunk:") + .expect("search retry chunk anchor"); + let expected_source = format!("code-chunk:{}:{chunk_id}", code_generation(&retry)); + assert!( + result["candidate"]["occurrences"] + .as_array() + .is_some_and(|occurrences| occurrences.iter().any(|occurrence| { + occurrence["source_occurrence_id"].as_str() == Some(expected_source.as_str()) + && occurrence["file_occurrence_id"] + .as_str() + .is_some_and(|file| !file.is_empty()) + })), + "the returned chunk must retain its exact generation, chunk, and file occurrence: {retry}" + ); + let stable = wait_for_search_payload(&server, arguments).await; + assert_eq!( + code_generation(&stable), + code_generation(&retry), + "a positive retry must not schedule another generation: {stable}" + ); + assert_eq!( + stable["results"].as_array().map(Vec::len), + Some(1), + "a positive retry must continue returning exactly one result: {stable}" + ); + fixture.harness.shutdown().await; +} diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/dependency_hint_test/lazy_cutover.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/dependency_hint_test/lazy_cutover.rs deleted file mode 100644 index 19230e1c14..0000000000 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/dependency_hint_test/lazy_cutover.rs +++ /dev/null @@ -1,213 +0,0 @@ -use super::*; - -const GENERATION_ADVANCED_REASON: &str = - "application.symbol-graph.ignored-dependency-generation-advanced"; - -enum DependencyImportStyle { - Named, - Default, -} - -async fn lazy_dependency_fixture( - symbol: &str, - import_style: DependencyImportStyle, -) -> ProductionCompositionFixture { - let (declaration, import) = match import_style { - DependencyImportStyle::Named => ( - format!("export interface {symbol} {{ value: string }}\n"), - format!("import type {{ {symbol} }} from \"pkg\";\n"), - ), - DependencyImportStyle::Default => ( - format!("export default interface {symbol} {{ value: string }}\n"), - format!("import type {symbol} from \"pkg\";\n"), - ), - }; - production_composition_fixture_with_sources(move |project| { - fs::create_dir_all(project.join("src")).unwrap(); - write_dependency_declaration(project, "pkg", &declaration); - fs::write( - project.join("src/app.ts"), - "export function GenerationAnchor() { return 1; }\n", - ) - .unwrap(); - fs::write(project.join("src/dependency-types.ts"), import).unwrap(); - }) - .await -} - -fn assert_generation_advanced_retry(response: &Value) { - assert!( - response["result"].is_null(), - "the generation-advancing call must not return a same-call symbol payload: {response}" - ); - assert_eq!( - response["error"]["data"]["reason_code"].as_str(), - Some(GENERATION_ADVANCED_REASON), - "lazy admission must expose the canonical usecase retry reason: {response}" - ); - assert_eq!(response["error"]["data"]["retryable"], true); -} - -fn code_generation(payload: &Value) -> &str { - payload["code_generation"] - .as_str() - .unwrap_or_else(|| panic!("search response code generation: {payload}")) -} - -async fn exact_symbol_payload(server: &McpServer, arguments: Value) -> Value { - let response = - handle_real_server_tool_call_raw(server, "tracedecay_find_exact_symbol", arguments).await; - assert!( - response["error"].is_null(), - "exact-symbol read must succeed: {response}" - ); - serde_json::from_str(extract_real_server_text(&response["result"])) - .expect("exact-symbol response JSON") -} - -#[tokio::test] -async fn exact_symbol_explicit_lazy_admission_advances_generation_then_retry_finds_symbol_once() { - let fixture = - lazy_dependency_fixture("ExactOnlyDependency", DependencyImportStyle::Named).await; - let server = fixture - .harness - .server(&fixture.project_root) - .expect("production project server"); - let before = - wait_for_search_payload(&server, json!({"query": "GenerationAnchor", "limit": 1})).await; - - let zero = - exact_symbol_payload(&server, json!({"name": "ExactOnlyDependency", "limit": 5})).await; - assert_eq!( - zero["count"], 0, - "the ignored dependency must be absent before explicit admission: {zero}" - ); - let after_zero = - wait_for_search_payload(&server, json!({"query": "GenerationAnchor", "limit": 1})).await; - assert_eq!(code_generation(&after_zero), code_generation(&before)); - - let arguments = json!({ - "name": "ExactOnlyDependency", - "limit": 5, - "lazy_index_ignored_dependencies": true - }); - let first = handle_real_server_tool_call_raw( - &server, - "tracedecay_find_exact_symbol", - arguments.clone(), - ) - .await; - assert_generation_advanced_retry(&first); - - let after = - wait_for_search_payload(&server, json!({"query": "GenerationAnchor", "limit": 1})).await; - assert_ne!( - code_generation(&after), - code_generation(&before), - "the scheduler must publish a new serving generation before requesting a retry" - ); - - let payload = exact_symbol_payload(&server, arguments).await; - assert_eq!(payload["count"], 1, "retry must find one symbol: {payload}"); - let matches = payload["matches"] - .as_array() - .expect("exact-symbol retry matches"); - assert_eq!(matches.len(), 1, "retry must find one symbol: {payload}"); - assert_eq!( - matches[0]["name"], "ExactOnlyDependency", - "retry must return the requested dependency symbol: {payload}" - ); - assert_eq!( - matches[0]["file"], "node_modules/pkg/index.d.ts", - "retry must return the ignored dependency declaration: {payload}" - ); - fixture.harness.shutdown().await; -} - -#[tokio::test] -async fn search_explicit_lazy_admission_advances_generation_then_retry_finds_dependency_chunk_once() -{ - let fixture = - lazy_dependency_fixture("SearchOnlyDependency", DependencyImportStyle::Default).await; - let server = fixture - .harness - .server(&fixture.project_root) - .expect("production project server"); - let before = - wait_for_search_payload(&server, json!({"query": "GenerationAnchor", "limit": 1})).await; - - let zero = search_payload(&server, json!({"query": "default", "limit": 5})).await; - assert_eq!( - zero["results"], - json!([]), - "the ignored dependency must be absent before explicit admission: {zero}" - ); - let after_zero = - wait_for_search_payload(&server, json!({"query": "GenerationAnchor", "limit": 1})).await; - assert_eq!(code_generation(&after_zero), code_generation(&before)); - - let arguments = json!({ - "query": "default", - "limit": 5, - "lazy_index_ignored_dependencies": true - }); - let first = search_payload(&server, arguments.clone()).await; - assert_eq!( - first["results"], - json!([]), - "generation-advancing admission must preserve the successful lexical result: {first}" - ); - assert_eq!( - code_generation(&first), - code_generation(&before), - "the current response remains bound to the generation that produced its lexical result" - ); - - let retry = wait_for_search_payload(&server, arguments.clone()).await; - assert_ne!( - code_generation(&retry), - code_generation(&before), - "the retry must bind the scheduler-published generation" - ); - let results = retry["results"].as_array().expect("search retry results"); - assert_eq!( - results.len(), - 1, - "retry must find one dependency chunk: {retry}" - ); - let result = &results[0]; - let anchor = result["candidate"]["anchor_id"] - .as_str() - .expect("search retry candidate anchor"); - assert!( - anchor.starts_with("code-chunk:"), - "the default keyword must bind the admitted dependency chunk: {retry}" - ); - let chunk_id = anchor - .strip_prefix("code-chunk:") - .expect("search retry chunk anchor"); - let expected_source = format!("code-chunk:{}:{chunk_id}", code_generation(&retry)); - assert!( - result["candidate"]["occurrences"] - .as_array() - .is_some_and(|occurrences| occurrences.iter().any(|occurrence| { - occurrence["source_occurrence_id"].as_str() == Some(expected_source.as_str()) - && occurrence["file_occurrence_id"] - .as_str() - .is_some_and(|file| !file.is_empty()) - })), - "the returned chunk must retain its exact generation, chunk, and file occurrence: {retry}" - ); - let stable = wait_for_search_payload(&server, arguments).await; - assert_eq!( - code_generation(&stable), - code_generation(&retry), - "a positive retry must not schedule another generation: {stable}" - ); - assert_eq!( - stable["results"].as_array().map(Vec::len), - Some(1), - "a positive retry must continue returning exactly one result: {stable}" - ); - fixture.harness.shutdown().await; -} diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/graph_analysis_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/graph_analysis_test.rs index 9f113bd258..99ea278962 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/graph_analysis_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/graph_analysis_test.rs @@ -1,14 +1,7 @@ #![cfg(feature = "test-transport")] -mod diff_context_behavior; -mod gini; -mod graph_readiness; -mod hotspots; -mod recursion_behavior; - use crate::common::fixture::git_run; use crate::support::*; -use graph_readiness::{find_node_id, wait_for_current_graph}; use serde_json::{Value, json}; use std::fmt::Write as _; use std::fs; @@ -2229,7 +2222,7 @@ async fn recursion_keeps_direct_recursion() { .unwrap(); let output = extract_json(&result.value); assert_eq!( - recursion_behavior::public_recursion_report(&output), +public_recursion_report(&output), json!({ "cycle_count": 1, "cycles": [{ @@ -2242,7 +2235,7 @@ async fn recursion_keeps_direct_recursion() { }), "direct recursion must be the only cycle, and `nonrecursive` must stay out: {output}" ); - recursion_behavior::assert_reported_cycles_close(&output); +assert_reported_cycles_close(&output); } #[tokio::test] @@ -2263,7 +2256,7 @@ async fn recursion_filters_self_edge_artifacts() { .unwrap(); let output = extract_json(&result.value); assert_eq!( - recursion_behavior::public_recursion_report(&output), +public_recursion_report(&output), json!({ "cycle_count": 1, "cycles": [{ @@ -2276,7 +2269,7 @@ async fn recursion_filters_self_edge_artifacts() { }), "`self.rows.push` must not become a cycle while `recurse` is reported: {output}" ); - recursion_behavior::assert_reported_cycles_close(&output); +assert_reported_cycles_close(&output); } #[tokio::test] @@ -2301,7 +2294,7 @@ pub fn c() { a(); } .unwrap(); let output = extract_json(&result.value); assert_eq!( - recursion_behavior::public_recursion_report(&output), +public_recursion_report(&output), json!({ "cycle_count": 1, "cycles": [{ @@ -2316,7 +2309,7 @@ pub fn c() { a(); } }), "the only cycle is a -> b -> c -> a: {output}" ); - recursion_behavior::assert_reported_cycles_close(&output); +assert_reported_cycles_close(&output); } /// `tracedecay_changelog`'s response must not list directories under @@ -4337,3 +4330,1231 @@ pub fn closure_then_sibling(counter: &Counter) -> u32 { close_test_graph(host).await; } + +async fn wait_for_current_graph(host: &impl AnalysisToolHost) { + tokio::time::timeout(Duration::from_secs(20), async { + loop { + let status = handle_tool_call( + host, + "tracedecay_status", + json!({ + "format": "json", + "include_branch_diagnostics": false, + "include_storage_health": false, + "include_session_ingest": false, + "include_staleness": false, + }), + None, + None, + ) + .await + .expect("typed project status while awaiting the current graph"); + let status: Value = serde_json::from_str(extract_text(&status.value)) + .expect("typed project status JSON"); + let freshness = &status["code_index_freshness"]; + let serving = &freshness["worktree"]["code_graph_serving"]; + match ( + freshness["status"].as_str(), + serving["state"].as_str(), + serving["reason"].as_str(), + freshness["worktree"]["staleness_state"].as_str(), + ) { + (Some("current"), Some("ready"), _, _) => break, + (Some("warming"), _, _, _) + | (Some("stale"), Some("ready"), _, Some("verifying")) + | (_, Some("pending"), _, _) + | (_, Some("unavailable"), Some("generation_unavailable"), _) => { + tokio::task::yield_now().await; + } + (_, Some("refused"), _, _) | (_, _, Some("activation_disabled"), _) => { + panic!("graph readiness was refused: {status}"); + } + actual => panic!("graph readiness became {actual:?}: {status}"), + } + } + }) + .await + .expect("graph did not become current within the publication budget"); +} + +async fn find_node_id(host: &impl AnalysisToolHost, name: &str) -> String { + wait_for_current_graph(host).await; + let result = handle_tool_call( + host, + "tracedecay_find_exact_symbol", + json!({"name": name, "limit": 20}), + None, + None, + ) + .await + .unwrap_or_else(|error| panic!("production exact-symbol read failed: {error}")); + let payload: Value = + serde_json::from_str(extract_text(&result.value)).expect("exact-symbol JSON"); + payload["matches"] + .as_array() + .and_then(|matches| { + matches + .iter() + .find(|result| result["name"].as_str() == Some(name)) + }) + .and_then(|result| result["id"].as_str()) + .unwrap_or_else(|| panic!("node '{name}' not found in production generation: {payload}")) + .to_owned() +} + +// `tracedecay_diff_context` as an agent host observes it: one production +// MCP `tools/call`, then the JSON text the caller reads. +// +// The fixture is one crate. `tier_b` calls `tier_c`, and `tier_a` calls +// `tier_b`. `#[test]` on `checks_tier_c` is itself a modified symbol +// (`annotation_usage` named `test`). Lines are the extractor's 0-based +// tree-sitter rows. + +const LIB_RS: &str = "mod tier_a;\nmod tier_b;\nmod tier_c;\n"; +const TIER_A_RS: &str = "use crate::tier_b::tier_b;\n\npub fn tier_a() -> u8 {\n tier_b()\n}\n"; +const TIER_B_RS: &str = "use crate::tier_c::tier_c;\n\npub fn tier_b() -> u8 {\n tier_c()\n}\n"; +const TIER_C_RS: &str = "\ +pub fn tier_c() -> u8 {\n\ + 1\n\ +}\n\ +\n\ +#[test]\n\ +fn checks_tier_c() {\n\ + let _ = tier_c();\n\ +}\n"; + +fn diff_symbol_facts(symbols: &Value) -> Vec { + let Some(symbols) = symbols.as_array() else { + panic!("diff_context symbol list is not an array: {symbols}"); + }; + let mut facts = symbols + .iter() + .map(|symbol| { + json!({ + "name": symbol["name"], + "kind": symbol["kind"], + "file": symbol["file"], + "line": symbol["line"], + }) + }) + .collect::>(); + facts.sort_by(|left, right| { + ( + left["file"].as_str(), + left["name"].as_str(), + left["line"].as_u64(), + ) + .cmp(&( + right["file"].as_str(), + right["name"].as_str(), + right["line"].as_u64(), + )) + }); + facts +} + +fn write_call_chain(project: &Path) { + fs::create_dir_all(project.join("src")).unwrap(); + fs::write(project.join("src/lib.rs"), LIB_RS).unwrap(); + fs::write(project.join("src/tier_a.rs"), TIER_A_RS).unwrap(); + fs::write(project.join("src/tier_b.rs"), TIER_B_RS).unwrap(); + fs::write(project.join("src/tier_c.rs"), TIER_C_RS).unwrap(); +} + +#[tokio::test] +async fn diff_context_reports_changed_symbols_callers_and_refuses_invalid_input() { + let dir = test_temp_dir(); + let project = dir.path().join("project"); + write_call_chain(&project); + let (host, _) = init_test_project(&project).await; + + let changed = handle_tool_call( + &host, + "tracedecay_diff_context", + json!({"files": ["src/tier_c.rs"], "depth": 1, "format": "json"}), + None, + None, + ) + .await + .expect("depth-1 diff_context"); + let changed = extract_json(&changed.value); + assert_eq!(changed["changed_files"], json!(["src/tier_c.rs"])); + // `tier_a` calls `tier_b`, so a depth-1 walk stops with callers still + // unexplored. The tool must say so instead of pretending the radius is + // complete. + assert_eq!(changed["impact_complete"], json!(false), "{changed}"); + assert_eq!( + diff_symbol_facts(&changed["modified_symbols"]), + vec![ + json!({"name": "checks_tier_c", "kind": "function", "file": "src/tier_c.rs", "line": 5}), + json!({"name": "test", "kind": "annotation_usage", "file": "src/tier_c.rs", "line": 4}), + json!({"name": "tier_c", "kind": "function", "file": "src/tier_c.rs", "line": 0}), + ], + "modified symbols: {changed}" + ); + assert_eq!(changed["impacted_symbols_count"], json!(1)); + assert_eq!( + diff_symbol_facts(&changed["impacted_symbols"]), + vec![json!({"name": "tier_b", "kind": "function", "file": "src/tier_b.rs", "line": 2}),], + "direct callers of tier_c: {changed}" + ); + assert_eq!( + changed["affected_tests"], + json!(["src/tier_c.rs"]), + "{changed}" + ); + + let wider = handle_tool_call( + &host, + "tracedecay_diff_context", + json!({"files": ["src/tier_c.rs"], "depth": 2, "format": "json"}), + None, + None, + ) + .await + .expect("depth-2 diff_context"); + let wider = extract_json(&wider.value); + assert_eq!(wider["impact_complete"], json!(true), "{wider}"); + assert_eq!(wider["impacted_symbols_count"], json!(2)); + assert_eq!( + diff_symbol_facts(&wider["impacted_symbols"]), + vec![ + json!({"name": "tier_a", "kind": "function", "file": "src/tier_a.rs", "line": 2}), + json!({"name": "tier_b", "kind": "function", "file": "src/tier_b.rs", "line": 2}), + ], + "depth 2 also reaches tier_a, which only calls tier_b: {wider}" + ); + + let duplicated = handle_tool_call( + &host, + "tracedecay_diff_context", + json!({ + "files": ["src/tier_c.rs", "src/tier_c.rs"], + "depth": 1, + "format": "json" + }), + None, + None, + ) + .await + .expect("duplicate-path diff_context"); + let duplicated = extract_json(&duplicated.value); + assert_eq!(duplicated["changed_files"], json!(["src/tier_c.rs"])); + assert_eq!( + diff_symbol_facts(&duplicated["modified_symbols"]), + vec![ + json!({"name": "checks_tier_c", "kind": "function", "file": "src/tier_c.rs", "line": 5}), + json!({"name": "test", "kind": "annotation_usage", "file": "src/tier_c.rs", "line": 4}), + json!({"name": "tier_c", "kind": "function", "file": "src/tier_c.rs", "line": 0}), + ] + ); + assert_eq!( + diff_symbol_facts(&duplicated["impacted_symbols"]), + vec![json!({"name": "tier_b", "kind": "function", "file": "src/tier_b.rs", "line": 2}),] + ); + + // A path this generation never published carries no symbols, so the + // affected-test walk has no seeds and answers empty and complete rather + // than turning "no such file here" into an invalid request. + let absent = handle_tool_call( + &host, + "tracedecay_diff_context", + json!({"files": ["src/not_in_repo.rs"], "format": "json"}), + None, + None, + ) + .await + .expect("unpublished-path diff_context"); + assert_eq!( + extract_json(&absent.value), + json!({ + "changed_files": ["src/not_in_repo.rs"], + "modified_symbols": [], + "impacted_symbols_count": 0, + "impacted_symbols": [], + "impact_complete": true, + "affected_tests": [] + }) + ); + + let empty_files = handle_tool_call( + &host, + "tracedecay_diff_context", + json!({"files": [], "format": "json"}), + None, + None, + ) + .await + .expect("empty-files diff_context"); + assert_eq!( + extract_json(&empty_files.value), + json!({ + "changed_files": [], + "modified_symbols": [], + "impacted_symbols_count": 0, + "impacted_symbols": [], + "impact_complete": true, + "affected_tests": [] + }) + ); + + let missing_files = handle_tool_call( + &host, + "tracedecay_diff_context", + json!({"format": "json"}), + None, + None, + ) + .await; + assert_eq!( + missing_files + .expect_err("missing files must be refused") + .to_string(), + "config error: tracedecay_diff_context failed over production MCP: missing required parameter: files (array of strings)" + ); + + let files_not_array = handle_tool_call( + &host, + "tracedecay_diff_context", + json!({"files": "src/tier_c.rs", "format": "json"}), + None, + None, + ) + .await; + assert_eq!( + files_not_array + .expect_err("a string files argument must be refused") + .to_string(), + "config error: tracedecay_diff_context failed over production MCP: missing required parameter: files (array of strings)" + ); + + let not_object = handle_tool_call( + &host, + "tracedecay_diff_context", + json!(["src/tier_c.rs"]), + None, + None, + ) + .await; + assert_eq!( + not_object + .expect_err("non-object arguments must be refused") + .to_string(), + "config error: tracedecay_diff_context failed over production MCP: tool execution failed: config error: invalid arguments: tracedecay_diff_context expects a JSON object" + ); + + let zero_depth = handle_tool_call( + &host, + "tracedecay_diff_context", + json!({"files": ["src/tier_c.rs"], "depth": 0, "format": "json"}), + None, + None, + ) + .await; + assert_eq!( + zero_depth.expect_err("depth 0 must be refused").to_string(), + "config error: tracedecay_diff_context failed over production MCP: tool project route failed: reason_code=code-graph-invalid-request retryable=false: the code-graph read request is invalid: code graph impact depth must be positive" + ); + + close_test_graph(host).await; +} + +// Literal `tracedecay_gini` results for a fixture whose metric values are +// fixed by source shape, not by reading the coefficient back out of the tool. +// +// The handler rounds `2*Σ(i*x_i)/(n*Σx) - (n+1)/n` (1-indexed `i` on values +// sorted ascending) to four decimals. One file, or a missing path, is the +// empty-or-singleton case and the coefficient is exactly 0. + +fn write_gini_distribution_sources(project: &Path) { + std::fs::create_dir_all(project.join("src/spans")).unwrap(); + // Body is one block and no branch: complexity 1, line span 1. + std::fs::write( + project.join("src/spans/short.rs"), + "pub fn short() -> i32 { 1 }\n", + ) + .unwrap(); + // Same complexity 1, line span 3 (declaration, body, closing brace). + std::fs::write( + project.join("src/spans/tall.rs"), + "pub fn tall() -> i32 {\n 1\n}\n", + ) + .unwrap(); + // Tiny has one field, Big has three. `plain` is a single block (complexity + // 1). `branched` is if + else (2 branches) inside a body block, so the + // inner blocks reach nesting 2 and the symbol score is 4. + std::fs::write( + project.join("src/kinds.rs"), + "\ +pub struct Tiny {\n \ + pub only: i32,\n\ +}\n\ +\n\ +pub struct Big {\n \ + pub a: i32,\n \ + pub b: i32,\n \ + pub c: i32,\n\ +}\n\ +\n\ +pub fn plain() -> i32 { 1 }\n\ +\n\ +pub fn branched(n: i32) -> i32 {\n \ + if n > 0 {\n \ + n\n \ + } else {\n \ + 0\n \ + }\n\ +}\n", + ) + .unwrap(); +} + +async fn gini_json(host: &impl AnalysisToolHost, args: Value) -> Value { + let result = handle_tool_call(host, "tracedecay_gini", args, None, None) + .await + .expect("tracedecay_gini over production MCP"); + extract_json(&result.value) +} + +/// Equal values do not define an outlier rank. Sort by name so the assertion +/// stays on the reported rows rather than `HashMap` iteration order. +fn outliers_sorted_by_name(mut payload: Value) -> Value { + if let Some(outliers) = payload.get_mut("outliers").and_then(Value::as_array_mut) { + outliers.sort_by(|left, right| left["name"].as_str().cmp(&right["name"].as_str())); + } + payload +} + +#[tokio::test] +async fn gini_reports_literal_coefficients_for_known_distributions() { + let host = production_composition_fixture_with_sources(write_gini_distribution_sources).await; + + let lines = gini_json( + &host, + json!({ + "format": "json", + "metric": "lines", + "scope": "file", + "path": "src/spans", + }), + ) + .await; + assert_eq!( + lines, + json!({ + "gini": 0.25, + "interpretation": "moderate inequality", + "total_items": 2, + "metric": "lines", + "scope": "file", + "incomplete_complexity_symbols": 0, + "outliers": [ + {"name": "src/spans/tall.rs", "value": 3.0, "pct_of_max": 100.0}, + {"name": "src/spans/short.rs", "value": 1.0, "pct_of_max": 33.0}, + ], + }), + "line spans 1 and 3 must produce Gini 0.25: {lines}" + ); + + let truncated = gini_json( + &host, + json!({ + "format": "json", + "metric": "lines", + "scope": "file", + "path": "src/spans", + "limit": 1, + }), + ) + .await; + assert_eq!( + truncated, + json!({ + "gini": 0.25, + "interpretation": "moderate inequality", + "total_items": 2, + "metric": "lines", + "scope": "file", + "incomplete_complexity_symbols": 0, + "outliers": [ + {"name": "src/spans/tall.rs", "value": 3.0, "pct_of_max": 100.0}, + ], + }), + "limit truncates the ranking and keeps the census: {truncated}" + ); + + let one_file = gini_json( + &host, + json!({ + "format": "json", + "metric": "lines", + "path": "src/spans/tall.rs", + }), + ) + .await; + assert_eq!( + one_file, + json!({ + "gini": 0.0, + "interpretation": "low inequality (healthy)", + "total_items": 1, + "metric": "lines", + "scope": "file", + "incomplete_complexity_symbols": 0, + "outliers": [ + {"name": "src/spans/tall.rs", "value": 3.0, "pct_of_max": 100.0}, + ], + }), + "a single file is perfect equality: {one_file}" + ); + + let missing = gini_json( + &host, + json!({ + "format": "json", + "metric": "lines", + "path": "src/nowhere", + }), + ) + .await; + assert_eq!( + missing, + json!({ + "gini": 0.0, + "interpretation": "low inequality (healthy)", + "total_items": 0, + "metric": "lines", + "scope": "file", + "incomplete_complexity_symbols": 0, + "outliers": [], + }), + "a path with no symbols is an empty census, not the unfiltered one: {missing}" + ); + + // Defaults are complexity + file. Both span functions score 1, so this + // coefficient is 0. The lines call above is 0.25 for the same path. + let defaults = gini_json( + &host, + json!({ + "format": "json", + "path": "src/spans", + }), + ) + .await; + assert_eq!( + outliers_sorted_by_name(defaults.clone()), + json!({ + "gini": 0.0, + "interpretation": "low inequality (healthy)", + "total_items": 2, + "metric": "complexity", + "scope": "file", + "incomplete_complexity_symbols": 0, + "outliers": [ + {"name": "src/spans/short.rs", "value": 1.0, "pct_of_max": 100.0}, + {"name": "src/spans/tall.rs", "value": 1.0, "pct_of_max": 100.0}, + ], + }), + "default metric is complexity, not lines: {defaults}" + ); + + let members = gini_json( + &host, + json!({ + "format": "json", + "metric": "members", + "path": "src/kinds.rs", + }), + ) + .await; + assert_eq!( + members, + json!({ + "gini": 0.25, + "interpretation": "moderate inequality", + "total_items": 2, + "metric": "members", + "scope": "file", + "incomplete_complexity_symbols": 0, + "outliers": [ + {"name": "Big", "value": 3.0, "pct_of_max": 100.0}, + {"name": "Tiny", "value": 1.0, "pct_of_max": 33.0}, + ], + }), + "struct member counts 1 and 3 must produce Gini 0.25: {members}" + ); + + let symbols = gini_json( + &host, + json!({ + "format": "json", + "metric": "complexity", + "scope": "symbol", + "path": "src/kinds.rs", + }), + ) + .await; + assert_eq!( + symbols, + json!({ + "gini": 0.3, + "interpretation": "moderate inequality", + "total_items": 2, + "metric": "complexity", + "scope": "symbol", + "incomplete_complexity_symbols": 0, + "outliers": [ + {"name": "src/kinds.rs:branched", "value": 4.0, "pct_of_max": 100.0}, + {"name": "src/kinds.rs:plain", "value": 1.0, "pct_of_max": 25.0}, + ], + }), + "symbol scores 1 and 4 must produce Gini 0.3: {symbols}" + ); + + close_test_graph(host).await; +} + +#[tokio::test] +async fn gini_empty_index_reports_perfect_equality() { + let (host, _, _) = setup_empty_analysis_project().await; + let payload = gini_json(&host, json!({"format": "json"})).await; + assert_eq!( + payload, + json!({ + "gini": 0.0, + "interpretation": "low inequality (healthy)", + "total_items": 0, + "metric": "complexity", + "scope": "file", + "incomplete_complexity_symbols": 0, + "outliers": [], + }), + "an empty index is not a missing field: {payload}" + ); + close_test_graph(host).await; +} + +// `tracedecay_hotspots` through production MCP `tools/call`. +// +// Occurrence ids are minted per project, so two equal totals may swap order +// across runs. The host-visible ranking of distinct degrees, the line and +// degree of each named symbol, the default page, the clamped page, and the +// zero-limit rejection are stable and asserted literally. + +const CHAIN_SOURCE: &str = "\ +export function quiet(): number {\n\ + return 0;\n\ +}\n\ +\n\ +export function leaf(): number {\n\ + return 1;\n\ +}\n\ +\n\ +export function mid(): number {\n\ + return leaf();\n\ +}\n\ +\n\ +export function hub(): number {\n\ + return mid();\n\ +}\n\ +"; + +fn write_package(project: &Path, name: &str) { + fs::create_dir_all(project.join("src")).unwrap(); + fs::write( + project.join("package.json"), + format!("{{\"name\":\"{name}\",\"private\":true,\"type\":\"module\"}}\n"), + ) + .unwrap(); +} + +fn write_chain_project(project: &Path) { + write_package(project, "hotspots-chain"); + fs::write(project.join("src/calls.ts"), CHAIN_SOURCE).unwrap(); +} + +/// `hub` plus 101 callers. Returns the source length the savings footer +/// measures for `src/fanout.ts`. +fn write_fanout_project(project: &Path) -> usize { + write_package(project, "hotspots-fanout"); + let mut source = String::from("export function hub(): number { return 1; }\n"); + for index in 0..101 { + source.push_str(&format!( + "export function caller{index}(): number {{ return hub(); }}\n" + )); + } + let bytes = source.len(); + fs::write(project.join("src/fanout.ts"), source).unwrap(); + bytes +} + +async fn call_hotspots(host: &MountedProductionProject, arguments: Value) -> Value { + let result = handle_tool_call(host, "tracedecay_hotspots", arguments, None, None) + .await + .unwrap_or_else(|error| panic!("tracedecay_hotspots failed over production MCP: {error}")); + result.value +} + +fn content(result: &Value) -> &[Value] { + result["content"] + .as_array() + .unwrap_or_else(|| panic!("hotspots content missing: {result}")) +} + +fn body_text(result: &Value) -> &str { + let item = &content(result)[0]; + assert_eq!(item["type"], "text", "{result}"); + item["text"] + .as_str() + .unwrap_or_else(|| panic!("hotspots text missing: {result}")) +} + +fn parse_body(result: &Value) -> Value { + let text = body_text(result); + serde_json::from_str(text) + .unwrap_or_else(|error| panic!("hotspots JSON did not parse: {error}\n{text}")) +} + +fn assert_savings_footer(result: &Value, source_bytes: usize) { + let items = content(result); + assert_eq!(items.len(), 2, "{result}"); + assert_eq!(items[1]["type"], "text", "{result}"); + let footer = items[1]["text"] + .as_str() + .unwrap_or_else(|| panic!("hotspots footer missing: {result}")); + assert_eq!( + footer, + format!( + "\ntracedecay_metrics: before={} after={}", + source_bytes / 4, + body_text(result).len() / 4 + ) + ); +} + +fn assert_symbol_id(id: &str) { + let prefix = "symbol.v1.sha256:"; + let Some(hex) = id.strip_prefix(prefix) else { + panic!("hotspot id {id} is not a sealed symbol occurrence"); + }; + assert_eq!(hex.len(), 64, "{id}"); + assert!( + hex.chars().all(|character| character.is_ascii_hexdigit()), + "{id}" + ); +} + +fn assert_exact_hotspot( + row: &Value, + name: &str, + file: &str, + line: u64, + incoming: u64, + outgoing: u64, + total: u64, +) { + let id = row["id"] + .as_str() + .unwrap_or_else(|| panic!("hotspot id missing: {row}")); + assert_symbol_id(id); + assert_eq!( + row, + &json!({ + "id": id, + "name": name, + "kind": "function", + "file": file, + "line": line, + "incoming": incoming, + "outgoing": outgoing, + "total": total, + }), + "{row}" + ); +} + +fn hotspots(payload: &Value) -> &[Value] { + let rows = payload["hotspots"] + .as_array() + .unwrap_or_else(|| panic!("hotspots array missing: {payload}")); + assert_eq!( + payload["hotspot_count"].as_u64(), + Some(u64::try_from(rows.len()).expect("hotspot count fits")), + "{payload}" + ); + rows +} + +fn assert_chain_ranking(payload: &Value) { + let rows = hotspots(payload); + assert_eq!(rows.len(), 4, "{payload}"); + assert_exact_hotspot(&rows[0], "mid", "src/calls.ts", 9, 1, 1, 2); + assert_exact_hotspot(&rows[3], "quiet", "src/calls.ts", 1, 0, 0, 0); + let mut tied = [rows[1].clone(), rows[2].clone()]; + tied.sort_by(|left, right| { + left["name"] + .as_str() + .unwrap_or("") + .cmp(right["name"].as_str().unwrap_or("")) + }); + assert_exact_hotspot(&tied[0], "hub", "src/calls.ts", 13, 0, 1, 1); + assert_exact_hotspot(&tied[1], "leaf", "src/calls.ts", 5, 1, 0, 1); + assert!( + rows.windows(2) + .all(|pair| pair[0]["total"].as_u64() >= pair[1]["total"].as_u64()), + "chain ranking is not highest degree first: {payload}" + ); +} + +fn assert_fanout_page(payload: &Value, expected_count: usize) { + let rows = hotspots(payload); + assert_eq!(rows.len(), expected_count, "{payload}"); + assert_exact_hotspot(&rows[0], "hub", "src/fanout.ts", 1, 101, 0, 101); + let mut seen = Vec::new(); + for row in rows.iter().skip(1) { + let name = row["name"] + .as_str() + .unwrap_or_else(|| panic!("caller name missing: {row}")); + let index: u64 = name + .strip_prefix("caller") + .unwrap_or_else(|| panic!("non-caller in the fan-out page: {row}")) + .parse() + .unwrap_or_else(|_| panic!("caller index missing: {row}")); + assert!(index < 101, "caller outside the fixture: {row}"); + assert_exact_hotspot(row, name, "src/fanout.ts", index + 2, 0, 1, 1); + seen.push(index); + } + seen.sort_unstable(); + seen.dedup(); + assert_eq!(seen.len(), expected_count - 1, "{payload}"); +} + +fn assert_clamped_truncation(payload: &Value) { + assert_eq!(payload["truncated"], true, "{payload}"); + assert_eq!(payload["retrieve_tool"], "tracedecay_retrieve", "{payload}"); + assert_eq!(payload["retrieve_ttl_seconds"], 86_400, "{payload}"); + let preview_chars = payload["preview_chars"] + .as_u64() + .unwrap_or_else(|| panic!("preview_chars missing: {payload}")); + assert_eq!(preview_chars, 11_928, "{payload}"); + let original_chars = payload["original_chars"] + .as_u64() + .unwrap_or_else(|| panic!("original_chars missing: {payload}")); + assert!( + original_chars > preview_chars, + "clamped body must not fit in the preview: {payload}" + ); + let preview = payload["preview"] + .as_str() + .unwrap_or_else(|| panic!("preview missing: {payload}")); + assert_eq!(preview.chars().count() as u64, preview_chars, "{preview}"); + let marker = r#"{"hotspot_count":100,"hotspots":["#; + let array = preview + .strip_prefix(marker) + .unwrap_or_else(|| panic!("clamped preview did not start with 100 rows: {preview}")); + assert!( + array.starts_with('{'), + "clamped preview omitted the hub object: {preview}" + ); + let mut depth = 0_i32; + let mut end = None; + for (index, byte) in array.bytes().enumerate() { + match byte { + b'{' => depth += 1, + b'}' => { + depth -= 1; + if depth == 0 { + end = Some(index); + break; + } + } + _ => {} + } + } + let end = end.unwrap_or_else(|| panic!("clamped hub object was cut off: {preview}")); + let first: Value = serde_json::from_str(&array[..=end]).unwrap_or_else(|error| { + panic!( + "clamped hub object did not parse: {error}\n{}", + &array[..=end] + ) + }); + assert_exact_hotspot(&first, "hub", "src/fanout.ts", 1, 101, 0, 101); + + let handle = payload["handle"] + .as_str() + .unwrap_or_else(|| panic!("truncation handle missing: {payload}")); + assert!( + handle.starts_with("rh_") && handle.len() > "rh_".len(), + "{payload}" + ); + let expires = payload["retrieve_expires_at"] + .as_i64() + .unwrap_or_else(|| panic!("retrieve expiry missing: {payload}")); + let instruction = payload["retrieve_instruction"] + .as_str() + .unwrap_or_else(|| panic!("retrieve instruction missing: {payload}")); + assert!(instruction.contains(handle), "{instruction}"); + assert!(instruction.contains(&expires.to_string()), "{instruction}"); + assert!( + instruction.contains(&preview_chars.to_string()), + "{instruction}" + ); + assert!( + instruction.contains(&original_chars.to_string()), + "{instruction}" + ); + assert!(instruction.contains("tracedecay_retrieve"), "{instruction}"); +} + +#[tokio::test] +async fn hotspots_ranks_symbols_by_edge_degree_and_clamps_limit() { + let chain_dir = test_temp_dir(); + let chain_root = chain_dir.path().join("project"); + write_chain_project(&chain_root); + let (chain, _env) = init_test_project(&chain_root).await; + + let chain_default = call_hotspots(&chain, json!({"format": "json"})).await; + let chain_limit_one = call_hotspots(&chain, json!({"format": "json", "limit": 1})).await; + let chain_markdown = call_hotspots(&chain, json!({"format": "markdown", "limit": 1})).await; + let chain_rejected = chain + .harness + .call_tool( + &chain.project_root, + "tracedecay_hotspots", + json!({"limit": 0, "format": "json"}), + ) + .await + .expect("zero limit still reaches the MCP server"); + close_test_graph(chain).await; + + let chain_default_payload = parse_body(&chain_default); + assert_chain_ranking(&chain_default_payload); + assert_savings_footer(&chain_default, CHAIN_SOURCE.len()); + + let chain_one_payload = parse_body(&chain_limit_one); + let one = hotspots(&chain_one_payload); + assert_eq!(one.len(), 1, "{chain_one_payload}"); + assert_exact_hotspot(&one[0], "mid", "src/calls.ts", 9, 1, 1, 2); + assert_savings_footer(&chain_limit_one, CHAIN_SOURCE.len()); + + let mid_id = one[0]["id"].as_str().expect("mid occurrence id").to_owned(); + assert_eq!( + body_text(&chain_markdown), + format!( + "**hotspot_count:** 1\n\n## hotspots\n- **mid**\n **kind:** function\n **file:** src/calls.ts\n **line:** 9\n **id:** `{mid_id}`\n **incoming:** 1\n **outgoing:** 1\n **total:** 2\n" + ) + ); + assert_savings_footer(&chain_markdown, CHAIN_SOURCE.len()); + + let rejected = chain_rejected.error.expect("zero limit is a tool error"); + assert_eq!(rejected.code, -32603); + assert_eq!( + rejected.message, + "tool execution failed: config error: invalid parameter: tracedecay_hotspots requires limit to be at least 1" + ); + assert_eq!( + rejected.data, + Some(json!({ + "tool": "tracedecay_hotspots", + "cli_fallback": "This tool is also available from the shell: `tracedecay tool hotspots ...` (`tracedecay tool hotspots --help` for parameters). If MCP calls keep failing or timing out, fall back to that CLI instead of querying .tracedecay databases directly." + })) + ); + + let fanout_dir = test_temp_dir(); + let fanout_root = fanout_dir.path().join("project"); + let fanout_bytes = write_fanout_project(&fanout_root); + let (fanout, _env) = init_test_project(&fanout_root).await; + let fanout_default = call_hotspots(&fanout, json!({"format": "json"})).await; + let fanout_capped = call_hotspots(&fanout, json!({"format": "json", "limit": 250})).await; + let fanout_one = call_hotspots(&fanout, json!({"format": "json", "limit": 1})).await; + close_test_graph(fanout).await; + + let fanout_default_payload = parse_body(&fanout_default); + assert_fanout_page(&fanout_default_payload, 10); + assert_savings_footer(&fanout_default, fanout_bytes); + + let fanout_one_payload = parse_body(&fanout_one); + let fanout_top = hotspots(&fanout_one_payload); + assert_eq!(fanout_top.len(), 1, "{fanout_one_payload}"); + assert_exact_hotspot(&fanout_top[0], "hub", "src/fanout.ts", 1, 101, 0, 101); + assert_savings_footer(&fanout_one, fanout_bytes); + + assert_clamped_truncation(&parse_body(&fanout_capped)); + assert_savings_footer(&fanout_capped, fanout_bytes); +} + +// Literal `tracedecay_recursion` results from production MCP `tools/call`. +// +// `length` is the number of call edges in the cycle. The chain repeats its +// start symbol so the path is closed. Occurrence ids include the temp +// project path, so which node the search starts on changes between runs. +// Comparisons rotate each chain to its smallest `(name, file, line)` and +// then pin names, kinds, files, and lines. Ids are still required to close +// the cycle. + +const DIRECT_SOURCE: &str = "\ +pub fn recurse(n: u32) -> u32 { + if n == 0 { 0 } else { recurse(n - 1) } +} + +pub fn leaf() -> u32 { 1 } +"; + +const MUTUAL_SOURCE: &str = "\ +pub fn ping() { pong(); } +pub fn pong() { ping(); } +"; + +const NOISE_SOURCE: &str = "\ +pub struct Triplet { + rows: Vec, +} + +impl Triplet { + pub fn push(&mut self, row: usize) { + self.rows.push(row); + } +} +"; + +fn direct_cycle() -> Value { + json!({ + "length": 1, + "chain": [ + {"name": "recurse", "kind": "function", "file": "src/direct.rs", "line": 1}, + {"name": "recurse", "kind": "function", "file": "src/direct.rs", "line": 1} + ] + }) +} + +fn mutual_cycle() -> Value { + json!({ + "length": 2, + "chain": [ + {"name": "ping", "kind": "function", "file": "src/mutual.rs", "line": 1}, + {"name": "pong", "kind": "function", "file": "src/mutual.rs", "line": 2}, + {"name": "ping", "kind": "function", "file": "src/mutual.rs", "line": 1} + ] + }) +} + +fn full_report() -> Value { + json!({ + "cycle_count": 2, + "cycles": [direct_cycle(), mutual_cycle()] + }) +} + +fn public_recursion_report(payload: &Value) -> Value { + let keys = sorted_keys(payload, "recursion payload"); + assert_eq!( + keys, + ["cycle_count", "cycles"], + "recursion payload keys drifted: {payload}" + ); + let cycles = payload["cycles"] + .as_array() + .unwrap_or_else(|| panic!("cycles must be an array: {payload}")); + let cycles = cycles + .iter() + .map(|cycle| { + let cycle_keys = sorted_keys(cycle, "cycle"); + assert_eq!( + cycle_keys, + ["chain", "length"], + "cycle keys drifted: {cycle}" + ); + let chain = cycle["chain"] + .as_array() + .unwrap_or_else(|| panic!("chain must be an array: {cycle}")); + json!({ + "length": cycle["length"], + "chain": canonical_public_chain(chain), + }) + }) + .collect::>(); + let mut cycles = cycles; + cycles.sort_by(|left, right| cycle_order_key(left).cmp(&cycle_order_key(right))); + json!({ + "cycle_count": payload["cycle_count"], + "cycles": cycles, + }) +} + +fn cycle_order_key(cycle: &Value) -> (i64, String) { + ( + cycle["length"].as_i64().unwrap_or(i64::MAX), + cycle["chain"].to_string(), + ) +} + +fn canonical_public_chain(chain: &[Value]) -> Vec { + let public = chain.iter().map(public_chain_node).collect::>(); + assert!( + public.len() >= 2, + "a cycle chain must repeat its start: {public:?}" + ); + assert_eq!( + public.first(), + public.last(), + "a cycle chain must close on the same symbol: {public:?}" + ); + let body = &public[..public.len() - 1]; + let start = body + .iter() + .enumerate() + .min_by(|(_, left), (_, right)| public_node_order(left).cmp(&public_node_order(right))) + .map(|(index, _)| index) + .expect("a cycle body is non-empty"); + let mut rotated = body[start..] + .iter() + .chain(&body[..start]) + .cloned() + .collect::>(); + rotated.push(rotated[0].clone()); + rotated +} + +fn public_node_order(node: &Value) -> (String, String, i64) { + ( + node["name"].as_str().unwrap_or_default().to_owned(), + node["file"].as_str().unwrap_or_default().to_owned(), + node["line"].as_i64().unwrap_or(i64::MAX), + ) +} + +fn sorted_keys<'a>(value: &'a Value, label: &str) -> Vec<&'a str> { + let mut keys = value + .as_object() + .unwrap_or_else(|| panic!("{label} must be an object: {value}")) + .keys() + .map(String::as_str) + .collect::>(); + keys.sort_unstable(); + keys +} + +fn public_chain_node(node: &Value) -> Value { + let keys = sorted_keys(node, "chain node"); + assert_eq!( + keys, + ["file", "id", "kind", "line", "name"], + "chain node keys drifted: {node}" + ); + assert!( + node["id"].as_str().is_some_and(|id| !id.is_empty()), + "chain node id must be a non-empty string: {node}" + ); + json!({ + "name": node["name"], + "kind": node["kind"], + "file": node["file"], + "line": node["line"], + }) +} + +fn assert_reported_cycles_close(payload: &Value) { + let cycles = payload["cycles"] + .as_array() + .unwrap_or_else(|| panic!("cycles must be an array: {payload}")); + for cycle in cycles { + let chain = cycle["chain"] + .as_array() + .unwrap_or_else(|| panic!("chain must be an array: {cycle}")); + let start = chain + .first() + .and_then(|node| node["id"].as_str()) + .unwrap_or_else(|| panic!("cycle is missing its start id: {cycle}")); + let end = chain + .last() + .and_then(|node| node["id"].as_str()) + .unwrap_or_else(|| panic!("cycle is missing its closing id: {cycle}")); + assert_eq!( + start, end, + "a reported cycle must return to its start symbol: {cycle}" + ); + } +} + +async fn call_recursion(graph: &impl AnalysisToolHost, arguments: Value) -> Value { + let result = handle_tool_call(graph, "tracedecay_recursion", arguments, None, None) + .await + .unwrap_or_else(|error| panic!("tracedecay_recursion failed: {error}")); + extract_json(&result.value) +} + +#[tokio::test] +async fn recursion_reports_literal_cycles_and_refuses_non_positive_limit() { + let dir = test_temp_dir(); + let project_root = dir.path().join("project"); + fs_write_fixture(&project_root); + let (graph, ()) = init_test_project(&project_root).await; + + let payload = call_recursion(&graph, json!({"format": "json", "limit": 10})).await; + assert_eq!( + public_recursion_report(&payload), + full_report(), + "default-sized recursion report: {payload}" + ); + assert_reported_cycles_close(&payload); + + let scoped = call_recursion(&graph, json!({"format": "json", "path": "src/direct.rs"})).await; + assert_eq!( + public_recursion_report(&scoped), + json!({"cycle_count": 1, "cycles": [direct_cycle()]}), + "path filter must keep only the direct cycle: {scoped}" + ); + + let mutual = call_recursion( + &graph, + json!({"format": "json", "path": "src/mutual.rs", "limit": 10}), + ) + .await; + assert_eq!( + public_recursion_report(&mutual), + json!({"cycle_count": 1, "cycles": [mutual_cycle()]}), + "path filter must keep only the mutual cycle: {mutual}" + ); + + let noise = call_recursion(&graph, json!({"format": "json", "path": "src/noise.rs"})).await; + assert_eq!( + public_recursion_report(&noise), + json!({"cycle_count": 0, "cycles": []}), + "receiver `.push` must not be a cycle when the same graph has real cycles: {noise}" + ); + + let limited = call_recursion(&graph, json!({"format": "json", "limit": 1})).await; + assert_eq!( + public_recursion_report(&limited), + json!({"cycle_count": 1, "cycles": [direct_cycle()]}), + "limit 1 keeps the shortest cycle: {limited}" + ); + + let error = expect_tool_error( + handle_tool_call( + &graph, + "tracedecay_recursion", + json!({"format": "json", "limit": 0}), + None, + None, + ) + .await, + ); + assert_eq!( + error, + "config error: tracedecay_recursion failed over production MCP: tool execution failed: config error: invalid parameter: tracedecay_recursion requires limit to be at least 1" + ); + close_test_graph(graph).await; +} + +fn fs_write_fixture(project_root: &Path) { + std::fs::create_dir_all(project_root.join("src")).unwrap(); + std::fs::write( + project_root.join("src/lib.rs"), + "pub mod direct;\npub mod mutual;\npub mod noise;\n", + ) + .unwrap(); + std::fs::write(project_root.join("src/direct.rs"), DIRECT_SOURCE).unwrap(); + std::fs::write(project_root.join("src/mutual.rs"), MUTUAL_SOURCE).unwrap(); + std::fs::write(project_root.join("src/noise.rs"), NOISE_SOURCE).unwrap(); +} diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/graph_analysis_test/diff_context_behavior.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/graph_analysis_test/diff_context_behavior.rs deleted file mode 100644 index 2386e449b9..0000000000 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/graph_analysis_test/diff_context_behavior.rs +++ /dev/null @@ -1,262 +0,0 @@ -//! `tracedecay_diff_context` as an agent host observes it: one production -//! MCP `tools/call`, then the JSON text the caller reads. -//! -//! The fixture is one crate. `tier_b` calls `tier_c`, and `tier_a` calls -//! `tier_b`. `#[test]` on `checks_tier_c` is itself a modified symbol -//! (`annotation_usage` named `test`). Lines are the extractor's 0-based -//! tree-sitter rows. - -use super::{close_test_graph, handle_tool_call, init_test_project}; -use crate::support::{extract_json, test_temp_dir}; -use serde_json::{Value, json}; -use std::fs; -use std::path::Path; - -const LIB_RS: &str = "mod tier_a;\nmod tier_b;\nmod tier_c;\n"; -const TIER_A_RS: &str = "use crate::tier_b::tier_b;\n\npub fn tier_a() -> u8 {\n tier_b()\n}\n"; -const TIER_B_RS: &str = "use crate::tier_c::tier_c;\n\npub fn tier_b() -> u8 {\n tier_c()\n}\n"; -const TIER_C_RS: &str = "\ -pub fn tier_c() -> u8 {\n\ - 1\n\ -}\n\ -\n\ -#[test]\n\ -fn checks_tier_c() {\n\ - let _ = tier_c();\n\ -}\n"; - -fn symbol_facts(symbols: &Value) -> Vec { - let Some(symbols) = symbols.as_array() else { - panic!("diff_context symbol list is not an array: {symbols}"); - }; - let mut facts = symbols - .iter() - .map(|symbol| { - json!({ - "name": symbol["name"], - "kind": symbol["kind"], - "file": symbol["file"], - "line": symbol["line"], - }) - }) - .collect::>(); - facts.sort_by(|left, right| { - ( - left["file"].as_str(), - left["name"].as_str(), - left["line"].as_u64(), - ) - .cmp(&( - right["file"].as_str(), - right["name"].as_str(), - right["line"].as_u64(), - )) - }); - facts -} - -fn write_call_chain(project: &Path) { - fs::create_dir_all(project.join("src")).unwrap(); - fs::write(project.join("src/lib.rs"), LIB_RS).unwrap(); - fs::write(project.join("src/tier_a.rs"), TIER_A_RS).unwrap(); - fs::write(project.join("src/tier_b.rs"), TIER_B_RS).unwrap(); - fs::write(project.join("src/tier_c.rs"), TIER_C_RS).unwrap(); -} - -#[tokio::test] -async fn diff_context_reports_changed_symbols_callers_and_refuses_invalid_input() { - let dir = test_temp_dir(); - let project = dir.path().join("project"); - write_call_chain(&project); - let (host, _) = init_test_project(&project).await; - - let changed = handle_tool_call( - &host, - "tracedecay_diff_context", - json!({"files": ["src/tier_c.rs"], "depth": 1, "format": "json"}), - None, - None, - ) - .await - .expect("depth-1 diff_context"); - let changed = extract_json(&changed.value); - assert_eq!(changed["changed_files"], json!(["src/tier_c.rs"])); - // `tier_a` calls `tier_b`, so a depth-1 walk stops with callers still - // unexplored. The tool must say so instead of pretending the radius is - // complete. - assert_eq!(changed["impact_complete"], json!(false), "{changed}"); - assert_eq!( - symbol_facts(&changed["modified_symbols"]), - vec![ - json!({"name": "checks_tier_c", "kind": "function", "file": "src/tier_c.rs", "line": 5}), - json!({"name": "test", "kind": "annotation_usage", "file": "src/tier_c.rs", "line": 4}), - json!({"name": "tier_c", "kind": "function", "file": "src/tier_c.rs", "line": 0}), - ], - "modified symbols: {changed}" - ); - assert_eq!(changed["impacted_symbols_count"], json!(1)); - assert_eq!( - symbol_facts(&changed["impacted_symbols"]), - vec![json!({"name": "tier_b", "kind": "function", "file": "src/tier_b.rs", "line": 2}),], - "direct callers of tier_c: {changed}" - ); - assert_eq!( - changed["affected_tests"], - json!(["src/tier_c.rs"]), - "{changed}" - ); - - let wider = handle_tool_call( - &host, - "tracedecay_diff_context", - json!({"files": ["src/tier_c.rs"], "depth": 2, "format": "json"}), - None, - None, - ) - .await - .expect("depth-2 diff_context"); - let wider = extract_json(&wider.value); - assert_eq!(wider["impact_complete"], json!(true), "{wider}"); - assert_eq!(wider["impacted_symbols_count"], json!(2)); - assert_eq!( - symbol_facts(&wider["impacted_symbols"]), - vec![ - json!({"name": "tier_a", "kind": "function", "file": "src/tier_a.rs", "line": 2}), - json!({"name": "tier_b", "kind": "function", "file": "src/tier_b.rs", "line": 2}), - ], - "depth 2 also reaches tier_a, which only calls tier_b: {wider}" - ); - - let duplicated = handle_tool_call( - &host, - "tracedecay_diff_context", - json!({ - "files": ["src/tier_c.rs", "src/tier_c.rs"], - "depth": 1, - "format": "json" - }), - None, - None, - ) - .await - .expect("duplicate-path diff_context"); - let duplicated = extract_json(&duplicated.value); - assert_eq!(duplicated["changed_files"], json!(["src/tier_c.rs"])); - assert_eq!( - symbol_facts(&duplicated["modified_symbols"]), - vec![ - json!({"name": "checks_tier_c", "kind": "function", "file": "src/tier_c.rs", "line": 5}), - json!({"name": "test", "kind": "annotation_usage", "file": "src/tier_c.rs", "line": 4}), - json!({"name": "tier_c", "kind": "function", "file": "src/tier_c.rs", "line": 0}), - ] - ); - assert_eq!( - symbol_facts(&duplicated["impacted_symbols"]), - vec![json!({"name": "tier_b", "kind": "function", "file": "src/tier_b.rs", "line": 2}),] - ); - - // A path this generation never published carries no symbols, so the - // affected-test walk has no seeds and answers empty and complete rather - // than turning "no such file here" into an invalid request. - let absent = handle_tool_call( - &host, - "tracedecay_diff_context", - json!({"files": ["src/not_in_repo.rs"], "format": "json"}), - None, - None, - ) - .await - .expect("unpublished-path diff_context"); - assert_eq!( - extract_json(&absent.value), - json!({ - "changed_files": ["src/not_in_repo.rs"], - "modified_symbols": [], - "impacted_symbols_count": 0, - "impacted_symbols": [], - "impact_complete": true, - "affected_tests": [] - }) - ); - - let empty_files = handle_tool_call( - &host, - "tracedecay_diff_context", - json!({"files": [], "format": "json"}), - None, - None, - ) - .await - .expect("empty-files diff_context"); - assert_eq!( - extract_json(&empty_files.value), - json!({ - "changed_files": [], - "modified_symbols": [], - "impacted_symbols_count": 0, - "impacted_symbols": [], - "impact_complete": true, - "affected_tests": [] - }) - ); - - let missing_files = handle_tool_call( - &host, - "tracedecay_diff_context", - json!({"format": "json"}), - None, - None, - ) - .await; - assert_eq!( - missing_files - .expect_err("missing files must be refused") - .to_string(), - "config error: tracedecay_diff_context failed over production MCP: missing required parameter: files (array of strings)" - ); - - let files_not_array = handle_tool_call( - &host, - "tracedecay_diff_context", - json!({"files": "src/tier_c.rs", "format": "json"}), - None, - None, - ) - .await; - assert_eq!( - files_not_array - .expect_err("a string files argument must be refused") - .to_string(), - "config error: tracedecay_diff_context failed over production MCP: missing required parameter: files (array of strings)" - ); - - let not_object = handle_tool_call( - &host, - "tracedecay_diff_context", - json!(["src/tier_c.rs"]), - None, - None, - ) - .await; - assert_eq!( - not_object - .expect_err("non-object arguments must be refused") - .to_string(), - "config error: tracedecay_diff_context failed over production MCP: tool execution failed: config error: invalid arguments: tracedecay_diff_context expects a JSON object" - ); - - let zero_depth = handle_tool_call( - &host, - "tracedecay_diff_context", - json!({"files": ["src/tier_c.rs"], "depth": 0, "format": "json"}), - None, - None, - ) - .await; - assert_eq!( - zero_depth.expect_err("depth 0 must be refused").to_string(), - "config error: tracedecay_diff_context failed over production MCP: tool project route failed: reason_code=code-graph-invalid-request retryable=false: the code-graph read request is invalid: code graph impact depth must be positive" - ); - - close_test_graph(host).await; -} diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/graph_analysis_test/gini.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/graph_analysis_test/gini.rs deleted file mode 100644 index c290d998ff..0000000000 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/graph_analysis_test/gini.rs +++ /dev/null @@ -1,282 +0,0 @@ -//! Literal `tracedecay_gini` results for a fixture whose metric values are -//! fixed by source shape, not by reading the coefficient back out of the tool. -//! -//! The handler rounds `2*Σ(i*x_i)/(n*Σx) - (n+1)/n` (1-indexed `i` on values -//! sorted ascending) to four decimals. One file, or a missing path, is the -//! empty-or-singleton case and the coefficient is exactly 0. - -use std::path::Path; - -use serde_json::{Value, json}; - -use crate::support::{extract_json, production_composition_fixture_with_sources}; - -use super::{AnalysisToolHost, close_test_graph, handle_tool_call, setup_empty_analysis_project}; - -fn write_gini_distribution_sources(project: &Path) { - std::fs::create_dir_all(project.join("src/spans")).unwrap(); - // Body is one block and no branch: complexity 1, line span 1. - std::fs::write( - project.join("src/spans/short.rs"), - "pub fn short() -> i32 { 1 }\n", - ) - .unwrap(); - // Same complexity 1, line span 3 (declaration, body, closing brace). - std::fs::write( - project.join("src/spans/tall.rs"), - "pub fn tall() -> i32 {\n 1\n}\n", - ) - .unwrap(); - // Tiny has one field, Big has three. `plain` is a single block (complexity - // 1). `branched` is if + else (2 branches) inside a body block, so the - // inner blocks reach nesting 2 and the symbol score is 4. - std::fs::write( - project.join("src/kinds.rs"), - "\ -pub struct Tiny {\n \ - pub only: i32,\n\ -}\n\ -\n\ -pub struct Big {\n \ - pub a: i32,\n \ - pub b: i32,\n \ - pub c: i32,\n\ -}\n\ -\n\ -pub fn plain() -> i32 { 1 }\n\ -\n\ -pub fn branched(n: i32) -> i32 {\n \ - if n > 0 {\n \ - n\n \ - } else {\n \ - 0\n \ - }\n\ -}\n", - ) - .unwrap(); -} - -async fn gini_json(host: &impl AnalysisToolHost, args: Value) -> Value { - let result = handle_tool_call(host, "tracedecay_gini", args, None, None) - .await - .expect("tracedecay_gini over production MCP"); - extract_json(&result.value) -} - -/// Equal values do not define an outlier rank. Sort by name so the assertion -/// stays on the reported rows rather than `HashMap` iteration order. -fn outliers_sorted_by_name(mut payload: Value) -> Value { - if let Some(outliers) = payload.get_mut("outliers").and_then(Value::as_array_mut) { - outliers.sort_by(|left, right| left["name"].as_str().cmp(&right["name"].as_str())); - } - payload -} - -#[tokio::test] -async fn gini_reports_literal_coefficients_for_known_distributions() { - let host = production_composition_fixture_with_sources(write_gini_distribution_sources).await; - - let lines = gini_json( - &host, - json!({ - "format": "json", - "metric": "lines", - "scope": "file", - "path": "src/spans", - }), - ) - .await; - assert_eq!( - lines, - json!({ - "gini": 0.25, - "interpretation": "moderate inequality", - "total_items": 2, - "metric": "lines", - "scope": "file", - "incomplete_complexity_symbols": 0, - "outliers": [ - {"name": "src/spans/tall.rs", "value": 3.0, "pct_of_max": 100.0}, - {"name": "src/spans/short.rs", "value": 1.0, "pct_of_max": 33.0}, - ], - }), - "line spans 1 and 3 must produce Gini 0.25: {lines}" - ); - - let truncated = gini_json( - &host, - json!({ - "format": "json", - "metric": "lines", - "scope": "file", - "path": "src/spans", - "limit": 1, - }), - ) - .await; - assert_eq!( - truncated, - json!({ - "gini": 0.25, - "interpretation": "moderate inequality", - "total_items": 2, - "metric": "lines", - "scope": "file", - "incomplete_complexity_symbols": 0, - "outliers": [ - {"name": "src/spans/tall.rs", "value": 3.0, "pct_of_max": 100.0}, - ], - }), - "limit truncates the ranking and keeps the census: {truncated}" - ); - - let one_file = gini_json( - &host, - json!({ - "format": "json", - "metric": "lines", - "path": "src/spans/tall.rs", - }), - ) - .await; - assert_eq!( - one_file, - json!({ - "gini": 0.0, - "interpretation": "low inequality (healthy)", - "total_items": 1, - "metric": "lines", - "scope": "file", - "incomplete_complexity_symbols": 0, - "outliers": [ - {"name": "src/spans/tall.rs", "value": 3.0, "pct_of_max": 100.0}, - ], - }), - "a single file is perfect equality: {one_file}" - ); - - let missing = gini_json( - &host, - json!({ - "format": "json", - "metric": "lines", - "path": "src/nowhere", - }), - ) - .await; - assert_eq!( - missing, - json!({ - "gini": 0.0, - "interpretation": "low inequality (healthy)", - "total_items": 0, - "metric": "lines", - "scope": "file", - "incomplete_complexity_symbols": 0, - "outliers": [], - }), - "a path with no symbols is an empty census, not the unfiltered one: {missing}" - ); - - // Defaults are complexity + file. Both span functions score 1, so this - // coefficient is 0. The lines call above is 0.25 for the same path. - let defaults = gini_json( - &host, - json!({ - "format": "json", - "path": "src/spans", - }), - ) - .await; - assert_eq!( - outliers_sorted_by_name(defaults.clone()), - json!({ - "gini": 0.0, - "interpretation": "low inequality (healthy)", - "total_items": 2, - "metric": "complexity", - "scope": "file", - "incomplete_complexity_symbols": 0, - "outliers": [ - {"name": "src/spans/short.rs", "value": 1.0, "pct_of_max": 100.0}, - {"name": "src/spans/tall.rs", "value": 1.0, "pct_of_max": 100.0}, - ], - }), - "default metric is complexity, not lines: {defaults}" - ); - - let members = gini_json( - &host, - json!({ - "format": "json", - "metric": "members", - "path": "src/kinds.rs", - }), - ) - .await; - assert_eq!( - members, - json!({ - "gini": 0.25, - "interpretation": "moderate inequality", - "total_items": 2, - "metric": "members", - "scope": "file", - "incomplete_complexity_symbols": 0, - "outliers": [ - {"name": "Big", "value": 3.0, "pct_of_max": 100.0}, - {"name": "Tiny", "value": 1.0, "pct_of_max": 33.0}, - ], - }), - "struct member counts 1 and 3 must produce Gini 0.25: {members}" - ); - - let symbols = gini_json( - &host, - json!({ - "format": "json", - "metric": "complexity", - "scope": "symbol", - "path": "src/kinds.rs", - }), - ) - .await; - assert_eq!( - symbols, - json!({ - "gini": 0.3, - "interpretation": "moderate inequality", - "total_items": 2, - "metric": "complexity", - "scope": "symbol", - "incomplete_complexity_symbols": 0, - "outliers": [ - {"name": "src/kinds.rs:branched", "value": 4.0, "pct_of_max": 100.0}, - {"name": "src/kinds.rs:plain", "value": 1.0, "pct_of_max": 25.0}, - ], - }), - "symbol scores 1 and 4 must produce Gini 0.3: {symbols}" - ); - - close_test_graph(host).await; -} - -#[tokio::test] -async fn gini_empty_index_reports_perfect_equality() { - let (host, _, _) = setup_empty_analysis_project().await; - let payload = gini_json(&host, json!({"format": "json"})).await; - assert_eq!( - payload, - json!({ - "gini": 0.0, - "interpretation": "low inequality (healthy)", - "total_items": 0, - "metric": "complexity", - "scope": "file", - "incomplete_complexity_symbols": 0, - "outliers": [], - }), - "an empty index is not a missing field: {payload}" - ); - close_test_graph(host).await; -} diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/graph_analysis_test/graph_readiness.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/graph_analysis_test/graph_readiness.rs deleted file mode 100644 index 075d8352e3..0000000000 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/graph_analysis_test/graph_readiness.rs +++ /dev/null @@ -1,78 +0,0 @@ -use std::time::Duration; - -use serde_json::{Value, json}; - -use crate::support::extract_text; - -use super::{AnalysisToolHost, handle_tool_call}; - -pub(super) async fn wait_for_current_graph(host: &impl AnalysisToolHost) { - tokio::time::timeout(Duration::from_secs(20), async { - loop { - let status = handle_tool_call( - host, - "tracedecay_status", - json!({ - "format": "json", - "include_branch_diagnostics": false, - "include_storage_health": false, - "include_session_ingest": false, - "include_staleness": false, - }), - None, - None, - ) - .await - .expect("typed project status while awaiting the current graph"); - let status: Value = serde_json::from_str(extract_text(&status.value)) - .expect("typed project status JSON"); - let freshness = &status["code_index_freshness"]; - let serving = &freshness["worktree"]["code_graph_serving"]; - match ( - freshness["status"].as_str(), - serving["state"].as_str(), - serving["reason"].as_str(), - freshness["worktree"]["staleness_state"].as_str(), - ) { - (Some("current"), Some("ready"), _, _) => break, - (Some("warming"), _, _, _) - | (Some("stale"), Some("ready"), _, Some("verifying")) - | (_, Some("pending"), _, _) - | (_, Some("unavailable"), Some("generation_unavailable"), _) => { - tokio::task::yield_now().await; - } - (_, Some("refused"), _, _) | (_, _, Some("activation_disabled"), _) => { - panic!("graph readiness was refused: {status}"); - } - actual => panic!("graph readiness became {actual:?}: {status}"), - } - } - }) - .await - .expect("graph did not become current within the publication budget"); -} - -pub(super) async fn find_node_id(host: &impl AnalysisToolHost, name: &str) -> String { - wait_for_current_graph(host).await; - let result = handle_tool_call( - host, - "tracedecay_find_exact_symbol", - json!({"name": name, "limit": 20}), - None, - None, - ) - .await - .unwrap_or_else(|error| panic!("production exact-symbol read failed: {error}")); - let payload: Value = - serde_json::from_str(extract_text(&result.value)).expect("exact-symbol JSON"); - payload["matches"] - .as_array() - .and_then(|matches| { - matches - .iter() - .find(|result| result["name"].as_str() == Some(name)) - }) - .and_then(|result| result["id"].as_str()) - .unwrap_or_else(|| panic!("node '{name}' not found in production generation: {payload}")) - .to_owned() -} diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/graph_analysis_test/hotspots.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/graph_analysis_test/hotspots.rs deleted file mode 100644 index d313de4eac..0000000000 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/graph_analysis_test/hotspots.rs +++ /dev/null @@ -1,356 +0,0 @@ -//! `tracedecay_hotspots` through production MCP `tools/call`. -//! -//! Occurrence ids are minted per project, so two equal totals may swap order -//! across runs. The host-visible ranking of distinct degrees, the line and -//! degree of each named symbol, the default page, the clamped page, and the -//! zero-limit rejection are stable and asserted literally. - -use std::fs; -use std::path::Path; - -use serde_json::{Value, json}; - -use super::{MountedProductionProject, close_test_graph, handle_tool_call, init_test_project}; -use crate::support::test_temp_dir; - -const CHAIN_SOURCE: &str = "\ -export function quiet(): number {\n\ - return 0;\n\ -}\n\ -\n\ -export function leaf(): number {\n\ - return 1;\n\ -}\n\ -\n\ -export function mid(): number {\n\ - return leaf();\n\ -}\n\ -\n\ -export function hub(): number {\n\ - return mid();\n\ -}\n\ -"; - -fn write_package(project: &Path, name: &str) { - fs::create_dir_all(project.join("src")).unwrap(); - fs::write( - project.join("package.json"), - format!("{{\"name\":\"{name}\",\"private\":true,\"type\":\"module\"}}\n"), - ) - .unwrap(); -} - -fn write_chain_project(project: &Path) { - write_package(project, "hotspots-chain"); - fs::write(project.join("src/calls.ts"), CHAIN_SOURCE).unwrap(); -} - -/// `hub` plus 101 callers. Returns the source length the savings footer -/// measures for `src/fanout.ts`. -fn write_fanout_project(project: &Path) -> usize { - write_package(project, "hotspots-fanout"); - let mut source = String::from("export function hub(): number { return 1; }\n"); - for index in 0..101 { - source.push_str(&format!( - "export function caller{index}(): number {{ return hub(); }}\n" - )); - } - let bytes = source.len(); - fs::write(project.join("src/fanout.ts"), source).unwrap(); - bytes -} - -async fn call_hotspots(host: &MountedProductionProject, arguments: Value) -> Value { - let result = handle_tool_call(host, "tracedecay_hotspots", arguments, None, None) - .await - .unwrap_or_else(|error| panic!("tracedecay_hotspots failed over production MCP: {error}")); - result.value -} - -fn content(result: &Value) -> &[Value] { - result["content"] - .as_array() - .unwrap_or_else(|| panic!("hotspots content missing: {result}")) -} - -fn body_text(result: &Value) -> &str { - let item = &content(result)[0]; - assert_eq!(item["type"], "text", "{result}"); - item["text"] - .as_str() - .unwrap_or_else(|| panic!("hotspots text missing: {result}")) -} - -fn parse_body(result: &Value) -> Value { - let text = body_text(result); - serde_json::from_str(text) - .unwrap_or_else(|error| panic!("hotspots JSON did not parse: {error}\n{text}")) -} - -fn assert_savings_footer(result: &Value, source_bytes: usize) { - let items = content(result); - assert_eq!(items.len(), 2, "{result}"); - assert_eq!(items[1]["type"], "text", "{result}"); - let footer = items[1]["text"] - .as_str() - .unwrap_or_else(|| panic!("hotspots footer missing: {result}")); - assert_eq!( - footer, - format!( - "\ntracedecay_metrics: before={} after={}", - source_bytes / 4, - body_text(result).len() / 4 - ) - ); -} - -fn assert_symbol_id(id: &str) { - let prefix = "symbol.v1.sha256:"; - let Some(hex) = id.strip_prefix(prefix) else { - panic!("hotspot id {id} is not a sealed symbol occurrence"); - }; - assert_eq!(hex.len(), 64, "{id}"); - assert!( - hex.chars().all(|character| character.is_ascii_hexdigit()), - "{id}" - ); -} - -fn assert_exact_hotspot( - row: &Value, - name: &str, - file: &str, - line: u64, - incoming: u64, - outgoing: u64, - total: u64, -) { - let id = row["id"] - .as_str() - .unwrap_or_else(|| panic!("hotspot id missing: {row}")); - assert_symbol_id(id); - assert_eq!( - row, - &json!({ - "id": id, - "name": name, - "kind": "function", - "file": file, - "line": line, - "incoming": incoming, - "outgoing": outgoing, - "total": total, - }), - "{row}" - ); -} - -fn hotspots(payload: &Value) -> &[Value] { - let rows = payload["hotspots"] - .as_array() - .unwrap_or_else(|| panic!("hotspots array missing: {payload}")); - assert_eq!( - payload["hotspot_count"].as_u64(), - Some(u64::try_from(rows.len()).expect("hotspot count fits")), - "{payload}" - ); - rows -} - -fn assert_chain_ranking(payload: &Value) { - let rows = hotspots(payload); - assert_eq!(rows.len(), 4, "{payload}"); - assert_exact_hotspot(&rows[0], "mid", "src/calls.ts", 9, 1, 1, 2); - assert_exact_hotspot(&rows[3], "quiet", "src/calls.ts", 1, 0, 0, 0); - let mut tied = [rows[1].clone(), rows[2].clone()]; - tied.sort_by(|left, right| { - left["name"] - .as_str() - .unwrap_or("") - .cmp(right["name"].as_str().unwrap_or("")) - }); - assert_exact_hotspot(&tied[0], "hub", "src/calls.ts", 13, 0, 1, 1); - assert_exact_hotspot(&tied[1], "leaf", "src/calls.ts", 5, 1, 0, 1); - assert!( - rows.windows(2) - .all(|pair| pair[0]["total"].as_u64() >= pair[1]["total"].as_u64()), - "chain ranking is not highest degree first: {payload}" - ); -} - -fn assert_fanout_page(payload: &Value, expected_count: usize) { - let rows = hotspots(payload); - assert_eq!(rows.len(), expected_count, "{payload}"); - assert_exact_hotspot(&rows[0], "hub", "src/fanout.ts", 1, 101, 0, 101); - let mut seen = Vec::new(); - for row in rows.iter().skip(1) { - let name = row["name"] - .as_str() - .unwrap_or_else(|| panic!("caller name missing: {row}")); - let index: u64 = name - .strip_prefix("caller") - .unwrap_or_else(|| panic!("non-caller in the fan-out page: {row}")) - .parse() - .unwrap_or_else(|_| panic!("caller index missing: {row}")); - assert!(index < 101, "caller outside the fixture: {row}"); - assert_exact_hotspot(row, name, "src/fanout.ts", index + 2, 0, 1, 1); - seen.push(index); - } - seen.sort_unstable(); - seen.dedup(); - assert_eq!(seen.len(), expected_count - 1, "{payload}"); -} - -fn assert_clamped_truncation(payload: &Value) { - assert_eq!(payload["truncated"], true, "{payload}"); - assert_eq!(payload["retrieve_tool"], "tracedecay_retrieve", "{payload}"); - assert_eq!(payload["retrieve_ttl_seconds"], 86_400, "{payload}"); - let preview_chars = payload["preview_chars"] - .as_u64() - .unwrap_or_else(|| panic!("preview_chars missing: {payload}")); - assert_eq!(preview_chars, 11_928, "{payload}"); - let original_chars = payload["original_chars"] - .as_u64() - .unwrap_or_else(|| panic!("original_chars missing: {payload}")); - assert!( - original_chars > preview_chars, - "clamped body must not fit in the preview: {payload}" - ); - let preview = payload["preview"] - .as_str() - .unwrap_or_else(|| panic!("preview missing: {payload}")); - assert_eq!(preview.chars().count() as u64, preview_chars, "{preview}"); - let marker = r#"{"hotspot_count":100,"hotspots":["#; - let array = preview - .strip_prefix(marker) - .unwrap_or_else(|| panic!("clamped preview did not start with 100 rows: {preview}")); - assert!( - array.starts_with('{'), - "clamped preview omitted the hub object: {preview}" - ); - let mut depth = 0_i32; - let mut end = None; - for (index, byte) in array.bytes().enumerate() { - match byte { - b'{' => depth += 1, - b'}' => { - depth -= 1; - if depth == 0 { - end = Some(index); - break; - } - } - _ => {} - } - } - let end = end.unwrap_or_else(|| panic!("clamped hub object was cut off: {preview}")); - let first: Value = serde_json::from_str(&array[..=end]).unwrap_or_else(|error| { - panic!( - "clamped hub object did not parse: {error}\n{}", - &array[..=end] - ) - }); - assert_exact_hotspot(&first, "hub", "src/fanout.ts", 1, 101, 0, 101); - - let handle = payload["handle"] - .as_str() - .unwrap_or_else(|| panic!("truncation handle missing: {payload}")); - assert!( - handle.starts_with("rh_") && handle.len() > "rh_".len(), - "{payload}" - ); - let expires = payload["retrieve_expires_at"] - .as_i64() - .unwrap_or_else(|| panic!("retrieve expiry missing: {payload}")); - let instruction = payload["retrieve_instruction"] - .as_str() - .unwrap_or_else(|| panic!("retrieve instruction missing: {payload}")); - assert!(instruction.contains(handle), "{instruction}"); - assert!(instruction.contains(&expires.to_string()), "{instruction}"); - assert!( - instruction.contains(&preview_chars.to_string()), - "{instruction}" - ); - assert!( - instruction.contains(&original_chars.to_string()), - "{instruction}" - ); - assert!(instruction.contains("tracedecay_retrieve"), "{instruction}"); -} - -#[tokio::test] -async fn hotspots_ranks_symbols_by_edge_degree_and_clamps_limit() { - let chain_dir = test_temp_dir(); - let chain_root = chain_dir.path().join("project"); - write_chain_project(&chain_root); - let (chain, _env) = init_test_project(&chain_root).await; - - let chain_default = call_hotspots(&chain, json!({"format": "json"})).await; - let chain_limit_one = call_hotspots(&chain, json!({"format": "json", "limit": 1})).await; - let chain_markdown = call_hotspots(&chain, json!({"format": "markdown", "limit": 1})).await; - let chain_rejected = chain - .harness - .call_tool( - &chain.project_root, - "tracedecay_hotspots", - json!({"limit": 0, "format": "json"}), - ) - .await - .expect("zero limit still reaches the MCP server"); - close_test_graph(chain).await; - - let chain_default_payload = parse_body(&chain_default); - assert_chain_ranking(&chain_default_payload); - assert_savings_footer(&chain_default, CHAIN_SOURCE.len()); - - let chain_one_payload = parse_body(&chain_limit_one); - let one = hotspots(&chain_one_payload); - assert_eq!(one.len(), 1, "{chain_one_payload}"); - assert_exact_hotspot(&one[0], "mid", "src/calls.ts", 9, 1, 1, 2); - assert_savings_footer(&chain_limit_one, CHAIN_SOURCE.len()); - - let mid_id = one[0]["id"].as_str().expect("mid occurrence id").to_owned(); - assert_eq!( - body_text(&chain_markdown), - format!( - "**hotspot_count:** 1\n\n## hotspots\n- **mid**\n **kind:** function\n **file:** src/calls.ts\n **line:** 9\n **id:** `{mid_id}`\n **incoming:** 1\n **outgoing:** 1\n **total:** 2\n" - ) - ); - assert_savings_footer(&chain_markdown, CHAIN_SOURCE.len()); - - let rejected = chain_rejected.error.expect("zero limit is a tool error"); - assert_eq!(rejected.code, -32603); - assert_eq!( - rejected.message, - "tool execution failed: config error: invalid parameter: tracedecay_hotspots requires limit to be at least 1" - ); - assert_eq!( - rejected.data, - Some(json!({ - "tool": "tracedecay_hotspots", - "cli_fallback": "This tool is also available from the shell: `tracedecay tool hotspots ...` (`tracedecay tool hotspots --help` for parameters). If MCP calls keep failing or timing out, fall back to that CLI instead of querying .tracedecay databases directly." - })) - ); - - let fanout_dir = test_temp_dir(); - let fanout_root = fanout_dir.path().join("project"); - let fanout_bytes = write_fanout_project(&fanout_root); - let (fanout, _env) = init_test_project(&fanout_root).await; - let fanout_default = call_hotspots(&fanout, json!({"format": "json"})).await; - let fanout_capped = call_hotspots(&fanout, json!({"format": "json", "limit": 250})).await; - let fanout_one = call_hotspots(&fanout, json!({"format": "json", "limit": 1})).await; - close_test_graph(fanout).await; - - let fanout_default_payload = parse_body(&fanout_default); - assert_fanout_page(&fanout_default_payload, 10); - assert_savings_footer(&fanout_default, fanout_bytes); - - let fanout_one_payload = parse_body(&fanout_one); - let fanout_top = hotspots(&fanout_one_payload); - assert_eq!(fanout_top.len(), 1, "{fanout_one_payload}"); - assert_exact_hotspot(&fanout_top[0], "hub", "src/fanout.ts", 1, 101, 0, 101); - assert_savings_footer(&fanout_one, fanout_bytes); - - assert_clamped_truncation(&parse_body(&fanout_capped)); - assert_savings_footer(&fanout_capped, fanout_bytes); -} diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/graph_analysis_test/recursion_behavior.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/graph_analysis_test/recursion_behavior.rs deleted file mode 100644 index a238036b7d..0000000000 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/graph_analysis_test/recursion_behavior.rs +++ /dev/null @@ -1,282 +0,0 @@ -//! Literal `tracedecay_recursion` results from production MCP `tools/call`. -//! -//! `length` is the number of call edges in the cycle. The chain repeats its -//! start symbol so the path is closed. Occurrence ids include the temp -//! project path, so which node the search starts on changes between runs. -//! Comparisons rotate each chain to its smallest `(name, file, line)` and -//! then pin names, kinds, files, and lines. Ids are still required to close -//! the cycle. - -use std::path::Path; - -use serde_json::{Value, json}; - -use super::{close_test_graph, handle_tool_call, init_test_project}; -use crate::support::{expect_tool_error, extract_json, test_temp_dir}; - -const DIRECT_SOURCE: &str = "\ -pub fn recurse(n: u32) -> u32 { - if n == 0 { 0 } else { recurse(n - 1) } -} - -pub fn leaf() -> u32 { 1 } -"; - -const MUTUAL_SOURCE: &str = "\ -pub fn ping() { pong(); } -pub fn pong() { ping(); } -"; - -const NOISE_SOURCE: &str = "\ -pub struct Triplet { - rows: Vec, -} - -impl Triplet { - pub fn push(&mut self, row: usize) { - self.rows.push(row); - } -} -"; - -fn direct_cycle() -> Value { - json!({ - "length": 1, - "chain": [ - {"name": "recurse", "kind": "function", "file": "src/direct.rs", "line": 1}, - {"name": "recurse", "kind": "function", "file": "src/direct.rs", "line": 1} - ] - }) -} - -fn mutual_cycle() -> Value { - json!({ - "length": 2, - "chain": [ - {"name": "ping", "kind": "function", "file": "src/mutual.rs", "line": 1}, - {"name": "pong", "kind": "function", "file": "src/mutual.rs", "line": 2}, - {"name": "ping", "kind": "function", "file": "src/mutual.rs", "line": 1} - ] - }) -} - -fn full_report() -> Value { - json!({ - "cycle_count": 2, - "cycles": [direct_cycle(), mutual_cycle()] - }) -} - -pub(super) fn public_recursion_report(payload: &Value) -> Value { - let keys = sorted_keys(payload, "recursion payload"); - assert_eq!( - keys, - ["cycle_count", "cycles"], - "recursion payload keys drifted: {payload}" - ); - let cycles = payload["cycles"] - .as_array() - .unwrap_or_else(|| panic!("cycles must be an array: {payload}")); - let cycles = cycles - .iter() - .map(|cycle| { - let cycle_keys = sorted_keys(cycle, "cycle"); - assert_eq!( - cycle_keys, - ["chain", "length"], - "cycle keys drifted: {cycle}" - ); - let chain = cycle["chain"] - .as_array() - .unwrap_or_else(|| panic!("chain must be an array: {cycle}")); - json!({ - "length": cycle["length"], - "chain": canonical_public_chain(chain), - }) - }) - .collect::>(); - let mut cycles = cycles; - cycles.sort_by(|left, right| cycle_order_key(left).cmp(&cycle_order_key(right))); - json!({ - "cycle_count": payload["cycle_count"], - "cycles": cycles, - }) -} - -fn cycle_order_key(cycle: &Value) -> (i64, String) { - ( - cycle["length"].as_i64().unwrap_or(i64::MAX), - cycle["chain"].to_string(), - ) -} - -fn canonical_public_chain(chain: &[Value]) -> Vec { - let public = chain.iter().map(public_chain_node).collect::>(); - assert!( - public.len() >= 2, - "a cycle chain must repeat its start: {public:?}" - ); - assert_eq!( - public.first(), - public.last(), - "a cycle chain must close on the same symbol: {public:?}" - ); - let body = &public[..public.len() - 1]; - let start = body - .iter() - .enumerate() - .min_by(|(_, left), (_, right)| public_node_order(left).cmp(&public_node_order(right))) - .map(|(index, _)| index) - .expect("a cycle body is non-empty"); - let mut rotated = body[start..] - .iter() - .chain(&body[..start]) - .cloned() - .collect::>(); - rotated.push(rotated[0].clone()); - rotated -} - -fn public_node_order(node: &Value) -> (String, String, i64) { - ( - node["name"].as_str().unwrap_or_default().to_owned(), - node["file"].as_str().unwrap_or_default().to_owned(), - node["line"].as_i64().unwrap_or(i64::MAX), - ) -} - -fn sorted_keys<'a>(value: &'a Value, label: &str) -> Vec<&'a str> { - let mut keys = value - .as_object() - .unwrap_or_else(|| panic!("{label} must be an object: {value}")) - .keys() - .map(String::as_str) - .collect::>(); - keys.sort_unstable(); - keys -} - -fn public_chain_node(node: &Value) -> Value { - let keys = sorted_keys(node, "chain node"); - assert_eq!( - keys, - ["file", "id", "kind", "line", "name"], - "chain node keys drifted: {node}" - ); - assert!( - node["id"].as_str().is_some_and(|id| !id.is_empty()), - "chain node id must be a non-empty string: {node}" - ); - json!({ - "name": node["name"], - "kind": node["kind"], - "file": node["file"], - "line": node["line"], - }) -} - -pub(super) fn assert_reported_cycles_close(payload: &Value) { - let cycles = payload["cycles"] - .as_array() - .unwrap_or_else(|| panic!("cycles must be an array: {payload}")); - for cycle in cycles { - let chain = cycle["chain"] - .as_array() - .unwrap_or_else(|| panic!("chain must be an array: {cycle}")); - let start = chain - .first() - .and_then(|node| node["id"].as_str()) - .unwrap_or_else(|| panic!("cycle is missing its start id: {cycle}")); - let end = chain - .last() - .and_then(|node| node["id"].as_str()) - .unwrap_or_else(|| panic!("cycle is missing its closing id: {cycle}")); - assert_eq!( - start, end, - "a reported cycle must return to its start symbol: {cycle}" - ); - } -} - -async fn call_recursion(graph: &impl super::AnalysisToolHost, arguments: Value) -> Value { - let result = handle_tool_call(graph, "tracedecay_recursion", arguments, None, None) - .await - .unwrap_or_else(|error| panic!("tracedecay_recursion failed: {error}")); - extract_json(&result.value) -} - -#[tokio::test] -async fn recursion_reports_literal_cycles_and_refuses_non_positive_limit() { - let dir = test_temp_dir(); - let project_root = dir.path().join("project"); - fs_write_fixture(&project_root); - let (graph, ()) = init_test_project(&project_root).await; - - let payload = call_recursion(&graph, json!({"format": "json", "limit": 10})).await; - assert_eq!( - public_recursion_report(&payload), - full_report(), - "default-sized recursion report: {payload}" - ); - assert_reported_cycles_close(&payload); - - let scoped = call_recursion(&graph, json!({"format": "json", "path": "src/direct.rs"})).await; - assert_eq!( - public_recursion_report(&scoped), - json!({"cycle_count": 1, "cycles": [direct_cycle()]}), - "path filter must keep only the direct cycle: {scoped}" - ); - - let mutual = call_recursion( - &graph, - json!({"format": "json", "path": "src/mutual.rs", "limit": 10}), - ) - .await; - assert_eq!( - public_recursion_report(&mutual), - json!({"cycle_count": 1, "cycles": [mutual_cycle()]}), - "path filter must keep only the mutual cycle: {mutual}" - ); - - let noise = call_recursion(&graph, json!({"format": "json", "path": "src/noise.rs"})).await; - assert_eq!( - public_recursion_report(&noise), - json!({"cycle_count": 0, "cycles": []}), - "receiver `.push` must not be a cycle when the same graph has real cycles: {noise}" - ); - - let limited = call_recursion(&graph, json!({"format": "json", "limit": 1})).await; - assert_eq!( - public_recursion_report(&limited), - json!({"cycle_count": 1, "cycles": [direct_cycle()]}), - "limit 1 keeps the shortest cycle: {limited}" - ); - - let error = expect_tool_error( - handle_tool_call( - &graph, - "tracedecay_recursion", - json!({"format": "json", "limit": 0}), - None, - None, - ) - .await, - ); - assert_eq!( - error, - "config error: tracedecay_recursion failed over production MCP: tool execution failed: config error: invalid parameter: tracedecay_recursion requires limit to be at least 1" - ); - close_test_graph(graph).await; -} - -fn fs_write_fixture(project_root: &Path) { - std::fs::create_dir_all(project_root.join("src")).unwrap(); - std::fs::write( - project_root.join("src/lib.rs"), - "pub mod direct;\npub mod mutual;\npub mod noise;\n", - ) - .unwrap(); - std::fs::write(project_root.join("src/direct.rs"), DIRECT_SOURCE).unwrap(); - std::fs::write(project_root.join("src/mutual.rs"), MUTUAL_SOURCE).unwrap(); - std::fs::write(project_root.join("src/noise.rs"), NOISE_SOURCE).unwrap(); -} diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/retrieve_truncation_support.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/retrieve_truncation_support.rs deleted file mode 100644 index 2b9f49c73b..0000000000 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/retrieve_truncation_support.rs +++ /dev/null @@ -1,54 +0,0 @@ -use serde_json::{Value, json}; - -pub(super) fn retrieve_json_arguments(handle: &str) -> Value { - json!({ "format": "json", "handle": handle }) -} - -#[cfg(feature = "test-transport")] -pub(super) async fn retrieve_all_json_pages( - fixture: &crate::support::ProductionCompositionFixture, - handle: &str, -) -> String { - let mut offset = 0usize; - let mut content = String::new(); - loop { - let page = call_production_tool( - fixture, - "tracedecay_retrieve", - json!({"format": "json", "handle": handle, "offset": offset}), - ) - .await; - let page: Value = serde_json::from_str(crate::support::extract_text(&page.value)) - .expect("retrieve page JSON"); - let page_content = page["content"].as_str().expect("retrieve page content"); - content.push_str(page_content); - if !page["has_more"].as_bool().expect("retrieve has_more") { - return content; - } - offset = page["next_offset"].as_u64().expect("retrieve next_offset") as usize; - } -} - -#[cfg(feature = "test-transport")] -pub(super) async fn call_production_tool( - fixture: &crate::support::ProductionCompositionFixture, - tool_name: &str, - arguments: Value, -) -> tracedecay_mcp::ToolResult { - let response = fixture - .harness - .call_tool(&fixture.project_root, tool_name, arguments) - .await - .unwrap_or_else(|error| panic!("{tool_name} production invocation failed: {error}")); - assert!( - response.error.is_none(), - "{tool_name} returned a production MCP error: {:?}", - response.error.as_ref().map(|error| &error.message) - ); - tracedecay_mcp::ToolResult::new( - response - .result - .unwrap_or_else(|| panic!("{tool_name} returned no production MCP result")), - Vec::new(), - ) -} diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/retrieve_truncation_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/retrieve_truncation_test.rs index 24ae903fad..77b28b037b 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/retrieve_truncation_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/retrieve_truncation_test.rs @@ -3,13 +3,56 @@ use serde_json::{Value, json}; #[cfg(feature = "test-transport")] use std::fmt::Write as _; use std::fs; -#[path = "retrieve_truncation_support.rs"] -mod retrieve_truncation_support; + +fn retrieve_json_arguments(handle: &str) -> Value { + json!({ "format": "json", "handle": handle }) +} + #[cfg(feature = "test-transport")] -use retrieve_truncation_support::call_production_tool; +async fn retrieve_all_json_pages(fixture: &ProductionCompositionFixture, handle: &str) -> String { + let mut offset = 0usize; + let mut content = String::new(); + loop { + let page = call_production_tool( + fixture, + "tracedecay_retrieve", + json!({"format": "json", "handle": handle, "offset": offset}), + ) + .await; + let page: Value = + serde_json::from_str(extract_text(&page.value)).expect("retrieve page JSON"); + let page_content = page["content"].as_str().expect("retrieve page content"); + content.push_str(page_content); + if !page["has_more"].as_bool().expect("retrieve has_more") { + return content; + } + offset = page["next_offset"].as_u64().expect("retrieve next_offset") as usize; + } +} + #[cfg(feature = "test-transport")] -use retrieve_truncation_support::retrieve_all_json_pages; -use retrieve_truncation_support::retrieve_json_arguments; +async fn call_production_tool( + fixture: &ProductionCompositionFixture, + tool_name: &str, + arguments: Value, +) -> tracedecay_mcp::ToolResult { + let response = fixture + .harness + .call_tool(&fixture.project_root, tool_name, arguments) + .await + .unwrap_or_else(|error| panic!("{tool_name} production invocation failed: {error}")); + assert!( + response.error.is_none(), + "{tool_name} returned a production MCP error: {:?}", + response.error.as_ref().map(|error| &error.message) + ); + tracedecay_mcp::ToolResult::new( + response + .result + .unwrap_or_else(|| panic!("{tool_name} returned no production MCP result")), + Vec::new(), + ) +} #[tokio::test] async fn retrieve_tool_returns_full_stored_response() { diff --git a/crates/tracedecay/tests/mcp_suite/mcp_server_test/protocol_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_server_test/protocol_test.rs index 8bdd707732..6ee06bf634 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_server_test/protocol_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_server_test/protocol_test.rs @@ -1,15 +1,18 @@ use crate::mcp_server_test::support::*; use serde_json::{Value, json}; use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::Arc; use tempfile::TempDir; +use tracedecay::daemon::ProductionProjectCompositionHarnessV1; +use tracedecay::mcp::McpServer; use tracedecay::project::current_timestamp; use tracedecay_mcp::response_handles::{ RESPONSE_HANDLE_TTL_SECS, cleanup_expired_response_handles, store_response_handle, }; use tracedecay_runtime_core::storage::resolve_response_handle_root; -mod initialize_routing; - // --------------------------------------------------------------------------- // 1. test_initialize // --------------------------------------------------------------------------- @@ -38,12 +41,12 @@ async fn test_initialize() { #[tokio::test] async fn initialize_roots_route_registered_reader_tools_without_explicit_selector() { - initialize_routing::assert_registered_reader_uses_initialize_root().await; + assert_registered_reader_uses_initialize_root().await; } #[tokio::test] async fn initialize_root_route_rejects_caller_project_path_spoof() { - initialize_routing::assert_legacy_selectors_cannot_spoof_initialize_root().await; + assert_legacy_selectors_cannot_spoof_initialize_root().await; } // --------------------------------------------------------------------------- @@ -1923,3 +1926,178 @@ async fn repeated_serve_lcm_calls_do_not_rerun_migrations() { stat_sessions_db("after-second-serve"), ); } + +fn initialize_protocol_fixture(project: &Path, module: &str) { + fs::create_dir_all(project.join("src")).unwrap(); + fs::write( + project.join("Cargo.toml"), + "[package]\nname = \"fixture\"\nversion = \"0.1.0\"\nedition = \"2024\"\n", + ) + .unwrap(); + fs::write(project.join("src/lib.rs"), format!("pub mod {module};\n")).unwrap(); + fs::write( + project.join(format!("src/{module}.rs")), + format!("pub fn {module}_marker() {{}}\n"), + ) + .unwrap(); + for args in [ + &["init", "--quiet"][..], + &["add", "."][..], + &[ + "-c", + "user.name=TraceDecay Tests", + "-c", + "user.email=tests@tracedecay.invalid", + "commit", + "--quiet", + "-m", + "fixture", + ][..], + ] { + assert!( + Command::new("git") + .args(args) + .current_dir(project) + .status() + .unwrap() + .success() + ); + } +} + +async fn fixture() -> ( + TempDir, + ProductionProjectCompositionHarnessV1, + Arc, + PathBuf, +) { + let isolation = TempDir::new().unwrap(); + let active_project = isolation.path().join("active-project"); + let target_project = isolation.path().join("target-project"); + initialize_protocol_fixture(&active_project, "active"); + initialize_protocol_fixture(&target_project, "target"); + let harness = ProductionProjectCompositionHarnessV1::open( + isolation.path(), + [active_project.clone(), target_project.clone()], + ) + .await + .unwrap(); + let server = harness.server(&active_project).unwrap(); + (isolation, harness, server, target_project) +} + +fn initialize_request(target_project: &Path) -> String { + let target_root_uri = url::Url::from_file_path(target_project) + .expect("target project has a portable file URI") + .to_string(); + jsonrpc_request( + json!(1), + "initialize", + json!({ + "clientInfo": {"name": "codex", "version": "test"}, + "roots": [{"uri": target_root_uri, "name": "target-project"}] + }), + ) +} + +fn files_request(id: u64, arguments: Value) -> String { + jsonrpc_request( + json!(id), + "tools/call", + json!({ + "name": "tracedecay_files", + "arguments": arguments, + }), + ) +} + +async fn assert_registered_reader_uses_initialize_root() { + let (_isolation, _harness, server, target_project) = fixture().await; + let responses = run_server_with_messages( + server, + vec![ + initialize_request(&target_project), + files_request(2, json!({"layout": "flat"})), + ], + ) + .await; + + let files_response = response_with_id(&responses, json!(2)); + let text = files_response["result"]["content"][0]["text"] + .as_str() + .expect("files response text"); + assert!( + text.contains("src/target.rs"), + "initialize root should route reader tools to target project, got {text}" + ); + assert!( + !text.contains("src/active.rs"), + "implicit initialize-root routing should not read the active project: {text}" + ); +} + +async fn assert_legacy_selectors_cannot_spoof_initialize_root() { + let (_isolation, _harness, server, target_project) = fixture().await; + let active_graph = server.cg().await; + let active_root = active_graph.project_root().to_string_lossy().into_owned(); + let active_project_id = active_graph + .store_layout() + .identity + .project_id + .clone() + .expect("active project has a registered identity"); + + let spoof_cases = [ + ( + "top-level project_path", + json!({"layout": "flat", "project_path": active_root.clone()}), + ), + ( + "top-level project_root", + json!({"layout": "flat", "project_root": active_root.clone()}), + ), + ( + "nested selector path", + json!({"layout": "flat", "project_selector": {"path": active_root.clone()}}), + ), + ( + "nested selector project_path", + json!({"layout": "flat", "project_selector": {"project_path": active_root}}), + ), + ( + "top-level project_id alias", + json!({"layout": "flat", "project_id": active_project_id}), + ), + ]; + let mut messages = vec![initialize_request(&target_project)]; + for (offset, (_, arguments)) in spoof_cases.iter().enumerate() { + messages.push(files_request(10 + offset as u64, arguments.clone())); + } + messages.push(files_request(100, json!({"layout": "flat"}))); + + let responses = run_server_with_messages(server, messages).await; + for (offset, (case, _)) in spoof_cases.iter().enumerate() { + let response = response_with_id(&responses, json!(10 + offset as u64)); + assert_eq!( + response["error"]["code"], -32602, + "{case} must be rejected as invalid parameters instead of overriding the initialize-root route: {response}" + ); + assert!( + response["result"].is_null(), + "{case} must not return a tool result after invalid-parameter rejection: {response}" + ); + assert!( + !response.to_string().contains("src/active.rs"), + "{case} must not serve spoof-project data: {response}" + ); + } + + let clean_response = response_with_id(&responses, json!(100)); + let clean_text = clean_response["result"]["content"][0]["text"] + .as_str() + .unwrap_or_else(|| panic!("clean files response text: {clean_response}")); + assert!( + clean_text.contains("src/target.rs") && !clean_text.contains("src/active.rs"), + "rejected spoof attempts must not disturb the initialize-root route: {clean_text}" + ); +} diff --git a/crates/tracedecay/tests/mcp_suite/mcp_server_test/protocol_test/initialize_routing.rs b/crates/tracedecay/tests/mcp_suite/mcp_server_test/protocol_test/initialize_routing.rs deleted file mode 100644 index 70205196ad..0000000000 --- a/crates/tracedecay/tests/mcp_suite/mcp_server_test/protocol_test/initialize_routing.rs +++ /dev/null @@ -1,188 +0,0 @@ -use std::fs; -use std::path::{Path, PathBuf}; -use std::process::Command; -use std::sync::Arc; - -use serde_json::{Value, json}; -use tempfile::TempDir; -use tracedecay::daemon::ProductionProjectCompositionHarnessV1; -use tracedecay::mcp::McpServer; - -use crate::mcp_server_test::support::{ - jsonrpc_request, response_with_id, run_server_with_messages, -}; - -fn initialize_protocol_fixture(project: &Path, module: &str) { - fs::create_dir_all(project.join("src")).unwrap(); - fs::write( - project.join("Cargo.toml"), - "[package]\nname = \"fixture\"\nversion = \"0.1.0\"\nedition = \"2024\"\n", - ) - .unwrap(); - fs::write(project.join("src/lib.rs"), format!("pub mod {module};\n")).unwrap(); - fs::write( - project.join(format!("src/{module}.rs")), - format!("pub fn {module}_marker() {{}}\n"), - ) - .unwrap(); - for args in [ - &["init", "--quiet"][..], - &["add", "."][..], - &[ - "-c", - "user.name=TraceDecay Tests", - "-c", - "user.email=tests@tracedecay.invalid", - "commit", - "--quiet", - "-m", - "fixture", - ][..], - ] { - assert!( - Command::new("git") - .args(args) - .current_dir(project) - .status() - .unwrap() - .success() - ); - } -} - -async fn fixture() -> ( - TempDir, - ProductionProjectCompositionHarnessV1, - Arc, - PathBuf, -) { - let isolation = TempDir::new().unwrap(); - let active_project = isolation.path().join("active-project"); - let target_project = isolation.path().join("target-project"); - initialize_protocol_fixture(&active_project, "active"); - initialize_protocol_fixture(&target_project, "target"); - let harness = ProductionProjectCompositionHarnessV1::open( - isolation.path(), - [active_project.clone(), target_project.clone()], - ) - .await - .unwrap(); - let server = harness.server(&active_project).unwrap(); - (isolation, harness, server, target_project) -} - -fn initialize_request(target_project: &Path) -> String { - let target_root_uri = url::Url::from_file_path(target_project) - .expect("target project has a portable file URI") - .to_string(); - jsonrpc_request( - json!(1), - "initialize", - json!({ - "clientInfo": {"name": "codex", "version": "test"}, - "roots": [{"uri": target_root_uri, "name": "target-project"}] - }), - ) -} - -fn files_request(id: u64, arguments: Value) -> String { - jsonrpc_request( - json!(id), - "tools/call", - json!({ - "name": "tracedecay_files", - "arguments": arguments, - }), - ) -} - -pub(super) async fn assert_registered_reader_uses_initialize_root() { - let (_isolation, _harness, server, target_project) = fixture().await; - let responses = run_server_with_messages( - server, - vec![ - initialize_request(&target_project), - files_request(2, json!({"layout": "flat"})), - ], - ) - .await; - - let files_response = response_with_id(&responses, json!(2)); - let text = files_response["result"]["content"][0]["text"] - .as_str() - .expect("files response text"); - assert!( - text.contains("src/target.rs"), - "initialize root should route reader tools to target project, got {text}" - ); - assert!( - !text.contains("src/active.rs"), - "implicit initialize-root routing should not read the active project: {text}" - ); -} - -pub(super) async fn assert_legacy_selectors_cannot_spoof_initialize_root() { - let (_isolation, _harness, server, target_project) = fixture().await; - let active_graph = server.cg().await; - let active_root = active_graph.project_root().to_string_lossy().into_owned(); - let active_project_id = active_graph - .store_layout() - .identity - .project_id - .clone() - .expect("active project has a registered identity"); - - let spoof_cases = [ - ( - "top-level project_path", - json!({"layout": "flat", "project_path": active_root.clone()}), - ), - ( - "top-level project_root", - json!({"layout": "flat", "project_root": active_root.clone()}), - ), - ( - "nested selector path", - json!({"layout": "flat", "project_selector": {"path": active_root.clone()}}), - ), - ( - "nested selector project_path", - json!({"layout": "flat", "project_selector": {"project_path": active_root}}), - ), - ( - "top-level project_id alias", - json!({"layout": "flat", "project_id": active_project_id}), - ), - ]; - let mut messages = vec![initialize_request(&target_project)]; - for (offset, (_, arguments)) in spoof_cases.iter().enumerate() { - messages.push(files_request(10 + offset as u64, arguments.clone())); - } - messages.push(files_request(100, json!({"layout": "flat"}))); - - let responses = run_server_with_messages(server, messages).await; - for (offset, (case, _)) in spoof_cases.iter().enumerate() { - let response = response_with_id(&responses, json!(10 + offset as u64)); - assert_eq!( - response["error"]["code"], -32602, - "{case} must be rejected as invalid parameters instead of overriding the initialize-root route: {response}" - ); - assert!( - response["result"].is_null(), - "{case} must not return a tool result after invalid-parameter rejection: {response}" - ); - assert!( - !response.to_string().contains("src/active.rs"), - "{case} must not serve spoof-project data: {response}" - ); - } - - let clean_response = response_with_id(&responses, json!(100)); - let clean_text = clean_response["result"]["content"][0]["text"] - .as_str() - .unwrap_or_else(|| panic!("clean files response text: {clean_response}")); - assert!( - clean_text.contains("src/target.rs") && !clean_text.contains("src/active.rs"), - "rejected spoof attempts must not disturb the initialize-root route: {clean_text}" - ); -} From c2d1c1c5c1eb3838dbc4aabb8e52090629e223d3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:45:54 +0000 Subject: [PATCH 065/182] simplify(pass-4/5): share duplicate helper bodies Elapsed-time, trimmed text, job errors, and message roles each had one identical copy per caller. Co-authored-by: Zack Jackson --- .../src/automation/job_webhook.rs | 7 +------ .../src/automation/jobs.rs | 18 ++++-------------- .../src/automation/mod.rs | 13 +++++++++++++ .../src/automation/runner/evidence.rs | 9 +-------- .../src/automation/scheduler.rs | 2 +- .../src/automation/session_reflector.rs | 9 +-------- .../src/automation/skill_writer.rs | 10 +--------- crates/tracedecay-capture/src/codex.rs | 14 ++------------ crates/tracedecay-capture/src/cursor.rs | 16 +++------------- crates/tracedecay-capture/src/parse.rs | 12 +++++++++++- 10 files changed, 38 insertions(+), 72 deletions(-) diff --git a/crates/tracedecay-automation-runtime/src/automation/job_webhook.rs b/crates/tracedecay-automation-runtime/src/automation/job_webhook.rs index 63aa907db2..bbb7af6a62 100644 --- a/crates/tracedecay-automation-runtime/src/automation/job_webhook.rs +++ b/crates/tracedecay-automation-runtime/src/automation/job_webhook.rs @@ -6,6 +6,7 @@ use std::time::Duration; use serde_json::Value; use url::{Host, Url}; +use super::job_error; use tracedecay_domain::errors::{Result, TraceDecayError}; pub(crate) fn validate_url(raw: &str) -> Result<()> { @@ -395,12 +396,6 @@ fn host_header(url: &Url) -> Result { } } -fn job_error(message: &str) -> Result { - Err(TraceDecayError::Config { - message: message.to_string(), - }) -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/tracedecay-automation-runtime/src/automation/jobs.rs b/crates/tracedecay-automation-runtime/src/automation/jobs.rs index 3d36cce8b8..a6e00c764c 100644 --- a/crates/tracedecay-automation-runtime/src/automation/jobs.rs +++ b/crates/tracedecay-automation-runtime/src/automation/jobs.rs @@ -12,6 +12,7 @@ use super::backend::{ BackendRetryPolicy, classify_agent_task_error_message, run_agent_task_with_retry_report, }; use super::config::{AutomationBackend, AutomationConfig, AutomationHostMode}; +use super::job_error; use super::job_webhook; use super::lifecycle::{ AutomationRunLedgerPublication, AutomationRunSettlementGuard, RetainedAutomationRun, @@ -23,7 +24,9 @@ use super::run_ledger::{ AutomationTrigger, append_or_reuse_scheduler_diagnostic, append_run_record, latest_record_by_canonical_completion, load_run_ledger_task_summary, }; -use super::scheduler::{AutomationSchedule, AutomationTaskLock, cron_is_due, parse_schedule}; +use super::scheduler::{ + AutomationSchedule, AutomationTaskLock, cron_is_due, elapsed_secs, parse_schedule, +}; use tracedecay_automation::text::truncate_chars_for_prompt; use tracedecay_domain::errors::{Result, TraceDecayError}; use tracedecay_runtime_core::tracedecay::current_timestamp; @@ -447,13 +450,6 @@ fn latest_terminal_job_record<'a>( })) } -fn elapsed_secs(completed_at: i64, now_secs: i64) -> u64 { - if now_secs < completed_at { - return 0; - } - (now_secs - completed_at) as u64 -} - /// Executes one user job through the automation backend, delivering its /// output and recording the run in the shared ledger under /// `user_job:`. @@ -1053,12 +1049,6 @@ async fn run_pre_run_command(command: &str, project_root: Option<&Path>) -> Resu )) } -fn job_error(message: &str) -> Result { - Err(TraceDecayError::Config { - message: message.to_string(), - }) -} - #[cfg(test)] #[allow(clippy::unwrap_used, clippy::expect_used)] #[path = "jobs/scheduler_config_tests.rs"] diff --git a/crates/tracedecay-automation-runtime/src/automation/mod.rs b/crates/tracedecay-automation-runtime/src/automation/mod.rs index 961a39e670..5a569dc9a6 100644 --- a/crates/tracedecay-automation-runtime/src/automation/mod.rs +++ b/crates/tracedecay-automation-runtime/src/automation/mod.rs @@ -52,3 +52,16 @@ pub fn config_error(message: impl Into) -> tracedecay_domain::errors::Tr message: message.into(), } } + +pub(crate) fn job_error(message: &str) -> tracedecay_domain::errors::Result { + Err(config_error(message)) +} + +pub(crate) fn normalized_non_empty(value: &str) -> Option { + let value = value.trim(); + if value.is_empty() { + None + } else { + Some(value.to_string()) + } +} diff --git a/crates/tracedecay-automation-runtime/src/automation/runner/evidence.rs b/crates/tracedecay-automation-runtime/src/automation/runner/evidence.rs index 6454ce9d45..9967afcb52 100644 --- a/crates/tracedecay-automation-runtime/src/automation/runner/evidence.rs +++ b/crates/tracedecay-automation-runtime/src/automation/runner/evidence.rs @@ -5,6 +5,7 @@ use tracedecay_domain::TemporalCoverageCountsV1; use crate::ports::session_evidence::{LcmGrepHit, LcmGrepSort, LcmScope}; use crate::automation::artifacts::sha256_json; +use crate::automation::normalized_non_empty; use crate::automation::managed_skills::list_managed_skills; use crate::automation::skill_usage::{ DEFAULT_SKILL_OVERLAP_LIMIT, ingest_project_analytics_events, skill_overlap_candidates, @@ -280,14 +281,6 @@ fn session_reflector_replay_allowed( matches!(scope, LcmScope::All) || session_id.is_some() } -fn normalized_non_empty(value: &str) -> Option { - let value = value.trim(); - if value.is_empty() { - None - } else { - Some(value.to_string()) - } -} fn compare_evidence_items( left: &AutomationTemporalEvidenceItem, diff --git a/crates/tracedecay-automation-runtime/src/automation/scheduler.rs b/crates/tracedecay-automation-runtime/src/automation/scheduler.rs index 6c10f333c9..477990f7db 100644 --- a/crates/tracedecay-automation-runtime/src/automation/scheduler.rs +++ b/crates/tracedecay-automation-runtime/src/automation/scheduler.rs @@ -811,7 +811,7 @@ fn parse_started_at(record: &AutomationRunLedgerRecord) -> Result { canonical_record_started_at_seconds(record, &format!("run '{}' started_at", record.run_id)) } -fn elapsed_secs(completed_at: i64, now_secs: i64) -> u64 { +pub(crate) fn elapsed_secs(completed_at: i64, now_secs: i64) -> u64 { if now_secs < completed_at { return 0; } diff --git a/crates/tracedecay-automation-runtime/src/automation/session_reflector.rs b/crates/tracedecay-automation-runtime/src/automation/session_reflector.rs index 735f5fa528..7e7088b4e5 100644 --- a/crates/tracedecay-automation-runtime/src/automation/session_reflector.rs +++ b/crates/tracedecay-automation-runtime/src/automation/session_reflector.rs @@ -10,6 +10,7 @@ use tracedecay_store::{ ProjectMemoryFactSearchQuery, ProjectMemoryFactStore, }; +use super::normalized_non_empty; use crate::automation::lifecycle::AutomationRunControl; use tracedecay_domain::errors::{Result, TraceDecayError}; use tracedecay_session_memory::memory::MemoryApplication; @@ -560,11 +561,3 @@ fn quarantined_fact_with_validation( })) } -fn normalized_non_empty(value: &str) -> Option { - let value = value.trim(); - if value.is_empty() { - None - } else { - Some(value.to_string()) - } -} diff --git a/crates/tracedecay-automation-runtime/src/automation/skill_writer.rs b/crates/tracedecay-automation-runtime/src/automation/skill_writer.rs index ca60764ef0..3a0d480fab 100644 --- a/crates/tracedecay-automation-runtime/src/automation/skill_writer.rs +++ b/crates/tracedecay-automation-runtime/src/automation/skill_writer.rs @@ -22,7 +22,7 @@ use tracedecay_automation::managed_skills::validate_managed_skill_update; use tracedecay_automation::text::truncate_chars_for_prompt; use tracedecay_domain::errors::Result; -use super::config_error; +use super::{config_error, normalized_non_empty}; mod consolidation; @@ -936,14 +936,6 @@ fn rejected_skill(proposal: &Value, reason: &str) -> Value { }) } -fn normalized_non_empty(value: &str) -> Option { - let value = value.trim(); - if value.is_empty() { - None - } else { - Some(value.to_string()) - } -} #[cfg(test)] mod tests { diff --git a/crates/tracedecay-capture/src/codex.rs b/crates/tracedecay-capture/src/codex.rs index d2cf0b6a25..15e9cd3dc3 100644 --- a/crates/tracedecay-capture/src/codex.rs +++ b/crates/tracedecay-capture/src/codex.rs @@ -14,8 +14,8 @@ use tracedecay_domain::{ }; use crate::{ - ObservationRecordParseErrorV1, parse::canonical_u64_i64 as canonical_u64, parse::sha256_hex, - parse_rfc3339_timestamp, + ObservationRecordParseErrorV1, parse::canonical_message_role, + parse::canonical_u64_i64 as canonical_u64, parse::sha256_hex, parse_rfc3339_timestamp, }; const PROVIDER: &str = "codex"; @@ -895,16 +895,6 @@ fn timestamp_from_record(record: &Value) -> Option { .and_then(parse_rfc3339_timestamp) } -fn canonical_message_role(role: Option<&str>) -> CanonicalMessageRoleV1 { - match role { - Some("user") => CanonicalMessageRoleV1::User, - Some("assistant") => CanonicalMessageRoleV1::Assistant, - Some("system" | "developer") => CanonicalMessageRoleV1::System, - Some("tool") => CanonicalMessageRoleV1::Tool, - _ => CanonicalMessageRoleV1::Unknown, - } -} - fn canonical_native_observation_id( native_id: Option<&str>, fallback: &ObservationId, diff --git a/crates/tracedecay-capture/src/cursor.rs b/crates/tracedecay-capture/src/cursor.rs index ecf22f8468..bf554a89c2 100644 --- a/crates/tracedecay-capture/src/cursor.rs +++ b/crates/tracedecay-capture/src/cursor.rs @@ -1,7 +1,7 @@ use serde_json::Value; use sha2::{Digest, Sha256}; use tracedecay_domain::{ - CanonicalGitEvidenceKindV1, CanonicalMessageRoleV1, CanonicalObservationEnvelopeV1, + CanonicalGitEvidenceKindV1, CanonicalObservationEnvelopeV1, CanonicalObservationEvidenceV1, CanonicalObservationFactV1, CanonicalObservationRelationsV1, CanonicalReasoningVisibilityV1, CanonicalUnknownStateV1, CanonicalWorkflowEvidenceKindV1, ObservationId, ObservationOrderingDomainV1, ObservationPositionalOccurrenceV1, ProviderId, @@ -12,8 +12,8 @@ use tracedecay_store::cursor_dispatch::{cursor_model_string, is_subagent_dispatc use crate::git_facts::append_diff_and_pull_request_facts; use crate::{ - ObservationRecordParseErrorV1, parse::canonical_u64_i64 as canonical_u64, parse::sha256_hex, - parse_cursor_human_timestamp, + ObservationRecordParseErrorV1, parse::canonical_message_role, + parse::canonical_u64_i64 as canonical_u64, parse::sha256_hex, parse_cursor_human_timestamp, }; pub fn normalize_cursor_observation( @@ -567,16 +567,6 @@ pub fn cursor_projected_message_id( ObservationId::new(message_id).map_err(|_| ObservationRecordParseErrorV1::NormalizationFailed) } -fn canonical_message_role(role: Option<&str>) -> CanonicalMessageRoleV1 { - match role { - Some("user") => CanonicalMessageRoleV1::User, - Some("assistant") => CanonicalMessageRoleV1::Assistant, - Some("system" | "developer") => CanonicalMessageRoleV1::System, - Some("tool") => CanonicalMessageRoleV1::Tool, - _ => CanonicalMessageRoleV1::Unknown, - } -} - fn canonical_native_observation_id( native_id: Option<&str>, fallback: &ObservationId, diff --git a/crates/tracedecay-capture/src/parse.rs b/crates/tracedecay-capture/src/parse.rs index 732d9ed619..850c1037ca 100644 --- a/crates/tracedecay-capture/src/parse.rs +++ b/crates/tracedecay-capture/src/parse.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use thiserror::Error; pub use tracedecay_domain::MAX_OBSERVATION_RECORD_BYTES; use tracedecay_domain::{ - CanonicalObservationEnvelopeV1, MAX_OBSERVATION_STRUCTURE_DEPTH, + CanonicalMessageRoleV1, CanonicalObservationEnvelopeV1, MAX_OBSERVATION_STRUCTURE_DEPTH, MAX_OBSERVATION_STRUCTURE_VALUES, ObservationOrderingDomainV1, ObservationSourceRangeV1, ProviderId, }; @@ -367,6 +367,16 @@ fn record_digest(record: &[u8]) -> [u8; 32] { hotpath::measure_block!("capture.parse.record_digest", Sha256::digest(record).into()) } +pub(crate) fn canonical_message_role(role: Option<&str>) -> CanonicalMessageRoleV1 { + match role { + Some("user") => CanonicalMessageRoleV1::User, + Some("assistant") => CanonicalMessageRoleV1::Assistant, + Some("system" | "developer") => CanonicalMessageRoleV1::System, + Some("tool") => CanonicalMessageRoleV1::Tool, + _ => CanonicalMessageRoleV1::Unknown, + } +} + pub(crate) fn canonical_u64_i64(value: Option<&Value>) -> Option { value.and_then(|value| { value From 79bb8c26efe045539a38c3c05e4050e05c9c85e5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:46:12 +0000 Subject: [PATCH 066/182] simplify(pass-2/5): share lexical candidate binding Co-authored-by: Zack Jackson --- .../src/retrieval/lexical/projection.rs | 94 +++++++++++++++- .../lexical/projection/artifact/reader.rs | 105 ++++++------------ .../retrieval/lexical/projection/in_memory.rs | 88 +++------------ 3 files changed, 135 insertions(+), 152 deletions(-) diff --git a/crates/tracedecay-query/src/retrieval/lexical/projection.rs b/crates/tracedecay-query/src/retrieval/lexical/projection.rs index e283e0d61e..e7ee9a5165 100644 --- a/crates/tracedecay-query/src/retrieval/lexical/projection.rs +++ b/crates/tracedecay-query/src/retrieval/lexical/projection.rs @@ -5,10 +5,11 @@ use serde::{Deserialize, Serialize}; use tracedecay_code_index::production::VerifiedSealedLexicalSymbolDisplayV1; use tracedecay_domain::{ BoundedSanitizedText, CodeGenerationId, CodeSearchChunkAnchorV1, CodeSearchChunkGrainV1, - CodeSearchChunkId, CodeSearchChunkV1, ComponentRevision, ExactFieldV1, - ExactTechnicalTermKindV1, ExactTechnicalTermV1, FileOccurrenceId, LanguageDescriptorRevision, - RepositoryId, RetrievalAnchorId, ScoreDomainId, SourceFreshness, exact_search_canonical, - split_subtokens, technical_tokens, validate_code_logical_path, + CodeSearchChunkId, CodeSearchChunkV1, CompactCandidate, ComponentRevision, EvidenceRole, + ExactAdmissionProof, ExactFieldV1, ExactTechnicalTermKindV1, ExactTechnicalTermV1, + FileOccurrenceId, FixedPointScore, LanguageDescriptorRevision, LogicalEvidenceId, RepositoryId, + RetrievalAnchorId, RetrieverKind, ScoreDomainId, SourceFreshness, SourceOccurrenceId, + exact_search_canonical, split_subtokens, technical_tokens, validate_code_logical_path, }; use super::{ @@ -16,7 +17,9 @@ use super::{ normalize_lexical, }; use crate::retrieval::exact::{ExactAdmissionAuthority, ExactLaneRequest}; -use crate::retrieval::ports::{RetrievalPortError, contract_error}; +use crate::retrieval::ports::{ + CodeCandidateBindingV1, CodeOccurrenceRefV1, RetrievalPortError, contract_error, +}; mod artifact; #[cfg(feature = "search-eval")] @@ -658,6 +661,87 @@ impl LexicalFieldTextV1 for ProjectedChunkV1 { } } +/// Row identity shared by the in-memory projection and the artifact reader. +trait LexicalIndexedRow { + fn chunk_id(&self) -> &CodeSearchChunkId; + fn anchor(&self) -> &CodeSearchChunkAnchorV1; + fn language_descriptor_revision(&self) -> &LanguageDescriptorRevision; +} + +impl LexicalIndexedRow for ProjectedChunkV1 { + fn chunk_id(&self) -> &CodeSearchChunkId { + &self.id + } + + fn anchor(&self) -> &CodeSearchChunkAnchorV1 { + &self.anchor + } + + fn language_descriptor_revision(&self) -> &LanguageDescriptorRevision { + &self.language_descriptor_revision + } +} + +fn lexical_lane_candidate( + row: &impl LexicalIndexedRow, + freshness: &SourceFreshness, + repository_id: Option, + retriever: RetrieverKind, + retriever_revision: ComponentRevision, + score_domain: ScoreDomainId, + exact_admission_proof: Option, +) -> Result { + let lane = retriever.as_str(); + let chunk_id = row.chunk_id().as_str(); + let generation = row.anchor().generation_id.as_str(); + let evidence_id = row.anchor().symbol_occurrence_id.as_ref().map_or_else( + || format!("code-chunk:{chunk_id}"), + |symbol| format!("code-symbol:{}", symbol.as_str()), + ); + Ok(CompactCandidate { + anchor_id: retrieval_anchor(evidence_id.clone())?, + logical_evidence_id: LogicalEvidenceId::new(evidence_id).map_err(contract_error)?, + source_occurrence_id: SourceOccurrenceId::new(format!( + "code-chunk:{generation}:{chunk_id}" + )) + .map_err(contract_error)?, + file_occurrence_id: Some(row.anchor().file_occurrence_id.clone()), + source_namespace: freshness.source_namespace.clone(), + repository_id, + session_or_thread_id: None, + logical_copy_cluster_id: None, + logical_copy_evidence_anchor: None, + evidence_role: EvidenceRole::Primary, + retriever, + retriever_revision, + score_domain, + raw_score: FixedPointScore::ZERO, + ordinal_rank: 0, + exact_admission_proof, + retriever_evidence_anchor: retrieval_anchor(format!("code-lexical:{lane}:{chunk_id}"))?, + freshness: freshness.clone(), + }) +} + +fn lexical_lane_binding( + row: &impl LexicalIndexedRow, + candidate: &CompactCandidate, + matched_term_kinds: Vec, +) -> CodeCandidateBindingV1 { + CodeCandidateBindingV1 { + candidate_anchor: candidate.anchor_id.clone(), + occurrence: CodeOccurrenceRefV1 { + generation: row.anchor().generation_id.clone(), + file: row.anchor().file_occurrence_id.clone(), + symbol: row.anchor().symbol_occurrence_id.clone(), + chunk: Some(row.chunk_id().clone()), + }, + language_descriptor_revision: row.language_descriptor_revision().clone(), + matched_term_kinds, + source_occurrence: candidate.source_occurrence_id.clone(), + } +} + fn normalized_field_text<'a>( row: &'a impl LexicalFieldTextV1, field: LexicalFieldV1, diff --git a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/reader.rs b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/reader.rs index 575aad0e2e..95ac9a4377 100644 --- a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/reader.rs +++ b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/reader.rs @@ -24,11 +24,10 @@ use tracedecay_code_index::clones::{ }; use tracedecay_code_index::production::CodeIndexExecutionControlV1; use tracedecay_domain::{ - CodeGenerationId, CodeSearchChunkGrainV1, CodeSearchChunkId, CompactCandidate, - ComponentRevision, EvidenceRole, ExactAdmissionProof, ExactFieldV1, ExactTechnicalTermKindV1, - FixedPointScore, LogicalEvidenceId, ManifestDigest, RetrieverBatch, RetrieverCoverage, - RetrieverKind, RetrieverOutcome, ScoreDomainId, SourceOccurrenceId, SourceSpan, - SymbolOccurrenceId, canonical_sha256, + CodeGenerationId, CodeSearchChunkAnchorV1, CodeSearchChunkGrainV1, CodeSearchChunkId, + CompactCandidate, ExactFieldV1, ExactTechnicalTermKindV1, LanguageDescriptorRevision, + ManifestDigest, RetrieverBatch, RetrieverCoverage, RetrieverKind, RetrieverOutcome, + SourceOccurrenceId, SourceSpan, SymbolOccurrenceId, canonical_sha256, }; use tracedecay_private_fs::open_private_file; @@ -58,16 +57,17 @@ use super::{ use crate::retrieval::exact::{ExactAdmissionAuthority, ExactLaneEvidence, ExactLaneRequest}; use crate::retrieval::ports::RetrievalExecutionControl; use crate::retrieval::ports::{ - CodeCandidateBindingV1, CodeOccurrenceRefV1, ExactTermPostingReadPort, LexicalPostingReadPort, + CodeCandidateBindingV1, ExactTermPostingReadPort, LexicalPostingReadPort, RETRIEVAL_CANDIDATE_BATCH_SIZE, RetrievalPortError, contract_error, lane_candidate_cap, retrieval_checkpoint, }; use super::super::{ ExactMatchRowViewV1, FuzzyExpansionsV1, FuzzyQueryGroupV1, LexicalFieldTextV1, - LexicalRowScoreV1, LiteralProofCacheV1, PreparedLexicalQueryV1, bm25_score_micros, - exact_matches, field_weight_millis, fuzzy_distance_bound, matches_phrase, normalize_lexical, - retrieval_anchor, score_lexical_row, + LexicalIndexedRow, LexicalRowScoreV1, LiteralProofCacheV1, PreparedLexicalQueryV1, + bm25_score_micros, exact_matches, field_weight_millis, fuzzy_distance_bound, + lexical_lane_binding, lexical_lane_candidate, matches_phrase, normalize_lexical, + score_lexical_row, }; use crate::retrieval::lexical::{ LexicalFieldFilterV1, LexicalFieldV1, LexicalLaneEvidence, LexicalLaneRequest, @@ -109,6 +109,20 @@ impl LexicalFieldTextV1 for ArtifactRowV1 { } } +impl LexicalIndexedRow for ArtifactRowV1 { + fn chunk_id(&self) -> &CodeSearchChunkId { + &self.id + } + + fn anchor(&self) -> &CodeSearchChunkAnchorV1 { + &self.anchor + } + + fn language_descriptor_revision(&self) -> &LanguageDescriptorRevision { + &self.language_descriptor_revision + } +} + #[derive(Clone)] pub struct CodeLexicalArtifactReaderV1 { connection: Arc>, @@ -2196,9 +2210,10 @@ impl<'a> ArtifactQueryV1<'a> { score, row, } = entry; - let mut candidate = candidate( - self.receipt, + let mut candidate = lexical_lane_candidate( &row, + self.receipt.freshness(), + self.receipt.repository_id().cloned(), RetrieverKind::Lexical, self.metadata.lexical_retriever_revision.clone(), request.score_domain.clone(), @@ -2206,7 +2221,7 @@ impl<'a> ArtifactQueryV1<'a> { )?; candidate.ordinal_rank = ordinal as u32; let evidence = LexicalLaneEvidence { - binding: binding(&row, &candidate, score.matched_kinds), + binding: lexical_lane_binding(&row, &candidate, score.matched_kinds), field_scores_micros: score.field_scores, matched_whole_terms: score.matched_whole_terms, matched_subtokens: score.matched_subtokens, @@ -2304,9 +2319,10 @@ impl<'a> ArtifactQueryV1<'a> { .map(|literal| request.literals[*literal].clone()) .collect::>(); let row = self.row(document)?; - let mut candidate = candidate( - self.receipt, + let mut candidate = lexical_lane_candidate( &row, + self.receipt.freshness(), + self.receipt.repository_id().cloned(), RetrieverKind::ExactLiteral, self.metadata.exact_retriever_revision.clone(), self.metadata.exact_score_domain.clone(), @@ -2314,7 +2330,7 @@ impl<'a> ArtifactQueryV1<'a> { )?; candidate.ordinal_rank = ordinal as u32; let evidence = ExactLaneEvidence { - binding: binding(&row, &candidate, matched_kinds), + binding: lexical_lane_binding(&row, &candidate, matched_kinds), matched_literals, admission_proof: proof, }; @@ -2893,65 +2909,6 @@ impl LexicalStatsCacheV1 { } } -fn candidate( - receipt: &VerifiedCodeLexicalArtifactV1, - row: &ArtifactRowV1, - retriever: RetrieverKind, - retriever_revision: ComponentRevision, - score_domain: ScoreDomainId, - exact_admission_proof: Option, -) -> Result { - let lane = retriever.as_str(); - let chunk_id = row.id.as_str(); - let generation = row.anchor.generation_id.as_str(); - let evidence_id = row.anchor.symbol_occurrence_id.as_ref().map_or_else( - || format!("code-chunk:{chunk_id}"), - |symbol| format!("code-symbol:{}", symbol.as_str()), - ); - Ok(CompactCandidate { - anchor_id: retrieval_anchor(evidence_id.clone())?, - logical_evidence_id: LogicalEvidenceId::new(evidence_id).map_err(contract_error)?, - source_occurrence_id: SourceOccurrenceId::new(format!( - "code-chunk:{generation}:{chunk_id}" - )) - .map_err(contract_error)?, - file_occurrence_id: Some(row.anchor.file_occurrence_id.clone()), - source_namespace: receipt.freshness().source_namespace.clone(), - repository_id: receipt.repository_id().cloned(), - session_or_thread_id: None, - logical_copy_cluster_id: None, - logical_copy_evidence_anchor: None, - evidence_role: EvidenceRole::Primary, - retriever, - retriever_revision, - score_domain, - raw_score: FixedPointScore::ZERO, - ordinal_rank: 0, - exact_admission_proof, - retriever_evidence_anchor: retrieval_anchor(format!("code-lexical:{lane}:{chunk_id}"))?, - freshness: receipt.freshness().clone(), - }) -} - -fn binding( - row: &ArtifactRowV1, - candidate: &CompactCandidate, - matched_term_kinds: Vec, -) -> CodeCandidateBindingV1 { - CodeCandidateBindingV1 { - candidate_anchor: candidate.anchor_id.clone(), - occurrence: CodeOccurrenceRefV1 { - generation: row.anchor.generation_id.clone(), - file: row.anchor.file_occurrence_id.clone(), - symbol: row.anchor.symbol_occurrence_id.clone(), - chunk: Some(row.id.clone()), - }, - language_descriptor_revision: row.language_descriptor_revision.clone(), - matched_term_kinds, - source_occurrence: candidate.source_occurrence_id.clone(), - } -} - /// One admitted exact candidate retained during bounded selection: the /// canonical ranking key plus ordinals into the request literals, the /// admitting literal and every matched literal. Winner materialization diff --git a/crates/tracedecay-query/src/retrieval/lexical/projection/in_memory.rs b/crates/tracedecay-query/src/retrieval/lexical/projection/in_memory.rs index 1a0f75ac44..7b6afc811b 100644 --- a/crates/tracedecay-query/src/retrieval/lexical/projection/in_memory.rs +++ b/crates/tracedecay-query/src/retrieval/lexical/projection/in_memory.rs @@ -11,11 +11,9 @@ use std::time::{Duration, Instant}; use roaring::RoaringBitmap; use tracedecay_code_index::production::VerifiedSealedLexicalSymbolDisplayV1; use tracedecay_domain::{ - CodeGenerationId, CodeSearchChunkV1, CompactCandidate, ComponentRevision, EvidenceRole, - ExactFieldV1, ExactTechnicalTermKindV1, ExactTechnicalTermV1, ExtractionAdmittedChunkV1, - FixedPointScore, FreshnessCompatibilityV1, LogicalEvidenceId, RetrieverBatch, - RetrieverCoverage, RetrieverKind, RetrieverOutcome, ScoreDomainId, SourceOccurrenceId, - SymbolOccurrenceId, + CodeGenerationId, CodeSearchChunkV1, ExactFieldV1, ExactTechnicalTermV1, + ExtractionAdmittedChunkV1, FreshnessCompatibilityV1, RetrieverBatch, RetrieverCoverage, + RetrieverKind, RetrieverOutcome, SymbolOccurrenceId, }; use super::super::{ @@ -26,13 +24,13 @@ use super::{ CodeLexicalProjectionMetadataV1, ExactMatchRowViewV1, FuzzyExpansionsV1, FuzzyQueryGroupV1, LexicalRowScoreV1, LiteralProofCacheV1, PreparedLexicalQueryV1, ProjectedChunkV1, bm25_score_micros, canonical_projected_exact_term, exact_field_for_kind, exact_matches, - field_weight_millis, fuzzy_distance_bound, matches_phrase, normalize_lexical, - normalized_search_text, retrieval_anchor, score_lexical_row, + field_weight_millis, fuzzy_distance_bound, lexical_lane_binding, lexical_lane_candidate, + matches_phrase, normalize_lexical, normalized_search_text, score_lexical_row, }; use crate::retrieval::exact::{ExactAdmissionAuthority, ExactLaneEvidence, ExactLaneRequest}; use crate::retrieval::ports::{ - CodeCandidateBindingV1, CodeOccurrenceRefV1, ExactTermPostingReadPort, LexicalPostingReadPort, - RETRIEVAL_CANDIDATE_BATCH_SIZE, RetrievalPortError, contract_error, retrieval_checkpoint, + ExactTermPostingReadPort, LexicalPostingReadPort, RETRIEVAL_CANDIDATE_BATCH_SIZE, + RetrievalPortError, contract_error, retrieval_checkpoint, }; mod postings; @@ -863,15 +861,17 @@ impl CodeLexicalProjectionAdapterV1 { excluded += 1; continue; } - let candidate = self.candidate( + let candidate = lexical_lane_candidate( row, + &self.metadata.freshness, + self.metadata.repository_id.clone(), RetrieverKind::Lexical, self.metadata.lexical_retriever_revision.clone(), request.score_domain.clone(), None, )?; let evidence = LexicalLaneEvidence { - binding: self.binding(row, &candidate, score.matched_kinds), + binding: lexical_lane_binding(row, &candidate, score.matched_kinds), field_scores_micros: score.field_scores, matched_whole_terms: score.matched_whole_terms, matched_subtokens: score.matched_subtokens, @@ -1019,66 +1019,6 @@ impl CodeLexicalProjectionAdapterV1 { ) }) } - - fn candidate( - &self, - row: &ProjectedChunkV1, - retriever: RetrieverKind, - retriever_revision: ComponentRevision, - score_domain: ScoreDomainId, - exact_admission_proof: Option, - ) -> Result { - let lane = retriever.as_str(); - let chunk_id = row.id.as_str(); - let generation = row.anchor.generation_id.as_str(); - let evidence_id = row.anchor.symbol_occurrence_id.as_ref().map_or_else( - || format!("code-chunk:{chunk_id}"), - |symbol| format!("code-symbol:{}", symbol.as_str()), - ); - Ok(CompactCandidate { - anchor_id: retrieval_anchor(evidence_id.clone())?, - logical_evidence_id: LogicalEvidenceId::new(evidence_id).map_err(contract_error)?, - source_occurrence_id: SourceOccurrenceId::new(format!( - "code-chunk:{generation}:{chunk_id}" - )) - .map_err(contract_error)?, - file_occurrence_id: Some(row.anchor.file_occurrence_id.clone()), - source_namespace: self.metadata.freshness.source_namespace.clone(), - repository_id: self.metadata.repository_id.clone(), - session_or_thread_id: None, - logical_copy_cluster_id: None, - logical_copy_evidence_anchor: None, - evidence_role: EvidenceRole::Primary, - retriever, - retriever_revision, - score_domain, - raw_score: FixedPointScore::ZERO, - ordinal_rank: 0, - exact_admission_proof, - retriever_evidence_anchor: retrieval_anchor(format!("code-lexical:{lane}:{chunk_id}"))?, - freshness: self.metadata.freshness.clone(), - }) - } - - fn binding( - &self, - row: &ProjectedChunkV1, - candidate: &CompactCandidate, - matched_term_kinds: Vec, - ) -> CodeCandidateBindingV1 { - CodeCandidateBindingV1 { - candidate_anchor: candidate.anchor_id.clone(), - occurrence: CodeOccurrenceRefV1 { - generation: row.anchor.generation_id.clone(), - file: row.anchor.file_occurrence_id.clone(), - symbol: row.anchor.symbol_occurrence_id.clone(), - chunk: Some(row.id.clone()), - }, - language_descriptor_revision: row.language_descriptor_revision.clone(), - matched_term_kinds, - source_occurrence: candidate.source_occurrence_id.clone(), - } - } } impl LexicalPostingReadPort for CodeLexicalProjectionAdapterV1 { @@ -1142,15 +1082,17 @@ where .iter() .map(|ordinal| request.literals[*ordinal].clone()) .collect::>(); - let candidate = self.projection.candidate( + let candidate = lexical_lane_candidate( row, + &self.projection.metadata.freshness, + self.projection.metadata.repository_id.clone(), RetrieverKind::ExactLiteral, self.projection.metadata.exact_retriever_revision.clone(), self.projection.metadata.exact_score_domain.clone(), Some(proof.clone()), )?; let evidence = ExactLaneEvidence { - binding: self.projection.binding(row, &candidate, matched_kinds), + binding: lexical_lane_binding(row, &candidate, matched_kinds), matched_literals, admission_proof: proof, }; From 0c4136ba196d5a500fef4bfd5340aad3bc6d3784 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:48:38 +0000 Subject: [PATCH 067/182] simplify(pass-3/5): share byte n-gram packing Co-authored-by: Zack Jackson --- .../src/retrieval/lexical/projection.rs | 18 +++++++++++++++ .../lexical/projection/artifact/postings.rs | 18 ++------------- .../lexical/projection/in_memory/postings.rs | 22 ++----------------- 3 files changed, 22 insertions(+), 36 deletions(-) diff --git a/crates/tracedecay-query/src/retrieval/lexical/projection.rs b/crates/tracedecay-query/src/retrieval/lexical/projection.rs index e7ee9a5165..c91356a7c2 100644 --- a/crates/tracedecay-query/src/retrieval/lexical/projection.rs +++ b/crates/tracedecay-query/src/retrieval/lexical/projection.rs @@ -838,6 +838,24 @@ fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool { .any(|window| window == needle) } +fn pack_byte_ngram(bytes: &[u8]) -> u32 { + debug_assert!((1..=3).contains(&bytes.len())); + bytes + .iter() + .enumerate() + .fold((bytes.len() as u32) << 24, |packed, (index, byte)| { + packed | (u32::from(*byte) << (index * 8)) + }) +} + +fn packed_query_ngrams(bytes: &[u8]) -> BTreeSet { + let width = bytes.len().min(3); + if width == 0 { + return BTreeSet::new(); + } + bytes.windows(width).map(pack_byte_ngram).collect() +} + fn add_score(scores: &mut BTreeMap, field: LexicalFieldV1, score: u64) { scores .entry(field) diff --git a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/postings.rs b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/postings.rs index 211674f880..7667481d54 100644 --- a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/postings.rs +++ b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/postings.rs @@ -90,7 +90,7 @@ pub(super) fn document_ngrams( if observed.is_multiple_of(4_096) { checkpoint(control)?; } - ngrams.push(pack_byte_ngram(window)); + ngrams.push(super::super::pack_byte_ngram(window)); observed = observed.checked_add(1).ok_or_else(|| { CodeLexicalArtifactErrorV1::Contract( "lexical artifact n-gram work count overflowed".to_owned(), @@ -104,21 +104,7 @@ pub(super) fn document_ngrams( } pub(super) fn query_ngrams(bytes: &[u8]) -> BTreeSet { - let width = bytes.len().min(3); - if width == 0 { - return BTreeSet::new(); - } - bytes.windows(width).map(pack_byte_ngram).collect() -} - -fn pack_byte_ngram(bytes: &[u8]) -> u32 { - debug_assert!((1..=3).contains(&bytes.len())); - bytes - .iter() - .enumerate() - .fold((bytes.len() as u32) << 24, |packed, (index, byte)| { - packed | (u32::from(*byte) << (index * 8)) - }) + super::super::packed_query_ngrams(bytes) } #[cfg(test)] diff --git a/crates/tracedecay-query/src/retrieval/lexical/projection/in_memory/postings.rs b/crates/tracedecay-query/src/retrieval/lexical/projection/in_memory/postings.rs index 19b56ab0b8..343989df23 100644 --- a/crates/tracedecay-query/src/retrieval/lexical/projection/in_memory/postings.rs +++ b/crates/tracedecay-query/src/retrieval/lexical/projection/in_memory/postings.rs @@ -102,7 +102,7 @@ impl ByteNgramPostings { })?; debug_assert_eq!(unique.capacity(), scratch_entries); for width in 1..=bytes.len().min(3) { - unique.extend(bytes.windows(width).map(pack_byte_ngram)); + unique.extend(bytes.windows(width).map(super::super::pack_byte_ngram)); } unique.sort_unstable(); unique.dedup(); @@ -242,15 +242,7 @@ impl ByteNgramPostings { } pub(super) fn candidate_documents(&self, needle: &[u8]) -> RoaringBitmap { - let width = needle.len().min(3); - if width == 0 { - return RoaringBitmap::new(); - } - let ngrams = needle - .windows(width) - .map(pack_byte_ngram) - .collect::>(); - let mut ngrams = ngrams.into_iter(); + let mut ngrams = super::super::packed_query_ngrams(needle).into_iter(); let Some(first) = ngrams.next() else { return RoaringBitmap::new(); }; @@ -358,16 +350,6 @@ impl ByteNgramBudget { } } -fn pack_byte_ngram(bytes: &[u8]) -> u32 { - debug_assert!((1..=3).contains(&bytes.len())); - bytes - .iter() - .enumerate() - .fold((bytes.len() as u32) << 24, |packed, (index, byte)| { - packed | (u32::from(*byte) << (index * 8)) - }) -} - fn pack_posting(ngram: u32, document: u32) -> u64 { (u64::from(ngram) << 32) | u64::from(document) } From 8e27bc6490d7ddd243f7c453bac415099360916d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:49:04 +0000 Subject: [PATCH 068/182] simplify(pass-4/5): drop redundant fixture slots Source-edit and analysis fixtures returned a unit beside the real value. Return the fixture itself and inline the one-caller fact assertion. Co-authored-by: Zack Jackson --- .../insert_at_symbol_behavior_test.rs | 4 +- .../tests/mcp_suite/mcp_handler_test.rs | 2 - .../ast_grep_rewrite_behavior_test.rs | 2 +- .../mcp_suite/mcp_handler_test/edit_test.rs | 50 ++--- .../mcp_handler_test/graph_analysis_test.rs | 189 +++++++++--------- .../mcp_handler_test/insert_at_test.rs | 2 +- .../memory_fact_assertions.rs | 25 --- .../mcp_handler_test/memory_facts_test.rs | 24 ++- .../move_symbol_behavior_test.rs | 4 +- .../mcp_handler_test/move_symbol_test.rs | 30 +-- .../multi_str_replace_behavior_test.rs | 2 +- .../mcp_handler_test/rename_symbol_test.rs | 12 +- .../mcp_handler_test/replace_symbol_test.rs | 2 +- .../source_edit_reconcile_test.rs | 2 +- .../source_edit_rollback_test.rs | 4 +- .../str_replace_behavior_test.rs | 2 +- crates/tracedecay/tests/mcp_suite/support.rs | 16 +- 17 files changed, 175 insertions(+), 197 deletions(-) delete mode 100644 crates/tracedecay/tests/mcp_suite/mcp_handler_test/memory_fact_assertions.rs diff --git a/crates/tracedecay/tests/mcp_suite/insert_at_symbol_behavior_test.rs b/crates/tracedecay/tests/mcp_suite/insert_at_symbol_behavior_test.rs index baa88c7c28..ae26be88c8 100644 --- a/crates/tracedecay/tests/mcp_suite/insert_at_symbol_behavior_test.rs +++ b/crates/tracedecay/tests/mcp_suite/insert_at_symbol_behavior_test.rs @@ -134,7 +134,7 @@ async fn open_sources(files: &[(&str, &str)]) -> (ProductionSourceEditFixture, T for (relative, body) in files { fs::write(project.join(relative), body).unwrap(); } - let (fixture, _) = init_production_source_edit_project(&project).await; + let fixture = init_production_source_edit_project(&project).await; (fixture, dir) } @@ -142,7 +142,7 @@ async fn open_pair() -> (ProductionSourceEditFixture, TestTempDir) { let dir = test_temp_dir(); let project = dir.path().join("project"); write_pair(&project); - let (fixture, _) = init_production_source_edit_project(&project).await; + let fixture = init_production_source_edit_project(&project).await; (fixture, dir) } diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test.rs index fe316b654c..55fcb493dc 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test.rs @@ -97,8 +97,6 @@ mod lcm_load_session_behavior; mod lcm_test; #[cfg(feature = "test-transport")] mod memory_contradiction_contract_test; -#[cfg(feature = "test-transport")] -mod memory_fact_assertions; mod memory_fact_probe_test; #[cfg(feature = "test-transport")] mod memory_fact_store_reason_test; diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/ast_grep_rewrite_behavior_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/ast_grep_rewrite_behavior_test.rs index 47c5157040..01fed5c1a1 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/ast_grep_rewrite_behavior_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/ast_grep_rewrite_behavior_test.rs @@ -68,7 +68,7 @@ async fn open_project(files: &[(&str, &str)]) -> (ProductionSourceEditFixture, T fs::create_dir_all(path.parent().expect("fixture file has a parent")).unwrap(); fs::write(&path, contents).unwrap(); } - let (fixture, _) = init_production_source_edit_project(&project).await; + let fixture = init_production_source_edit_project(&project).await; (fixture, dir) } diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/edit_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/edit_test.rs index f228faada0..13410a542a 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/edit_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/edit_test.rs @@ -35,7 +35,7 @@ async fn source_edit_preview_apply_and_retry_use_daemon_owned_cas_authority() { let initial = b"fn old_name() {}\r\n// exact \xE2\x98\x83\n"; let applied = b"fn new_name() {}\r\n// exact \xE2\x98\x83\n"; fs::write(project.join("src/main.rs"), initial).unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let preview = handle_tool_call( &cg, @@ -153,7 +153,7 @@ async fn path_containment_config_rejects_parent_traversal_before_serving_config( ) .unwrap(); - let (cg, _env) = init_test_project(&project).await; + let cg = init_test_project(&project).await; let result = handle_tool_call( &cg, @@ -179,7 +179,7 @@ async fn path_containment_read_rejects_parent_traversal_before_serving_file() { fs::write(project.join("src/main.rs"), "fn main() {}\n").unwrap(); fs::write(dir.path().join("outside.rs"), "fn leaked() {}\n").unwrap(); - let (cg, _env) = init_test_project(&project).await; + let cg = init_test_project(&project).await; let result = handle_tool_call( &cg, @@ -207,7 +207,7 @@ async fn read_and_outline_preserve_symlink_indexed_file_key() { fs::write(indexed_src.join("lib.rs"), "pub fn through_symlink() {}\n").unwrap(); unix_fs::symlink(&indexed_src, project.join("src")).unwrap(); - let (cg, _env) = init_test_project(&project).await; + let cg = init_test_project(&project).await; wait_for_source_generation(&cg, "through_symlink").await; let read = handle_tool_call( @@ -274,7 +274,7 @@ async fn outline_preserves_generation_payload_and_adds_ast_grep_outline_when_ava let dir = test_temp_dir(); let project = dir.path().join("project"); crate::fixture::write_indexed_fixture_sources(&project); - let (cg, _env) = init_test_project(&project).await; + let cg = init_test_project(&project).await; wait_for_source_generation(&cg, "helper").await; let result = handle_tool_call( &cg, @@ -329,7 +329,7 @@ async fn outline_markdown_section_carries_preview_handle_and_checklist_state() { ), ) .unwrap(); - let (cg, _env) = init_test_project(&project).await; + let cg = init_test_project(&project).await; let result = handle_tool_call( &cg, @@ -414,7 +414,7 @@ async fn path_containment_config_rejects_symlink_escape_before_serving_config() .unwrap(); unix_fs::symlink(&outside_dir, project.join("escape")).unwrap(); - let (cg, _env) = init_test_project(&project).await; + let cg = init_test_project(&project).await; let result = handle_tool_call( &cg, @@ -460,7 +460,7 @@ async fn test_str_replace_not_found() { fs::write(project.join("src/main.rs"), "fn hello() {}\n").unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, @@ -491,7 +491,7 @@ async fn test_str_replace_multiple_matches_fails() { fs::write(project.join("src/main.rs"), "fn foo() {}\nfn foo() {}\n").unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, @@ -527,7 +527,7 @@ async fn test_multi_str_replace_atomic_failure() { fs::write(project.join("src/main.rs"), "fn foo() {}\nfn baz() {}\n").unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, @@ -571,7 +571,7 @@ async fn test_multi_str_replace_unicode_preview_does_not_panic() { let original = "fn main() {}\n"; fs::write(project.join("src/main.rs"), original).unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let missing_old = format!("{}é", "a".repeat(19)); let result = handle_tool_call( @@ -620,7 +620,7 @@ async fn test_multi_str_replace_earlier_insertion_collision_lands_correctly() { ) .unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, @@ -664,7 +664,7 @@ async fn test_multi_str_replace_overlapping_ranges_error() { let original = "abcdef\n"; fs::write(project.join("src/main.rs"), original).unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, @@ -716,7 +716,7 @@ async fn test_replace_symbol_documented_fn_keeps_single_doc_comment() { ) .unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, @@ -766,7 +766,7 @@ async fn test_insert_at_symbol_before_lands_above_attribute() { ) .unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, @@ -816,7 +816,7 @@ async fn test_str_replace_unsupported_file_type_succeeds() { "stylesheet fixture must exist before dispatch" ); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; wait_for_source_generation(&cg, "source_edit_anchor").await; let result = handle_tool_call( @@ -860,7 +860,7 @@ async fn ast_grep_rewrite_has_literal_fallback_when_binary_missing() { fs::create_dir_all(project.join("src")).unwrap(); fs::write(project.join("src/lib.rs"), "pub fn old_name() {}\n").unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, "tracedecay_ast_grep_rewrite", @@ -896,7 +896,7 @@ async fn ast_grep_rewrite_uses_current_cli_update_flag() { ) .unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, "tracedecay_ast_grep_rewrite", @@ -939,7 +939,7 @@ async fn ast_grep_rewrite_surfaces_useful_error_on_empty_stderr() { fs::create_dir_all(project.join("src")).unwrap(); fs::write(project.join("src/lib.rs"), "pub fn foo() {}\n").unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, "tracedecay_ast_grep_rewrite", @@ -991,7 +991,7 @@ async fn test_multi_str_replace_unsupported_file_type_succeeds() { "stylesheet fixture must exist before dispatch" ); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; wait_for_source_generation(&cg, "source_edit_anchor").await; let result = handle_tool_call( @@ -1043,7 +1043,7 @@ async fn test_insert_at_string_anchor_before() { let applied = b"line one\nfirst inserted\nsecond inserted\nline two\nline three\n"; fs::write(project.join("src/main.rs"), initial).unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let preview = handle_tool_call( &cg, @@ -1098,7 +1098,7 @@ async fn test_insert_at_line_number() { ) .unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, @@ -1141,7 +1141,7 @@ async fn test_insert_at_anchor_not_found() { fs::write(project.join("src/main.rs"), "line one\nline two\n").unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, @@ -1174,7 +1174,7 @@ async fn test_insert_at_unicode_anchor_prefix_does_not_panic() { let original = "line one\nline two\n"; fs::write(project.join("src/main.rs"), original).unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let long_anchor = format!("{}é", "a".repeat(99)); let result = handle_tool_call( @@ -1214,7 +1214,7 @@ async fn test_insert_at_ambiguous_anchor() { ) .unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/graph_analysis_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/graph_analysis_test.rs index 99ea278962..6c804e5a29 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/graph_analysis_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/graph_analysis_test.rs @@ -123,17 +123,12 @@ async fn close_test_graph(host: impl AnalysisToolHost) { host.close_analysis_host().await; } -async fn setup_project() -> (ProductionCompositionFixture, ()) { - (production_composition_fixture().await, ()) -} - -async fn setup_empty_analysis_project() -> (ProductionCompositionFixture, (), ()) { - let fixture = production_composition_fixture_with_sources(|project| { +async fn setup_empty_analysis_project() -> ProductionCompositionFixture { + production_composition_fixture_with_sources(|project| { fs::create_dir_all(project.join("src")).unwrap(); fs::write(project.join("src/lib.rs"), "").unwrap(); }) - .await; - (fixture, (), ()) + .await } fn write_integration_test_risk_sources(project: &Path) { @@ -160,14 +155,12 @@ fn write_integration_test_risk_sources(project: &Path) { .unwrap(); } -async fn setup_integration_test_risk_project() -> (ProductionCompositionFixture, ()) { - let fixture = - production_composition_fixture_with_sources(write_integration_test_risk_sources).await; - (fixture, ()) +async fn setup_integration_test_risk_project() -> ProductionCompositionFixture { + production_composition_fixture_with_sources(write_integration_test_risk_sources).await } -async fn setup_test_risk_non_src_fixture() -> (ProductionCompositionFixture, ()) { - let fixture = production_composition_fixture_with_sources(|project| { +async fn setup_test_risk_non_src_fixture() -> ProductionCompositionFixture { + production_composition_fixture_with_sources(|project| { write_integration_test_risk_sources(project); fs::write( project.join("build.rs"), @@ -176,12 +169,11 @@ async fn setup_test_risk_non_src_fixture() -> (ProductionCompositionFixture, ()) ) .unwrap(); }) - .await; - (fixture, ()) + .await } -async fn setup_workspace_test_risk_fixture() -> (ProductionCompositionFixture, ()) { - let fixture = production_composition_fixture_with_sources(|project| { +async fn setup_workspace_test_risk_fixture() -> ProductionCompositionFixture { + production_composition_fixture_with_sources(|project| { fs::create_dir_all(project.join("crates/demo/src")).unwrap(); fs::create_dir_all(project.join("crates/demo/tests")).unwrap(); fs::write( @@ -205,12 +197,11 @@ async fn setup_workspace_test_risk_fixture() -> (ProductionCompositionFixture, ( ) .unwrap(); }) - .await; - (fixture, ()) + .await } -async fn setup_ts_describe_it_project() -> (ProductionCompositionFixture, ()) { - let fixture = production_composition_fixture_with_sources(|project| { +async fn setup_ts_describe_it_project() -> ProductionCompositionFixture { + production_composition_fixture_with_sources(|project| { fs::create_dir_all(project.join("src")).unwrap(); fs::write( project.join("package.json"), @@ -229,12 +220,11 @@ async fn setup_ts_describe_it_project() -> (ProductionCompositionFixture, ()) { ) .unwrap(); }) - .await; - (fixture, ()) + .await } -async fn setup_unsafe_block_fixture() -> (ProductionCompositionFixture, ()) { - let fixture = production_composition_fixture_with_sources(|project| { +async fn setup_unsafe_block_fixture() -> ProductionCompositionFixture { + production_composition_fixture_with_sources(|project| { fs::create_dir_all(project.join("src")).unwrap(); fs::write( project.join("Cargo.toml"), @@ -260,11 +250,10 @@ pub fn safe_add(a: u64, b: u64) -> u64 { ) .unwrap(); }) - .await; - (fixture, ()) + .await } -async fn init_test_project(project: &Path) -> (MountedProductionProject, ()) { +async fn init_test_project(project: &Path) -> MountedProductionProject { if !project.join(".git").is_dir() { git_run(project, &["init", "--quiet"]); git_run(project, &["add", "."]); @@ -294,7 +283,7 @@ async fn init_test_project(project: &Path) -> (MountedProductionProject, ()) { project_root: project.to_path_buf(), }; wait_for_current_graph(&mounted).await; - (mounted, ()) + mounted } #[tokio::test] @@ -330,7 +319,7 @@ pub fn recovered() -> BuildOptions { "#, ) .unwrap(); - let (graph, _env) = init_test_project(&project_root).await; + let graph = init_test_project(&project_root).await; let result = handle_tool_call( &graph, @@ -392,7 +381,7 @@ pub mod second { "#, ) .unwrap(); - let (graph, _env) = init_test_project(&project_root).await; + let graph = init_test_project(&project_root).await; let result = handle_tool_call( &graph, @@ -459,7 +448,7 @@ export default defineConfig({ "export const orphan = 1;\n", ) .unwrap(); - let (graph, _env) = init_test_project(&project_root).await; + let graph = init_test_project(&project_root).await; let result = handle_tool_call( &graph, @@ -544,7 +533,7 @@ async fn test_branch_list_reports_live_vs_serving_drift_state() { /// zero below a real negative result rather than an unpopulated index. #[tokio::test] async fn test_dead_code() { - let (cg, _dir) = setup_project().await; + let cg = production_composition_fixture().await; let _populated = find_node_id(&cg, "format_greeting").await; let result = handle_tool_call(&cg, "tracedecay_dead_code", json!({}), None, None) @@ -571,7 +560,7 @@ async fn test_dead_code() { /// of the file's symbols as modified and `main` as impacted downstream. #[tokio::test] async fn test_diff_context() { - let (cg, _dir) = setup_project().await; + let cg = production_composition_fixture().await; wait_for_current_graph(&cg).await; let result = handle_tool_call( &cg, @@ -629,7 +618,7 @@ async fn test_diff_context() { /// a real "no cycles here" rather than "nothing was analysed". #[tokio::test] async fn test_circular() { - let (cg, _dir) = setup_project().await; + let cg = production_composition_fixture().await; let _populated = find_node_id(&cg, "helper").await; let result = handle_tool_call(&cg, "tracedecay_circular", json!({}), None, None) @@ -652,7 +641,7 @@ async fn test_circular() { #[tokio::test] async fn test_rename_preview() { - let (cg, _dir) = setup_project().await; + let cg = production_composition_fixture().await; let node_id = find_node_id(&cg, "helper").await; let result = handle_tool_call( &cg, @@ -709,7 +698,7 @@ async fn test_rename_preview() { /// proves the call edges are present, which is what makes zero meaningful. #[tokio::test] async fn test_recursion() { - let (cg, _dir) = setup_project().await; + let cg = production_composition_fixture().await; let _populated = find_node_id(&cg, "format_greeting").await; let result = handle_tool_call(&cg, "tracedecay_recursion", json!({}), None, None) @@ -800,7 +789,7 @@ async fn pr_context_no_git_returns_structured_git_error() { #[tokio::test] async fn test_port_status() { - let (cg, _dir) = setup_project().await; + let cg = production_composition_fixture().await; wait_for_current_graph(&cg).await; let result = handle_tool_call( &cg, @@ -883,7 +872,7 @@ async fn port_status_does_not_match_methods_of_different_parents() { ) .unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, @@ -942,7 +931,7 @@ async fn port_status_matches_methods_with_same_parent_type() { ) .unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, @@ -969,7 +958,7 @@ async fn port_status_matches_methods_with_same_parent_type() { #[tokio::test] async fn test_port_order() { - let (cg, _dir) = setup_project().await; + let cg = production_composition_fixture().await; wait_for_current_graph(&cg).await; let result = handle_tool_call( &cg, @@ -1048,7 +1037,7 @@ async fn port_order_sorts_a_tied_level_before_applying_the_limit() { "pub fn zeta() {}\npub fn alpha() {}\npub fn middle() {}\n", ) .unwrap(); - let (cg, _env) = init_test_project(&project_root).await; + let cg = init_test_project(&project_root).await; let result = handle_tool_call( &cg, @@ -1073,7 +1062,7 @@ async fn port_order_sorts_a_tied_level_before_applying_the_limit() { #[tokio::test] async fn test_rename_preview_not_found() { - let (cg, _env, _dir) = setup_empty_analysis_project().await; + let cg = setup_empty_analysis_project().await; let result = handle_tool_call( &cg, "tracedecay_rename_preview", @@ -1112,7 +1101,7 @@ async fn commit_context_clean_worktree_returns_json() { git_run(project, &["add", "."]); git_run(project, &["commit", "-m", "init"]); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, "tracedecay_commit_context", @@ -1166,7 +1155,7 @@ async fn commit_context_staged_source_and_test_reports_symbols() { ); git_run(project, &["add", "src/lib.rs", "tests/invoice_test.rs"]); - let (host, _env) = init_test_project(project).await; + let host = init_test_project(project).await; let result = handle_tool_call( &host, "tracedecay_commit_context", @@ -1228,7 +1217,7 @@ async fn commit_context_config_and_docs_report_chore() { write_project_file(project, "notes.txt", "Ship the invoice total.\n"); git_run(project, &["add", "billing.cfg", "notes.txt"]); - let (host, _env) = init_test_project(project).await; + let host = init_test_project(project).await; let result = handle_tool_call( &host, "tracedecay_commit_context", @@ -1291,7 +1280,7 @@ async fn commit_context_staged_only_excludes_unstaged_file() { ); git_run(project, &["add", "src/lib.rs"]); - let (host, _env) = init_test_project(project).await; + let host = init_test_project(project).await; let staged = handle_tool_call( &host, "tracedecay_commit_context", @@ -1367,7 +1356,7 @@ async fn commit_context_unborn_head_is_git_status_error() { &[("src/lib.rs", "pub fn baseline() {}\n")], "seed context", ); - let (host, _env) = init_test_project(project).await; + let host = init_test_project(project).await; git_run(project, &["symbolic-ref", "HEAD", "refs/heads/unborn"]); let result = handle_tool_call( @@ -1448,7 +1437,7 @@ async fn test_changelog_with_real_git() { git_run(project, &["add", "."]); git_run(project, &["commit", "-m", "add function"]); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, @@ -1489,7 +1478,7 @@ async fn test_changelog_with_real_git() { /// breakdown. #[tokio::test] async fn test_health_detailed_includes_raw_signals() { - let (cg, _dir) = setup_project().await; + let cg = production_composition_fixture().await; let result = handle_tool_call( &cg, "tracedecay_health", @@ -1804,7 +1793,7 @@ _No dependency clusters found._ #[tokio::test] async fn test_test_risk() { - let (cg, _dir) = setup_project().await; + let cg = production_composition_fixture().await; let result = handle_tool_call( &cg, "tracedecay_test_risk", @@ -1845,7 +1834,7 @@ async fn test_test_risk() { #[tokio::test] async fn test_test_risk_distinguishes_direct_and_closure_attribution() { - let (cg, _dir) = setup_integration_test_risk_project().await; + let cg = setup_integration_test_risk_project().await; let result = handle_tool_call( &cg, "tracedecay_test_risk", @@ -1921,7 +1910,7 @@ async fn test_test_risk_distinguishes_direct_and_closure_attribution() { #[tokio::test] async fn test_test_risk_scopes_workspace_source_before_following_external_test_callers() { - let (cg, _dir) = setup_workspace_test_risk_fixture().await; + let cg = setup_workspace_test_risk_fixture().await; let result = handle_tool_call( &cg, "tracedecay_test_risk", @@ -1951,7 +1940,7 @@ async fn test_test_risk_scopes_workspace_source_before_following_external_test_c #[tokio::test] async fn test_test_risk_attributes_ts_describe_it_tests() { - let (cg, _dir) = setup_ts_describe_it_project().await; + let cg = setup_ts_describe_it_project().await; let result = handle_tool_call( &cg, "tracedecay_test_risk", @@ -1991,7 +1980,7 @@ async fn test_test_risk_attributes_ts_describe_it_tests() { #[tokio::test] async fn test_test_map_lists_ts_it_title_as_covering_test() { - let (cg, _dir) = setup_ts_describe_it_project().await; + let cg = setup_ts_describe_it_project().await; let result = handle_tool_call( &cg, "tracedecay_test_map", @@ -2021,7 +2010,7 @@ async fn test_test_map_lists_ts_it_title_as_covering_test() { #[tokio::test] async fn test_test_risk_excludes_non_src_functions_from_denominator_and_risks() { - let (cg, _dir) = setup_test_risk_non_src_fixture().await; + let cg = setup_test_risk_non_src_fixture().await; let result = handle_tool_call( &cg, "tracedecay_test_risk", @@ -2089,7 +2078,7 @@ fn helper() { "#, ) .unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; wait_for_current_graph(&cg).await; let result = handle_tool_call(&cg, "tracedecay_todos", json!({}), None, None) @@ -2138,7 +2127,7 @@ fn main() { "#, ) .unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, @@ -2177,7 +2166,7 @@ pub fn second() { dep::shared(); } ) .unwrap(); fs::write(project.join("src/dep.rs"), "pub fn shared() {}\n").unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, @@ -2216,13 +2205,13 @@ async fn recursion_keeps_direct_recursion() { "pub fn recurse(n: u32) -> u32 {\n if n == 0 { 0 } else { recurse(n - 1) }\n}\n\npub fn nonrecursive() -> u32 { 42 }\n", ) .unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call(&cg, "tracedecay_recursion", json!({}), None, None) .await .unwrap(); let output = extract_json(&result.value); assert_eq!( -public_recursion_report(&output), + public_recursion_report(&output), json!({ "cycle_count": 1, "cycles": [{ @@ -2235,7 +2224,7 @@ public_recursion_report(&output), }), "direct recursion must be the only cycle, and `nonrecursive` must stay out: {output}" ); -assert_reported_cycles_close(&output); + assert_reported_cycles_close(&output); } #[tokio::test] @@ -2250,13 +2239,13 @@ async fn recursion_filters_self_edge_artifacts() { "pub fn recurse(n: u32) -> u32 {\n if n == 0 { 0 } else { recurse(n - 1) }\n}\n\npub struct Triplet {\n rows: Vec,\n}\n\nimpl Triplet {\n pub fn push(&mut self, row: usize) {\n self.rows.push(row);\n }\n}\n", ) .unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call(&cg, "tracedecay_recursion", json!({}), None, None) .await .unwrap(); let output = extract_json(&result.value); assert_eq!( -public_recursion_report(&output), + public_recursion_report(&output), json!({ "cycle_count": 1, "cycles": [{ @@ -2269,7 +2258,7 @@ public_recursion_report(&output), }), "`self.rows.push` must not become a cycle while `recurse` is reported: {output}" ); -assert_reported_cycles_close(&output); + assert_reported_cycles_close(&output); } #[tokio::test] @@ -2288,13 +2277,13 @@ pub fn c() { a(); } "#, ) .unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call(&cg, "tracedecay_recursion", json!({}), None, None) .await .unwrap(); let output = extract_json(&result.value); assert_eq!( -public_recursion_report(&output), + public_recursion_report(&output), json!({ "cycle_count": 1, "cycles": [{ @@ -2309,7 +2298,7 @@ public_recursion_report(&output), }), "the only cycle is a -> b -> c -> a: {output}" ); -assert_reported_cycles_close(&output); + assert_reported_cycles_close(&output); } /// `tracedecay_changelog`'s response must not list directories under @@ -2337,7 +2326,7 @@ async fn changelog_filters_directory_paths() { fs::write(project.join("src/sub/added.rs"), "pub fn a() {}\n").unwrap(); git_run(project, &["add", "."]); git_run(project, &["commit", "-m", "two"]); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, @@ -2384,7 +2373,7 @@ pub fn caller() { called(); } "#, ) .unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let default_result = handle_tool_call(&cg, "tracedecay_dead_code", json!({}), None, None) .await @@ -2438,7 +2427,7 @@ async fn diagnose_normalizes_absolute_and_backslash_paths() { "pub fn target() {}\npub fn caller() { target(); }\n", ) .unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let abs_path = project.join("src/lib.rs"); let abs_str = abs_path.to_string_lossy().to_string(); @@ -2504,7 +2493,7 @@ pub fn helper() {} "#, ) .unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let caller_id = find_node_id(&cg, "caller").await; let result = handle_tool_call( @@ -2563,7 +2552,7 @@ impl Default for B { fn default() -> Self { B } } "#, ) .unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, @@ -2623,7 +2612,7 @@ async fn circular_reports_one_entry_per_scc_not_per_walk() { "use crate::a::a_fn;\npub fn c_fn() { a_fn(); }\n", ) .unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call(&cg, "tracedecay_circular", json!({}), None, None) .await .unwrap(); @@ -2674,7 +2663,7 @@ pub fn leaf() {} "#, ) .unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, "tracedecay_port_order", @@ -2736,7 +2725,7 @@ pub fn h() { a(); } "#, ) .unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, "tracedecay_port_order", @@ -2814,7 +2803,7 @@ impl Triplet { "#, ) .unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, "tracedecay_port_order", @@ -2851,7 +2840,7 @@ pub trait Leaf: Middle {} "#, ) .unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call(&cg, "tracedecay_inheritance_depth", json!({}), None, None) .await .unwrap(); @@ -2901,7 +2890,7 @@ async fn analysis_symbol_locations_are_one_based() { "function abandonedHelper(): number { return 7; }\nexport function entry(): number { return 1; }\n", ) .unwrap(); - let (graph, _env) = init_test_project(&project_root).await; + let graph = init_test_project(&project_root).await; for (tool, arguments, collection, symbol, expected_line) in [ ( @@ -3006,7 +2995,7 @@ async fn typescript_typed_variables_reach_public_type_relation_queries() { export let fallback: Greeter = primary;\n", ) .unwrap(); - let (graph, ()) = init_test_project(&project_root).await; + let graph = init_test_project(&project_root).await; let greeter_id = find_node_id(&graph, "Greeter").await; let variable_ids = [ ("primary", find_node_id(&graph, "primary").await), @@ -3096,7 +3085,7 @@ function helper() { return unrelated; } "#, ) .unwrap(); - let (cg, _env) = init_test_project(&project_root).await; + let cg = init_test_project(&project_root).await; let parent_id = find_node_id(&cg, "SettingsEditable").await; let hierarchy = handle_tool_call( @@ -3250,7 +3239,7 @@ async fn circular_emits_disjoint_sccs_under_load() { ) .unwrap(); } - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; // Disjointness is only observable when every member is listed, so raise the // member bound above this fixture's 15-file component. let result = handle_tool_call( @@ -3302,7 +3291,7 @@ async fn diff_context_dedupes_modified_symbols_on_duplicate_input() { "pub struct S; pub fn one() {} pub fn two() {}\n", ) .unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, @@ -3351,7 +3340,7 @@ async fn changelog_filters_deleted_directory_entries() { fs::remove_dir_all(project.join("crates")).unwrap(); git_run(project, &["add", "-A"]); git_run(project, &["commit", "-m", "drop crates"]); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, "tracedecay_changelog", @@ -3411,7 +3400,7 @@ async fn pr_context_collapses_cargo_toml_keys() { git_run(project, &["add", "."]); git_run(project, &["commit", "-m", "deps"]); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, @@ -3585,7 +3574,7 @@ async fn pr_context_reports_the_pinned_feature_summary() { Some("2020-01-03T03:04:05Z"), ); - let (host, _env) = init_test_project(project).await; + let host = init_test_project(project).await; let feature = pr_context_json( &host, json!({"format": "json", "base_ref": "master", "head_ref": "feature"}), @@ -3801,7 +3790,7 @@ fn dead_helper_with_attr() {} "#, ) .unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call(&cg, "tracedecay_dead_code", json!({}), None, None) .await @@ -3826,7 +3815,7 @@ fn dead_helper_with_attr() {} /// must surface the site. #[tokio::test] async fn unsafe_patterns_reports_unsafe_block_in_markdown_and_json() { - let (cg, _project) = setup_unsafe_block_fixture().await; + let cg = setup_unsafe_block_fixture().await; // Markdown is the runtime default; request it explicitly so the test helper // (which force-injects `format=json` for tools outside its allowlist) does @@ -3921,7 +3910,7 @@ pub fn write_both(target: &mut Target, other: &mut Other) { "#, ) .unwrap(); - let (host, _env) = init_test_project(&project_root).await; + let host = init_test_project(&project_root).await; let result = handle_tool_call( &host, @@ -3986,7 +3975,7 @@ pub fn while_let_then_sibling(target: &Target, mut other: Option) -> u32 let shadow_root = shadow_dir.path().join("project"); fs::create_dir_all(shadow_root.join("src")).unwrap(); fs::write(shadow_root.join("src/lib.rs"), shadow_source).unwrap(); - let (shadow_host, _shadow_env) = init_test_project(&shadow_root).await; + let shadow_host = init_test_project(&shadow_root).await; let error = expect_tool_error( handle_tool_call( &shadow_host, @@ -4017,7 +4006,7 @@ async fn field_sites_ignores_field_text_in_real_rust_literals() { )), ) .unwrap(); - let (host, _env) = init_test_project(&project_root).await; + let host = init_test_project(&project_root).await; let result = handle_tool_call( &host, @@ -4126,7 +4115,7 @@ async fn field_sites_behavior_reports_literal_read_and_write_sites() { let project_root = dir.path().join("project"); fs::create_dir_all(project_root.join("src")).unwrap(); fs::write(project_root.join("src/lib.rs"), FIELD_BEHAVIOR_SOURCE).unwrap(); - let (host, _env) = init_test_project(&project_root).await; + let host = init_test_project(&project_root).await; let read_method = "src/lib.rs::Counter::read"; let bump = "src/lib.rs::bump"; @@ -4240,7 +4229,7 @@ async fn field_sites_behavior_reports_literal_read_and_write_sites() { let qualified_root = qualified_dir.path().join("project"); fs::create_dir_all(qualified_root.join("src")).unwrap(); fs::write(qualified_root.join("src/lib.rs"), FIELD_QUALIFIED_SOURCE).unwrap(); - let (qualified_host, _qualified_env) = init_test_project(&qualified_root).await; + let qualified_host = init_test_project(&qualified_root).await; let qualified = call_field_sites( &qualified_host, json!({"field": "Counter::n", "format": "json"}), @@ -4311,7 +4300,7 @@ pub fn closure_then_sibling(counter: &Counter) -> u32 { "#, ) .unwrap(); - let (host, _env) = init_test_project(&project_root).await; + let host = init_test_project(&project_root).await; let error = expect_tool_error( handle_tool_call( @@ -4466,7 +4455,7 @@ async fn diff_context_reports_changed_symbols_callers_and_refuses_invalid_input( let dir = test_temp_dir(); let project = dir.path().join("project"); write_call_chain(&project); - let (host, _) = init_test_project(&project).await; + let host = init_test_project(&project).await; let changed = handle_tool_call( &host, @@ -4916,7 +4905,7 @@ async fn gini_reports_literal_coefficients_for_known_distributions() { #[tokio::test] async fn gini_empty_index_reports_perfect_equality() { - let (host, _, _) = setup_empty_analysis_project().await; + let host = setup_empty_analysis_project().await; let payload = gini_json(&host, json!({"format": "json"})).await; assert_eq!( payload, @@ -5211,7 +5200,7 @@ async fn hotspots_ranks_symbols_by_edge_degree_and_clamps_limit() { let chain_dir = test_temp_dir(); let chain_root = chain_dir.path().join("project"); write_chain_project(&chain_root); - let (chain, _env) = init_test_project(&chain_root).await; + let chain = init_test_project(&chain_root).await; let chain_default = call_hotspots(&chain, json!({"format": "json"})).await; let chain_limit_one = call_hotspots(&chain, json!({"format": "json", "limit": 1})).await; @@ -5263,7 +5252,7 @@ async fn hotspots_ranks_symbols_by_edge_degree_and_clamps_limit() { let fanout_dir = test_temp_dir(); let fanout_root = fanout_dir.path().join("project"); let fanout_bytes = write_fanout_project(&fanout_root); - let (fanout, _env) = init_test_project(&fanout_root).await; + let fanout = init_test_project(&fanout_root).await; let fanout_default = call_hotspots(&fanout, json!({"format": "json"})).await; let fanout_capped = call_hotspots(&fanout, json!({"format": "json", "limit": 250})).await; let fanout_one = call_hotspots(&fanout, json!({"format": "json", "limit": 1})).await; @@ -5488,7 +5477,7 @@ async fn recursion_reports_literal_cycles_and_refuses_non_positive_limit() { let dir = test_temp_dir(); let project_root = dir.path().join("project"); fs_write_fixture(&project_root); - let (graph, ()) = init_test_project(&project_root).await; + let graph = init_test_project(&project_root).await; let payload = call_recursion(&graph, json!({"format": "json", "limit": 10})).await; assert_eq!( diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/insert_at_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/insert_at_test.rs index 5dcbc7eea1..9f2549e842 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/insert_at_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/insert_at_test.rs @@ -43,7 +43,7 @@ async fn open_project(files: &[(&str, &str)]) -> InsertProject { fs::create_dir_all(path.parent().unwrap_or(&project_root)).unwrap(); fs::write(path, contents).unwrap(); } - let (fixture, _) = init_production_source_edit_project(&project_root).await; + let fixture = init_production_source_edit_project(&project_root).await; let server = fixture .harness .server(&fixture.project_root) diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/memory_fact_assertions.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/memory_fact_assertions.rs deleted file mode 100644 index a68bd2e761..0000000000 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/memory_fact_assertions.rs +++ /dev/null @@ -1,25 +0,0 @@ -use serde_json::Value; - -pub(super) fn assert_fact_list(payload: &Value, included: &str, excluded: &str, context: &str) { - let facts = payload["facts"] - .as_array() - .unwrap_or_else(|| panic!("{context} must return canonical facts: {payload}")); - assert_eq!(facts.len(), 1, "{context}: {payload}"); - let contents: Vec<&str> = facts - .iter() - .map(|projection| { - assert_eq!(projection["kind"], "available", "{context}: {payload}"); - projection["fact"]["content"] - .as_str() - .unwrap_or_else(|| panic!("{context} fact content: {payload}")) - }) - .collect(); - assert!( - contents.iter().any(|content| content.contains(included)), - "{context}: {payload}" - ); - assert!( - contents.iter().all(|content| !content.contains(excluded)), - "{context}: {payload}" - ); -} diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/memory_facts_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/memory_facts_test.rs index b3d730b493..22a460b77d 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/memory_facts_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/memory_facts_test.rs @@ -7,7 +7,29 @@ use std::fs; use std::path::Path; use std::sync::Arc; -use super::memory_fact_assertions::assert_fact_list; +fn assert_fact_list(payload: &Value, included: &str, excluded: &str, context: &str) { + let facts = payload["facts"] + .as_array() + .unwrap_or_else(|| panic!("{context} must return canonical facts: {payload}")); + assert_eq!(facts.len(), 1, "{context}: {payload}"); + let contents: Vec<&str> = facts + .iter() + .map(|projection| { + assert_eq!(projection["kind"], "available", "{context}: {payload}"); + projection["fact"]["content"] + .as_str() + .unwrap_or_else(|| panic!("{context} fact content: {payload}")) + }) + .collect(); + assert!( + contents.iter().any(|content| content.contains(included)), + "{context}: {payload}" + ); + assert!( + contents.iter().all(|content| !content.contains(excluded)), + "{context}: {payload}" + ); +} /// The fact-store surfaces are daemon-owned application operations. Keep these /// tests on the production composition so they cannot accidentally exercise diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/move_symbol_behavior_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/move_symbol_behavior_test.rs index b676abb5f9..b71c5e4bf5 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/move_symbol_behavior_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/move_symbol_behavior_test.rs @@ -169,7 +169,7 @@ async fn dry_run_then_apply_moves_compute_grand_total() { let project_root = dir.path().join("project"); let project = project_root.as_path(); write_pricing_crate(project); - let (fixture, ()) = init_production_source_edit_project(project).await; + let fixture = init_production_source_edit_project(project).await; let server = fixture .harness .server(project) @@ -294,7 +294,7 @@ async fn move_symbol_refuses_unsafe_or_stale_requests() { let project_root = dir.path().join("project"); let project = project_root.as_path(); write_pricing_crate(project); - let (fixture, ()) = init_production_source_edit_project(project).await; + let fixture = init_production_source_edit_project(project).await; let server = fixture .harness .server(project) diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/move_symbol_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/move_symbol_test.rs index 3ba160ae9c..d756f37a0f 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/move_symbol_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/move_symbol_test.rs @@ -16,7 +16,7 @@ async fn test_move_symbol_dry_run_reports_impact_and_writes_nothing() { let project_root = dir.path().join("project"); let project = project_root.as_path(); move_pricing_fixture(project).await; - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let before_pricing = fs::read_to_string(project.join("src/pricing.rs")).unwrap(); let before_orders = fs::read_to_string(project.join("src/orders.rs")).unwrap(); @@ -101,7 +101,7 @@ async fn test_move_symbol_resolves_qualified_names_like_bare_names() { let project_root = dir.path().join("project"); let project = project_root.as_path(); move_pricing_fixture(project).await; - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let before_pricing = fs::read_to_string(project.join("src/pricing.rs")).unwrap(); let before_orders = fs::read_to_string(project.join("src/orders.rs")).unwrap(); @@ -177,7 +177,7 @@ async fn test_move_symbol_only_prefers_callable_for_bare_names() { "pub mod common {\n pub struct same;\n}\n", ) .unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let before_a = fs::read_to_string(project.join("src/a.rs")).unwrap(); let before_b = fs::read_to_string(project.join("src/b.rs")).unwrap(); @@ -240,7 +240,7 @@ async fn test_move_symbol_apply_moves_and_rerun_errors_cleanly() { let project_root = dir.path().join("project"); let project = project_root.as_path(); move_pricing_fixture(project).await; - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, @@ -314,7 +314,7 @@ async fn test_move_symbol_clean_move_has_empty_impact() { "//! b\n\npub fn other() -> u32 {\n 0\n}\n", ) .unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, @@ -354,7 +354,7 @@ async fn test_move_symbol_dest_collision_refuses() { "//! b\n\npub fn dup() -> u32 {\n 2\n}\n", ) .unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let before_b = fs::read_to_string(project.join("src/b.rs")).unwrap(); let result = handle_tool_call( @@ -401,7 +401,7 @@ async fn test_move_symbol_private_dependency_hints() { "//! b\n\npub fn other() -> u32 {\n 0\n}\n", ) .unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, @@ -471,7 +471,7 @@ async fn test_move_symbol_qualified_caller_hint() { }\n", ) .unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, @@ -529,7 +529,7 @@ async fn test_move_symbol_first_in_file_docs_travel() { "//! b\n\npub fn other() -> u32 {\n 0\n}\n", ) .unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, @@ -577,7 +577,7 @@ async fn test_move_symbol_dot_prefixed_same_file_refuses() { let project_root = dir.path().join("project"); let project = project_root.as_path(); move_pricing_fixture(project).await; - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let before_pricing = fs::read_to_string(project.join("src/pricing.rs")).unwrap(); let result = handle_tool_call( @@ -617,7 +617,7 @@ async fn test_move_symbol_symlink_escape_refuses() { move_pricing_fixture(project).await; let outside = tempfile::tempdir().unwrap(); unix_fs::symlink(outside.path(), project.join("src/escape")).unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let before = fs::read_to_string(project.join("src/pricing.rs")).unwrap(); let result = handle_tool_call( @@ -665,7 +665,7 @@ async fn test_move_symbol_aliases_to_source_refuse() { } else { unix_fs::symlink(&source, &alias).unwrap(); } - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let before = fs::read_to_string(&source).unwrap(); let result = handle_tool_call( @@ -703,7 +703,7 @@ async fn test_move_symbol_dot_prefixed_dest_normalizes() { let project_root = dir.path().join("project"); let project = project_root.as_path(); move_pricing_fixture(project).await; - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, @@ -752,7 +752,7 @@ async fn test_move_symbol_leaves_contiguous_module_doc_behind() { "//! b\n\npub fn other() -> u32 {\n 0\n}\n", ) .unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let result = handle_tool_call( &cg, @@ -807,7 +807,7 @@ async fn test_move_symbol_non_utf8_destination_refuses() { "//! a\n\npub fn movable() -> u32 {\n 1\n}\n", ) .unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let server = cg .harness .server(&cg.project_root) diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/multi_str_replace_behavior_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/multi_str_replace_behavior_test.rs index 5dfdf5c246..7535a56a8e 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/multi_str_replace_behavior_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/multi_str_replace_behavior_test.rs @@ -29,7 +29,7 @@ async fn open_fixture(files: &[(&str, &str)]) -> (TestTempDir, ProductionSourceE fs::create_dir_all(path.parent().expect("fixture file has a parent")).unwrap(); fs::write(&path, contents).unwrap(); } - let (fixture, ()) = init_production_source_edit_project(&project).await; + let fixture = init_production_source_edit_project(&project).await; (dir, fixture) } diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/rename_symbol_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/rename_symbol_test.rs index 987d9e6102..1839094674 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/rename_symbol_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/rename_symbol_test.rs @@ -333,7 +333,7 @@ async fn test_rename_symbol_dry_run_default_reports_plan_and_writes_nothing() { let project_root = dir.path().join("project"); let project = project_root.as_path(); rename_fixture(project).await; - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let node = preview_node(&cg, "compute_grand_total").await; assert_eq!(node["name"], "compute_grand_total"); @@ -415,7 +415,7 @@ async fn test_rename_symbol_apply_rewrites_declaration_and_callers() { let project_root = dir.path().join("project"); let project = project_root.as_path(); rename_fixture(project).await; - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let node = preview_node(&cg, "compute_grand_total").await; let preview = preview_rename(&cg, &node, "calculate_total_cents").await; @@ -490,7 +490,7 @@ async fn test_rename_symbol_stale_tree_refuses_before_writing() { let project_root = dir.path().join("project"); let project = project_root.as_path(); rename_fixture(project).await; - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let node = preview_node(&cg, "compute_grand_total").await; let preview = preview_rename(&cg, &node, "calculate_total_cents").await; @@ -595,7 +595,7 @@ async fn test_rename_symbol_denies_invalid_and_colliding_names() { let project_root = dir.path().join("project"); let project = project_root.as_path(); rename_fixture(project).await; - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let node = preview_node(&cg, "compute_grand_total").await; @@ -676,7 +676,7 @@ async fn test_rename_symbol_blocks_unresolved_cross_module_spelling() { let project = project_root.as_path(); rename_fixture(project).await; fs::write(project.join("src/nested/orders.rs"), ORDERS_CROSS_MODULE).unwrap(); - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let node = preview_node(&cg, "compute_grand_total").await; let payload = call_json( @@ -787,7 +787,7 @@ async fn test_rename_symbol_publication_failure_preserves_preimage() { let project_root = dir.path().join("project"); let project = project_root.as_path(); rename_fixture(project).await; - let (cg, _env) = init_test_project(project).await; + let cg = init_test_project(project).await; let node = preview_node(&cg, "compute_grand_total").await; let preview = preview_rename(&cg, &node, "calculate_total_cents").await; diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/replace_symbol_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/replace_symbol_test.rs index 3dda74f4a1..2efb317167 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/replace_symbol_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/replace_symbol_test.rs @@ -153,7 +153,7 @@ async fn open_sources( fs::create_dir_all(path.parent().expect("source file has a parent")).unwrap(); fs::write(&path, contents).unwrap(); } - let (fixture, _) = init_test_project(&project_root).await; + let fixture = init_test_project(&project_root).await; (dir, project_root, fixture) } diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/source_edit_reconcile_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/source_edit_reconcile_test.rs index 5a422d5000..963bbdc529 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/source_edit_reconcile_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/source_edit_reconcile_test.rs @@ -46,7 +46,7 @@ async fn open_project() -> OpenedProject { let project = dir.path().join("project"); fs::create_dir_all(project.join("src/locked")).unwrap(); fs::write(project.join(RELATIVE_PATH), PREIMAGE).unwrap(); - let (fixture, ()) = init_production_source_edit_project(&project).await; + let fixture = init_production_source_edit_project(&project).await; OpenedProject { file: project.join(RELATIVE_PATH), fixture, diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/source_edit_rollback_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/source_edit_rollback_test.rs index 1753dfc23b..f370421b08 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/source_edit_rollback_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/source_edit_rollback_test.rs @@ -90,7 +90,7 @@ async fn open_moved_project() -> MovedProject { fs::write(project.join("src/lib.rs"), LIB_RS).unwrap(); fs::write(project.join("src/source.rs"), SOURCE_RS).unwrap(); fs::write(project.join("src/dest.rs"), DEST_RS).unwrap(); - let (fixture, _) = init_production_source_edit_project(&project).await; + let fixture = init_production_source_edit_project(&project).await; let server = fixture .harness .server(&fixture.project_root) @@ -423,7 +423,7 @@ async fn source_edit_rollback_refuses_an_edit_without_retained_preimages() { let project = dir.path().join("project"); fs::create_dir_all(project.join("src")).unwrap(); fs::write(project.join("src/main.rs"), "fn old_name() {}\n").unwrap(); - let (fixture, _) = init_production_source_edit_project(&project).await; + let fixture = init_production_source_edit_project(&project).await; let preview = tool_payload( &call_tool( diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/str_replace_behavior_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/str_replace_behavior_test.rs index 2746223fc8..b4de374f59 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/str_replace_behavior_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/str_replace_behavior_test.rs @@ -34,7 +34,7 @@ async fn open_file( let file = project.join(relative); fs::create_dir_all(file.parent().expect("fixture file has a parent")).unwrap(); fs::write(&file, bytes).unwrap(); - let (fixture, ()) = init_production_source_edit_project(&project).await; + let fixture = init_production_source_edit_project(&project).await; (fixture, dir, file) } diff --git a/crates/tracedecay/tests/mcp_suite/support.rs b/crates/tracedecay/tests/mcp_suite/support.rs index 31290d285e..26c0014d42 100644 --- a/crates/tracedecay/tests/mcp_suite/support.rs +++ b/crates/tracedecay/tests/mcp_suite/support.rs @@ -418,7 +418,7 @@ pub(crate) struct ProductionSourceEditFixture { #[cfg(feature = "test-transport")] pub(crate) async fn init_production_source_edit_project( project_root: &Path, -) -> (ProductionSourceEditFixture, ()) { +) -> ProductionSourceEditFixture { let isolation_root = project_root .parent() .expect("source-edit project has an isolation parent"); @@ -429,13 +429,10 @@ pub(crate) async fn init_production_source_edit_project( )) .await .expect("production source-edit composition"); - ( - ProductionSourceEditFixture { - harness, - project_root: project_root.to_path_buf(), - }, - (), - ) + ProductionSourceEditFixture { + harness, + project_root: project_root.to_path_buf(), + } } #[cfg(feature = "test-transport")] @@ -991,9 +988,6 @@ pub(crate) fn canonicalize_test_db_path(path: &Path) -> PathBuf { ) } -// --------------------------------------------------------------------------- -// Shared setup -// --------------------------------------------------------------------------- pub(crate) struct TestTempDir { pub(crate) dir: Option, } From c082618edcb5a83b706de094a512cdec51a70a7c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:49:05 +0000 Subject: [PATCH 069/182] simplify(pass-5/5): deslop suite section banners The dashed headers only restated the next test or helper group. Co-authored-by: Zack Jackson --- .../mcp_handler_test/context_test.rs | 4 - .../mcp_server_test/protocol_test.rs | 88 ------------------- .../mcp_suite/mcp_server_test/support.rs | 4 - 3 files changed, 96 deletions(-) diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/context_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/context_test.rs index 48f020e44b..7f71146621 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/context_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/context_test.rs @@ -153,10 +153,6 @@ async fn test_context_appends_index_coverage_hint_for_skipped_generated_dirs() { fixture.harness.shutdown().await; } -// --------------------------------------------------------------------------- -// 2. tracedecay_context -// --------------------------------------------------------------------------- - #[tokio::test] async fn context_includes_matching_memory_facts() { let fixture = setup_production_project().await; diff --git a/crates/tracedecay/tests/mcp_suite/mcp_server_test/protocol_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_server_test/protocol_test.rs index 6ee06bf634..50ca37537c 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_server_test/protocol_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_server_test/protocol_test.rs @@ -13,10 +13,6 @@ use tracedecay_mcp::response_handles::{ }; use tracedecay_runtime_core::storage::resolve_response_handle_root; -// --------------------------------------------------------------------------- -// 1. test_initialize -// --------------------------------------------------------------------------- - #[tokio::test] async fn test_initialize() { let (server, _dir) = setup_server().await; @@ -49,10 +45,6 @@ async fn initialize_root_route_rejects_caller_project_path_spoof() { assert_legacy_selectors_cannot_spoof_initialize_root().await; } -// --------------------------------------------------------------------------- -// 2. notifications -// --------------------------------------------------------------------------- - #[tokio::test] async fn test_any_notification_without_id_produces_no_response() { let (server, _dir) = setup_server().await; @@ -123,10 +115,6 @@ async fn test_tools_call_explicit_null_id_is_still_a_request() { ); } -// --------------------------------------------------------------------------- -// 5. test_tools_list -// --------------------------------------------------------------------------- - #[tokio::test] async fn test_tools_list() { let (server, _dir) = setup_server().await; @@ -156,10 +144,6 @@ async fn test_tools_list() { ); } -// --------------------------------------------------------------------------- -// 6. test_tools_call_search -// --------------------------------------------------------------------------- - #[cfg(feature = "test-transport")] #[tokio::test] async fn test_tools_call_search() { @@ -283,10 +267,6 @@ async fn test_tools_call_plain_text_failure_sets_is_error() { ); } -// --------------------------------------------------------------------------- -// 6b. test_tools_call_timings_flag -// --------------------------------------------------------------------------- - #[tokio::test] async fn test_tools_call_timings_enabled_by_default() { let (server, _dir) = setup_server().await; @@ -628,10 +608,6 @@ async fn cancellable_tool_call_fails_connection_on_peer_write_failure() { fixture.harness.shutdown().await; } -// --------------------------------------------------------------------------- -// 7. test_tools_call_status -// --------------------------------------------------------------------------- - #[tokio::test] async fn test_tools_call_status() { let (server, _dir) = setup_server().await; @@ -672,10 +648,6 @@ async fn test_tools_call_status() { ); } -// --------------------------------------------------------------------------- -// 8. test_tools_call_missing_params -// --------------------------------------------------------------------------- - #[tokio::test] async fn test_tools_call_missing_params() { let (server, _dir) = setup_server().await; @@ -710,10 +682,6 @@ async fn test_tools_call_missing_params() { ); } -// --------------------------------------------------------------------------- -// 9. test_tools_call_missing_name -// --------------------------------------------------------------------------- - #[tokio::test] async fn test_tools_call_missing_name() { let (server, _dir) = setup_server().await; @@ -884,10 +852,6 @@ async fn test_tracedecay_retrieve_handle_read_failure_returns_actionable_interna ); } -// --------------------------------------------------------------------------- -// 10. test_unknown_method -// --------------------------------------------------------------------------- - #[tokio::test] async fn test_unknown_method() { let (server, _dir) = setup_server().await; @@ -907,10 +871,6 @@ async fn test_unknown_method() { ); } -// --------------------------------------------------------------------------- -// 11. test_malformed_json -// --------------------------------------------------------------------------- - #[tokio::test] async fn test_malformed_json() { let (server, _dir) = setup_server().await; @@ -954,10 +914,6 @@ async fn test_malformed_json() { ); } -// --------------------------------------------------------------------------- -// 11b. test_foreign_protocol_version_is_rejected_before_dispatch -// --------------------------------------------------------------------------- - /// The envelope rule is enforced once at the transport boundary: a frame /// whose `jsonrpc` is not exactly `"2.0"` is answered with `InvalidRequest` /// carrying its id and never reaches the handler, so it cannot execute a tool @@ -1012,10 +968,6 @@ async fn test_foreign_protocol_version_is_rejected_before_dispatch() { ); } -// --------------------------------------------------------------------------- -// 12. test_blank_lines_skipped -// --------------------------------------------------------------------------- - #[tokio::test] async fn test_blank_lines_skipped() { let (server, _dir) = setup_server().await; @@ -1306,10 +1258,6 @@ async fn test_server_stats_after_run() { assert_eq!(stats["ratios"]["tool_calls_per_jsonrpc_message"], 0.25); } -// --------------------------------------------------------------------------- -// 16. test_error_tracking -// --------------------------------------------------------------------------- - #[tokio::test] async fn test_error_tracking() { let (server, _dir) = setup_server().await; @@ -1365,10 +1313,6 @@ async fn test_error_tracking() { ); } -// --------------------------------------------------------------------------- -// 17. test_initialize_has_resources_capability -// --------------------------------------------------------------------------- - #[tokio::test] async fn test_initialize_has_resources_capability() { let (server, _dir) = setup_server().await; @@ -1385,10 +1329,6 @@ async fn test_initialize_has_resources_capability() { ); } -// --------------------------------------------------------------------------- -// 19. test_resources_list -// --------------------------------------------------------------------------- - #[tokio::test] async fn test_resources_list() { let (server, _dir) = setup_server().await; @@ -1441,10 +1381,6 @@ async fn test_resources_list() { } } -// --------------------------------------------------------------------------- -// 20. test_resources_read_status -// --------------------------------------------------------------------------- - #[tokio::test] async fn test_resources_read_status() { let (server, _dir) = setup_server().await; @@ -1486,10 +1422,6 @@ async fn test_resources_read_status() { ); } -// --------------------------------------------------------------------------- -// 21. test_resources_read_files -// --------------------------------------------------------------------------- - #[tokio::test] async fn test_resources_read_files() { let (server, _dir) = setup_server().await; @@ -1529,10 +1461,6 @@ async fn test_resources_read_files() { ); } -// --------------------------------------------------------------------------- -// 22. test_resources_read_overview -// --------------------------------------------------------------------------- - #[tokio::test] async fn test_resources_read_overview() { let (server, _dir) = setup_server().await; @@ -1578,10 +1506,6 @@ async fn test_resources_read_overview() { ); } -// --------------------------------------------------------------------------- -// 23. test_resources_read_unknown_uri -// --------------------------------------------------------------------------- - #[tokio::test] async fn test_resources_read_unknown_uri() { let (server, _dir) = setup_server().await; @@ -1612,10 +1536,6 @@ async fn test_resources_read_unknown_uri() { ); } -// --------------------------------------------------------------------------- -// 24. test_resources_read_missing_uri -// --------------------------------------------------------------------------- - #[tokio::test] async fn test_resources_read_missing_uri() { let (server, _dir) = setup_server().await; @@ -1640,10 +1560,6 @@ async fn test_resources_read_missing_uri() { ); } -// --------------------------------------------------------------------------- -// Regression: logging/setLevel must be handled (not return MethodNotFound) -// --------------------------------------------------------------------------- - /// The MCP client sends `logging/setLevel` immediately after initialisation /// whenever the server advertises the `logging` capability. Before the fix the /// server returned -32601 (MethodNotFound), which Claude Code logged as an @@ -1758,10 +1674,6 @@ async fn test_run_returns_transport_read_errors() { ); } -// --------------------------------------------------------------------------- -// search_call_writes_savings_ledger_row -// --------------------------------------------------------------------------- - // Repeated serve-mode LCM calls must keep working while the project session // DB schema is ensured at most once per process: after the first write-path // call creates the store and runs the migrations, later write-path calls diff --git a/crates/tracedecay/tests/mcp_suite/mcp_server_test/support.rs b/crates/tracedecay/tests/mcp_suite/mcp_server_test/support.rs index e947ec3bd0..d78b8925d2 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_server_test/support.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_server_test/support.rs @@ -13,10 +13,6 @@ use tracedecay::test_support::host_admission::HostAdmissionTestRuntimeV1; use tracedecay_mcp::transport::{ChannelTransport, McpTransport}; use tracedecay_runtime_core::storage::resolve_response_handle_root; -// --------------------------------------------------------------------------- -// Shared helpers -// --------------------------------------------------------------------------- - /// Creates a temporary Rust project and returns a direct protocol server. /// /// Graph journeys use [`crate::support::production_composition_fixture`] From d4c7241eb93cb166916b20b31df1e9c7d6a87ba6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:49:31 +0000 Subject: [PATCH 070/182] simplify(pass-3/5): collapse graph fan-out wrappers Co-authored-by: Zack Jackson --- crates/tracedecay-graph-db/src/runtime.rs | 339 +++++++++----------- crates/tracedecay-graph-db/src/traversal.rs | 238 +------------- 2 files changed, 154 insertions(+), 423 deletions(-) diff --git a/crates/tracedecay-graph-db/src/runtime.rs b/crates/tracedecay-graph-db/src/runtime.rs index 7ad3f099c0..c30858627f 100644 --- a/crates/tracedecay-graph-db/src/runtime.rs +++ b/crates/tracedecay-graph-db/src/runtime.rs @@ -503,33 +503,51 @@ impl GraphDb { Ok(result) } - #[hotpath::measure(label = "graph_db.traversal.outgoing_ids", impl_type = "GraphDb")] - pub fn outgoing_relation_ids( + #[allow(clippy::too_many_arguments)] + fn fanout_relation_rows( &self, namespace: &GraphNamespace, starts: &[GraphEntityId], relation_kinds: &BTreeSet, max_relations: usize, cancellation: Arc, - ) -> Result>, GraphDbError> { + direction: grafeo_core::graph::Direction, + overflow: traversal::RelationFanoutOverflow, + ) -> Result>, GraphDbError> { + self.fanout_batches(namespace, starts, |database, approve, _, _| { + traversal::directed_relations( + database, + namespace, + starts, + relation_kinds, + max_relations, + direction, + cancellation.as_ref(), + approve, + overflow, + ) + }) + } + + fn fanout_batches( + &self, + namespace: &GraphNamespace, + starts: &[GraphEntityId], + read: impl FnOnce( + &GrafeoDB, + &dyn Fn(&GraphNamespace, &GraphProjectionId) -> Result<(), GraphDbError>, + &crate::epoch_cache::LabelKeyCache, + &crate::adjacency_id_index::AdjacencyIdIndexCache, + ) -> Result>, GraphDbError>, + ) -> Result>, GraphDbError> { let guard = self.read_guard()?; let database = guard.as_ref().ok_or(GraphDbError::Closed)?; self.ensure_start_projections_readable(database, namespace, starts)?; - let approve_projection = |namespace: &GraphNamespace, projection: &GraphProjectionId| { - self.approve_projection(namespace, projection) - }; - let batches = traversal::outgoing_relation_ids( - traversal::RelationIdReadContext::new( - database, - &approve_projection, - &self.inner.label_keys, - &self.inner.adjacency_ids, - ), - namespace, - starts, - relation_kinds, - max_relations, - cancellation.as_ref(), + let batches = read( + database, + &|namespace, projection| self.approve_projection(namespace, projection), + &self.inner.label_keys, + &self.inner.adjacency_ids, )?; #[cfg(feature = "hotpath")] { @@ -542,6 +560,37 @@ impl GraphDb { Ok(batches) } + #[hotpath::measure(label = "graph_db.traversal.outgoing_ids", impl_type = "GraphDb")] + pub fn outgoing_relation_ids( + &self, + namespace: &GraphNamespace, + starts: &[GraphEntityId], + relation_kinds: &BTreeSet, + max_relations: usize, + cancellation: Arc, + ) -> Result>, GraphDbError> { + self.fanout_batches( + namespace, + starts, + |database, approve, label_keys, adjacency_ids| { + traversal::directed_relation_ids( + database, + namespace, + starts, + relation_kinds, + max_relations, + false, + cancellation.as_ref(), + approve, + label_keys, + adjacency_ids, + traversal::RelationFanoutOverflow::Refuse, + None, + ) + }, + ) + } + /// Bulk kind-filtered incoming fan-out: the counterpart of /// [`Self::outgoing_relation_ids`], with identical budget and /// cancellation semantics. @@ -554,34 +603,26 @@ impl GraphDb { max_relations: usize, cancellation: Arc, ) -> Result>, GraphDbError> { - let guard = self.read_guard()?; - let database = guard.as_ref().ok_or(GraphDbError::Closed)?; - self.ensure_start_projections_readable(database, namespace, starts)?; - let approve_projection = |namespace: &GraphNamespace, projection: &GraphProjectionId| { - self.approve_projection(namespace, projection) - }; - let batches = traversal::incoming_relation_ids( - traversal::RelationIdReadContext::new( - database, - &approve_projection, - &self.inner.label_keys, - &self.inner.adjacency_ids, - ), + self.fanout_batches( namespace, starts, - relation_kinds, - max_relations, - cancellation.as_ref(), - )?; - #[cfg(feature = "hotpath")] - { - let edges = batches.iter().map(Vec::len).sum(); - crate::hotpath_observe::record_counts(starts.len(), edges, 0, 0); - crate::hotpath_observe::record_hydration_source( - crate::hotpath_observe::HydrationSource::Live, - ); - } - Ok(batches) + |database, approve, label_keys, adjacency_ids| { + traversal::directed_relation_ids( + database, + namespace, + starts, + relation_kinds, + max_relations, + true, + cancellation.as_ref(), + approve, + label_keys, + adjacency_ids, + traversal::RelationFanoutOverflow::Refuse, + None, + ) + }, + ) } /// Cursor-exclusive ID page over outgoing adjacency. @@ -598,35 +639,26 @@ impl GraphDb { limit: usize, cancellation: Arc, ) -> Result>, GraphDbError> { - let guard = self.read_guard()?; - let database = guard.as_ref().ok_or(GraphDbError::Closed)?; - self.ensure_start_projections_readable(database, namespace, starts)?; - let approve_projection = |namespace: &GraphNamespace, projection: &GraphProjectionId| { - self.approve_projection(namespace, projection) - }; - let batches = traversal::outgoing_relation_ids_page( - traversal::RelationIdReadContext::new( - database, - &approve_projection, - &self.inner.label_keys, - &self.inner.adjacency_ids, - ), + self.fanout_batches( namespace, starts, - relation_kinds, - after, - limit, - cancellation.as_ref(), - )?; - #[cfg(feature = "hotpath")] - { - let edges = batches.iter().map(Vec::len).sum(); - crate::hotpath_observe::record_counts(starts.len(), edges, 0, 0); - crate::hotpath_observe::record_hydration_source( - crate::hotpath_observe::HydrationSource::Live, - ); - } - Ok(batches) + |database, approve, label_keys, adjacency_ids| { + traversal::directed_relation_ids( + database, + namespace, + starts, + relation_kinds, + limit, + false, + cancellation.as_ref(), + approve, + label_keys, + adjacency_ids, + traversal::RelationFanoutOverflow::Truncate, + after, + ) + }, + ) } /// Cursor-exclusive ID page over incoming adjacency. See @@ -641,35 +673,26 @@ impl GraphDb { limit: usize, cancellation: Arc, ) -> Result>, GraphDbError> { - let guard = self.read_guard()?; - let database = guard.as_ref().ok_or(GraphDbError::Closed)?; - self.ensure_start_projections_readable(database, namespace, starts)?; - let approve_projection = |namespace: &GraphNamespace, projection: &GraphProjectionId| { - self.approve_projection(namespace, projection) - }; - let batches = traversal::incoming_relation_ids_page( - traversal::RelationIdReadContext::new( - database, - &approve_projection, - &self.inner.label_keys, - &self.inner.adjacency_ids, - ), + self.fanout_batches( namespace, starts, - relation_kinds, - after, - limit, - cancellation.as_ref(), - )?; - #[cfg(feature = "hotpath")] - { - let edges = batches.iter().map(Vec::len).sum(); - crate::hotpath_observe::record_counts(starts.len(), edges, 0, 0); - crate::hotpath_observe::record_hydration_source( - crate::hotpath_observe::HydrationSource::Live, - ); - } - Ok(batches) + |database, approve, label_keys, adjacency_ids| { + traversal::directed_relation_ids( + database, + namespace, + starts, + relation_kinds, + limit, + true, + cancellation.as_ref(), + approve, + label_keys, + adjacency_ids, + traversal::RelationFanoutOverflow::Truncate, + after, + ) + }, + ) } #[hotpath::measure(label = "graph_db.traversal.outgoing", impl_type = "GraphDb")] @@ -681,27 +704,15 @@ impl GraphDb { max_relations: usize, cancellation: Arc, ) -> Result>, GraphDbError> { - let guard = self.read_guard()?; - let database = guard.as_ref().ok_or(GraphDbError::Closed)?; - self.ensure_start_projections_readable(database, namespace, starts)?; - let batches = traversal::outgoing_relations( - database, + self.fanout_relation_rows( namespace, starts, relation_kinds, max_relations, - cancellation.as_ref(), - &|namespace, projection| self.approve_projection(namespace, projection), - )?; - #[cfg(feature = "hotpath")] - { - let edges = batches.iter().map(Vec::len).sum(); - crate::hotpath_observe::record_counts(starts.len(), edges, 0, 0); - crate::hotpath_observe::record_hydration_source( - crate::hotpath_observe::HydrationSource::Live, - ); - } - Ok(batches) + cancellation, + grafeo_core::graph::Direction::Outgoing, + traversal::RelationFanoutOverflow::Refuse, + ) } /// Same shape as [`Self::outgoing_relations`], but stops at `max_relations` @@ -715,27 +726,15 @@ impl GraphDb { max_relations: usize, cancellation: Arc, ) -> Result>, GraphDbError> { - let guard = self.read_guard()?; - let database = guard.as_ref().ok_or(GraphDbError::Closed)?; - self.ensure_start_projections_readable(database, namespace, starts)?; - let batches = traversal::outgoing_relations_truncated( - database, + self.fanout_relation_rows( namespace, starts, relation_kinds, max_relations, - cancellation.as_ref(), - &|namespace, projection| self.approve_projection(namespace, projection), - )?; - #[cfg(feature = "hotpath")] - { - let edges = batches.iter().map(Vec::len).sum(); - crate::hotpath_observe::record_counts(starts.len(), edges, 0, 0); - crate::hotpath_observe::record_hydration_source( - crate::hotpath_observe::HydrationSource::Live, - ); - } - Ok(batches) + cancellation, + grafeo_core::graph::Direction::Outgoing, + traversal::RelationFanoutOverflow::Truncate, + ) } #[hotpath::measure(label = "graph_db.traversal.outgoing_targets", impl_type = "GraphDb")] @@ -747,27 +746,17 @@ impl GraphDb { max_relations: usize, cancellation: Arc, ) -> Result>, GraphDbError> { - let guard = self.read_guard()?; - let database = guard.as_ref().ok_or(GraphDbError::Closed)?; - self.ensure_start_projections_readable(database, namespace, starts)?; - let batches = traversal::outgoing_relation_targets( - database, - namespace, - starts, - relation_kinds, - max_relations, - cancellation.as_ref(), - &|namespace, projection| self.approve_projection(namespace, projection), - )?; - #[cfg(feature = "hotpath")] - { - let edges = batches.iter().map(Vec::len).sum(); - crate::hotpath_observe::record_counts(starts.len(), edges, 0, 0); - crate::hotpath_observe::record_hydration_source( - crate::hotpath_observe::HydrationSource::Live, - ); - } - Ok(batches) + self.fanout_batches(namespace, starts, |database, approve, _, _| { + traversal::outgoing_relation_targets( + database, + namespace, + starts, + relation_kinds, + max_relations, + cancellation.as_ref(), + approve, + ) + }) } #[hotpath::measure( @@ -815,27 +804,15 @@ impl GraphDb { max_relations: usize, cancellation: Arc, ) -> Result>, GraphDbError> { - let guard = self.read_guard()?; - let database = guard.as_ref().ok_or(GraphDbError::Closed)?; - self.ensure_start_projections_readable(database, namespace, starts)?; - let batches = traversal::incoming_relations( - database, + self.fanout_relation_rows( namespace, starts, relation_kinds, max_relations, - cancellation.as_ref(), - &|namespace, projection| self.approve_projection(namespace, projection), - )?; - #[cfg(feature = "hotpath")] - { - let edges = batches.iter().map(Vec::len).sum(); - crate::hotpath_observe::record_counts(starts.len(), edges, 0, 0); - crate::hotpath_observe::record_hydration_source( - crate::hotpath_observe::HydrationSource::Live, - ); - } - Ok(batches) + cancellation, + grafeo_core::graph::Direction::Incoming, + traversal::RelationFanoutOverflow::Refuse, + ) } /// Same shape as [`Self::incoming_relations`], but stops at `max_relations` @@ -849,27 +826,15 @@ impl GraphDb { max_relations: usize, cancellation: Arc, ) -> Result>, GraphDbError> { - let guard = self.read_guard()?; - let database = guard.as_ref().ok_or(GraphDbError::Closed)?; - self.ensure_start_projections_readable(database, namespace, starts)?; - let batches = traversal::incoming_relations_truncated( - database, + self.fanout_relation_rows( namespace, starts, relation_kinds, max_relations, - cancellation.as_ref(), - &|namespace, projection| self.approve_projection(namespace, projection), - )?; - #[cfg(feature = "hotpath")] - { - let edges = batches.iter().map(Vec::len).sum(); - crate::hotpath_observe::record_counts(starts.len(), edges, 0, 0); - crate::hotpath_observe::record_hydration_source( - crate::hotpath_observe::HydrationSource::Live, - ); - } - Ok(batches) + cancellation, + grafeo_core::graph::Direction::Incoming, + traversal::RelationFanoutOverflow::Truncate, + ) } #[allow(clippy::too_many_arguments)] diff --git a/crates/tracedecay-graph-db/src/traversal.rs b/crates/tracedecay-graph-db/src/traversal.rs index a7c3075e3f..b18559edec 100644 --- a/crates/tracedecay-graph-db/src/traversal.rs +++ b/crates/tracedecay-graph-db/src/traversal.rs @@ -147,189 +147,6 @@ pub(crate) fn traverse( } } -pub(crate) struct RelationIdReadContext<'a> { - database: &'a GrafeoDB, - ensure_projection_readable: - &'a dyn Fn(&GraphNamespace, &GraphProjectionId) -> Result<(), GraphDbError>, - label_keys_cache: &'a LabelKeyCache, - adjacency_ids: &'a AdjacencyIdIndexCache, -} - -impl<'a> RelationIdReadContext<'a> { - pub(crate) fn new( - database: &'a GrafeoDB, - ensure_projection_readable: &'a dyn Fn( - &GraphNamespace, - &GraphProjectionId, - ) -> Result<(), GraphDbError>, - label_keys_cache: &'a LabelKeyCache, - adjacency_ids: &'a AdjacencyIdIndexCache, - ) -> Self { - Self { - database, - ensure_projection_readable, - label_keys_cache, - adjacency_ids, - } - } -} - -pub(crate) fn outgoing_relation_ids( - context: RelationIdReadContext<'_>, - namespace: &GraphNamespace, - starts: &[GraphEntityId], - relation_kinds: &BTreeSet, - max_relations: usize, - cancellation: &dyn GraphCancellation, -) -> Result>, GraphDbError> { - directed_relation_ids( - context.database, - namespace, - starts, - relation_kinds, - max_relations, - false, - cancellation, - context.ensure_projection_readable, - context.label_keys_cache, - context.adjacency_ids, - RelationFanoutOverflow::Refuse, - None, - ) -} - -pub(crate) fn outgoing_relation_ids_page( - context: RelationIdReadContext<'_>, - namespace: &GraphNamespace, - starts: &[GraphEntityId], - relation_kinds: &BTreeSet, - after: Option<&GraphRelationId>, - limit: usize, - cancellation: &dyn GraphCancellation, -) -> Result>, GraphDbError> { - directed_relation_ids( - context.database, - namespace, - starts, - relation_kinds, - limit, - false, - cancellation, - context.ensure_projection_readable, - context.label_keys_cache, - context.adjacency_ids, - RelationFanoutOverflow::Truncate, - after, - ) -} - -/// Bulk kind-filtered incoming fan-out: the exact counterpart of -/// [`outgoing_relation_ids`], carrying the same batch, cancellation, dedupe, -/// and `max_relations` budget semantics. -/// -/// Plan 39 G7b needs this so an interactive caller/impact read can resolve -/// reverse adjacency through the graph store instead of a SQL `edges` join. -/// Only the traversal direction differs from the outgoing form, so both -/// delegate to [`directed_relations`]. -pub(crate) fn incoming_relation_ids( - context: RelationIdReadContext<'_>, - namespace: &GraphNamespace, - starts: &[GraphEntityId], - relation_kinds: &BTreeSet, - max_relations: usize, - cancellation: &dyn GraphCancellation, -) -> Result>, GraphDbError> { - directed_relation_ids( - context.database, - namespace, - starts, - relation_kinds, - max_relations, - true, - cancellation, - context.ensure_projection_readable, - context.label_keys_cache, - context.adjacency_ids, - RelationFanoutOverflow::Refuse, - None, - ) -} - -pub(crate) fn incoming_relation_ids_page( - context: RelationIdReadContext<'_>, - namespace: &GraphNamespace, - starts: &[GraphEntityId], - relation_kinds: &BTreeSet, - after: Option<&GraphRelationId>, - limit: usize, - cancellation: &dyn GraphCancellation, -) -> Result>, GraphDbError> { - directed_relation_ids( - context.database, - namespace, - starts, - relation_kinds, - limit, - true, - cancellation, - context.ensure_projection_readable, - context.label_keys_cache, - context.adjacency_ids, - RelationFanoutOverflow::Truncate, - after, - ) -} - -pub(crate) fn outgoing_relations( - database: &GrafeoDB, - namespace: &GraphNamespace, - starts: &[GraphEntityId], - relation_kinds: &BTreeSet, - max_relations: usize, - cancellation: &dyn GraphCancellation, - ensure_projection_readable: &dyn Fn( - &GraphNamespace, - &GraphProjectionId, - ) -> Result<(), GraphDbError>, -) -> Result>, GraphDbError> { - directed_relations( - database, - namespace, - starts, - relation_kinds, - max_relations, - Direction::Outgoing, - cancellation, - ensure_projection_readable, - RelationFanoutOverflow::Refuse, - ) -} - -pub(crate) fn outgoing_relations_truncated( - database: &GrafeoDB, - namespace: &GraphNamespace, - starts: &[GraphEntityId], - relation_kinds: &BTreeSet, - max_relations: usize, - cancellation: &dyn GraphCancellation, - ensure_projection_readable: &dyn Fn( - &GraphNamespace, - &GraphProjectionId, - ) -> Result<(), GraphDbError>, -) -> Result>, GraphDbError> { - directed_relations( - database, - namespace, - starts, - relation_kinds, - max_relations, - Direction::Outgoing, - cancellation, - ensure_projection_readable, - RelationFanoutOverflow::Truncate, - ) -} - pub(crate) fn outgoing_relation_targets( database: &GrafeoDB, namespace: &GraphNamespace, @@ -442,57 +259,6 @@ pub(crate) fn visit_outgoing_relation_targets( Ok(visited) } -/// Bulk kind-filtered incoming fan-out. See [`incoming_relation_ids`]. -pub(crate) fn incoming_relations( - database: &GrafeoDB, - namespace: &GraphNamespace, - starts: &[GraphEntityId], - relation_kinds: &BTreeSet, - max_relations: usize, - cancellation: &dyn GraphCancellation, - ensure_projection_readable: &dyn Fn( - &GraphNamespace, - &GraphProjectionId, - ) -> Result<(), GraphDbError>, -) -> Result>, GraphDbError> { - directed_relations( - database, - namespace, - starts, - relation_kinds, - max_relations, - Direction::Incoming, - cancellation, - ensure_projection_readable, - RelationFanoutOverflow::Refuse, - ) -} - -pub(crate) fn incoming_relations_truncated( - database: &GrafeoDB, - namespace: &GraphNamespace, - starts: &[GraphEntityId], - relation_kinds: &BTreeSet, - max_relations: usize, - cancellation: &dyn GraphCancellation, - ensure_projection_readable: &dyn Fn( - &GraphNamespace, - &GraphProjectionId, - ) -> Result<(), GraphDbError>, -) -> Result>, GraphDbError> { - directed_relations( - database, - namespace, - starts, - relation_kinds, - max_relations, - Direction::Incoming, - cancellation, - ensure_projection_readable, - RelationFanoutOverflow::Truncate, - ) -} - /// How [`ordered_relation_ids`] admits a start's neighborhood. enum RelationIdIndexMode { /// Walk every incident edge, sort, and publish the epoch index. @@ -505,7 +271,7 @@ enum RelationIdIndexMode { } #[allow(clippy::too_many_arguments)] -fn directed_relation_ids( +pub(crate) fn directed_relation_ids( database: &GrafeoDB, namespace: &GraphNamespace, starts: &[GraphEntityId], @@ -708,7 +474,7 @@ fn relation_projection_cached( /// [`RelationFanoutOverflow::Truncate`] stops and returns the prefix. #[allow(clippy::too_many_arguments)] #[hotpath::measure(label = "graph_db.compact.directed_relations")] -fn directed_relations( +pub(crate) fn directed_relations( database: &GrafeoDB, namespace: &GraphNamespace, starts: &[GraphEntityId], From c524b5ede300adc45eeb625bbec92ac432712280 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:49:41 +0000 Subject: [PATCH 071/182] simplify(pass-5/5): collapse row decodes and flatten index match Co-authored-by: Zack Jackson --- .../src/schema_contract/validation.rs | 70 +++++++++---------- .../src/work_attempt.rs | 56 ++++++--------- 2 files changed, 56 insertions(+), 70 deletions(-) diff --git a/crates/tracedecay-global-db/src/schema_contract/validation.rs b/crates/tracedecay-global-db/src/schema_contract/validation.rs index f3d8f0b4cf..a018ce1dad 100644 --- a/crates/tracedecay-global-db/src/schema_contract/validation.rs +++ b/crates/tracedecay-global-db/src/schema_contract/validation.rs @@ -215,43 +215,43 @@ fn index_matches(actual: &ActualIndex, expected: &Index) -> bool { || name.eq_ignore_ascii_case("idx_session_refresh_operations_one_running") || name.eq_ignore_ascii_case("idx_observations_valid_session_sequence") }); - expected + if expected .name - .is_none_or(|name| actual.name.eq_ignore_ascii_case(name)) - && actual.unique == expected.unique - && actual.origin.eq_ignore_ascii_case(expected.origin) - && actual.partial == expected_partial - && actual.columns.len() == expected.columns.len() - && actual - .columns + .is_some_and(|name| !actual.name.eq_ignore_ascii_case(name)) + || actual.unique != expected.unique + || !actual.origin.eq_ignore_ascii_case(expected.origin) + || actual.partial != expected_partial + || actual.columns.len() != expected.columns.len() + { + return false; + } + let descending = expected.name.and_then(|name| { + INDEX_DESCENDING_COLUMNS .iter() - .zip(expected.columns) - .all(|(actual, expected_column)| { - if expected_column.eq_ignore_ascii_case(INDEX_EXPRESSION_COLUMN) { - actual.cid == -2 && actual.name.is_none() - } else { - actual.cid >= 0 - && actual.descending - == expected - .name - .and_then(|name| { - INDEX_DESCENDING_COLUMNS - .iter() - .find(|(index, _)| index.eq_ignore_ascii_case(name)) - .map(|(_, columns)| *columns) - }) - .is_some_and(|columns| { - columns - .iter() - .any(|column| column.eq_ignore_ascii_case(expected_column)) - }) - && actual.collation.eq_ignore_ascii_case("BINARY") - && actual - .name - .as_deref() - .is_some_and(|name| name.eq_ignore_ascii_case(expected_column)) - } - }) + .find(|(index, _)| index.eq_ignore_ascii_case(name)) + .map(|(_, columns)| *columns) + }); + actual + .columns + .iter() + .zip(expected.columns) + .all(|(actual, expected_column)| { + if expected_column.eq_ignore_ascii_case(INDEX_EXPRESSION_COLUMN) { + return actual.cid == -2 && actual.name.is_none(); + } + actual.cid >= 0 + && actual.descending + == descending.is_some_and(|columns| { + columns + .iter() + .any(|column| column.eq_ignore_ascii_case(expected_column)) + }) + && actual.collation.eq_ignore_ascii_case("BINARY") + && actual + .name + .as_deref() + .is_some_and(|name| name.eq_ignore_ascii_case(expected_column)) + }) } fn primary_key_index_columns(contract: &Table) -> Option> { diff --git a/crates/tracedecay-rusqlite-runtime/src/work_attempt.rs b/crates/tracedecay-rusqlite-runtime/src/work_attempt.rs index d003e0933e..74e0d25773 100644 --- a/crates/tracedecay-rusqlite-runtime/src/work_attempt.rs +++ b/crates/tracedecay-rusqlite-runtime/src/work_attempt.rs @@ -60,8 +60,7 @@ pub(crate) fn insert_attempt_in_transaction( .map_err(|_| WorkAttemptStorageError::Unavailable)?; if let Some(existing) = load_attempt_payload(transaction, authority, attempt.identity())? { return if existing == payload { - let record: StoredWorkAttemptV1 = serde_json::from_str(&existing) - .map_err(|_| WorkAttemptStorageError::Unavailable)?; + let record = decode_stored(&existing)?; Ok(WorkAttemptInsertOutcome::Replayed(Box::new(record.attempt))) } else { Err(WorkAttemptStorageError::AttemptConflict) @@ -160,9 +159,7 @@ impl WorkAttemptStoragePort for WorkSqliteStorage { ) -> Result { let payload = load_attempt_payload(self.handle(), authority, identity)? .ok_or(WorkAttemptStorageError::NotFoundOrNotAuthorized)?; - serde_json::from_str::(&payload) - .map(|record| record.attempt) - .map_err(|_| WorkAttemptStorageError::Unavailable) + Ok(decode_stored(&payload)?.attempt) } fn load_admission_kind( @@ -172,8 +169,7 @@ impl WorkAttemptStoragePort for WorkSqliteStorage { ) -> Result { let payload = load_attempt_payload(self.handle(), authority, identity)? .ok_or(WorkAttemptStorageError::NotFoundOrNotAuthorized)?; - let record: StoredWorkAttemptV1 = - serde_json::from_str(&payload).map_err(|_| WorkAttemptStorageError::Unavailable)?; + let record = decode_stored(&payload)?; Ok(if record.synthesis.is_some() { WorkAttemptAdmissionKind::Synthesis } else { @@ -200,8 +196,7 @@ impl WorkAttemptStoragePort for WorkSqliteStorage { .map_err(|_| WorkAttemptStorageError::Unavailable)?; let existing = load_attempt_payload(&transaction, authority, next.identity())? .ok_or(WorkAttemptStorageError::NotFoundOrNotAuthorized)?; - let mut record: StoredWorkAttemptV1 = serde_json::from_str(&existing) - .map_err(|_| WorkAttemptStorageError::Unavailable)?; + let mut record = decode_stored(&existing)?; record.attempt = next.clone(); let payload = serde_json::to_string(&record).map_err(|_| WorkAttemptStorageError::Unavailable)?; @@ -280,16 +275,7 @@ impl WorkAttemptStoragePort for WorkSqliteStorage { ); let rows = registered_work_query(self.handle(), &sql, authority_params_owned(authority)) .map_err(|_| WorkAttemptStorageError::Unavailable)?; - rows.rows - .into_iter() - .map(|row| { - let payload = - exact_sql_text(&row.values, 0).ok_or(WorkAttemptStorageError::Unavailable)?; - serde_json::from_str::(payload) - .map(|record| record.attempt) - .map_err(|_| WorkAttemptStorageError::Unavailable) - }) - .collect() + rows.rows.into_iter().map(attempt_from_row).collect() } fn has_open_attempts_in_exact_scope( @@ -375,13 +361,7 @@ impl WorkAttemptStoragePort for WorkSqliteStorage { let attempts = rows .rows .into_iter() - .map(|row| { - let payload = exact_sql_text(&row.values, 0) - .ok_or(WorkAttemptStorageError::Unavailable)?; - serde_json::from_str::(payload) - .map(|record| record.attempt) - .map_err(|_| WorkAttemptStorageError::Unavailable) - }) + .map(attempt_from_row) .collect::, _>>()?; Ok(WorkAttemptListPageV1 { attempts, @@ -422,8 +402,7 @@ pub(crate) fn insert_synthesis_in_transaction( }) .map_err(|_| WorkAttemptStorageError::Unavailable)?; if let Some(existing) = load_attempt_payload(transaction, authority, attempt.identity())? { - let existing: StoredWorkAttemptV1 = - serde_json::from_str(&existing).map_err(|_| WorkAttemptStorageError::Unavailable)?; + let existing = decode_stored(&existing)?; return match existing.synthesis { Some(existing) if existing.request_digest == record.request_digest => Ok( WorkSynthesisInsertOutcome::Replayed(Box::new(existing.result)), @@ -472,8 +451,7 @@ impl WorkSynthesisAdmissionStoragePort for WorkSqliteStorage { ) -> Result { let payload = load_attempt_payload(self.handle(), authority, identity)? .ok_or(WorkAttemptStorageError::NotFoundOrNotAuthorized)?; - serde_json::from_str::(&payload) - .map_err(|_| WorkAttemptStorageError::Unavailable)? + decode_stored(&payload)? .synthesis .ok_or(WorkAttemptStorageError::AttemptConflict) } @@ -540,9 +518,7 @@ impl WorkAttemptEvidenceReadPort for WorkSqliteStorage { .map(|row| { let payload = exact_sql_text(&row.values, 0) .ok_or(WorkAttemptStorageError::Unavailable)?; - let attempt = serde_json::from_str::(payload) - .map_err(|_| WorkAttemptStorageError::Unavailable)? - .attempt; + let attempt = decode_stored(payload)?.attempt; let evidence = match exact_sql_text(&row.values, 1) { None => None, Some(evidence_payload) => Some( @@ -611,6 +587,17 @@ fn insert_attempt_row( Ok(()) } +fn decode_stored(payload: &str) -> Result { + serde_json::from_str(payload).map_err(|_| WorkAttemptStorageError::Unavailable) +} + +fn attempt_from_row( + row: crate::exact_sql::ExactSqlRow, +) -> Result { + let payload = exact_sql_text(&row.values, 0).ok_or(WorkAttemptStorageError::Unavailable)?; + Ok(decode_stored(payload)?.attempt) +} + fn load_attempt_payload( source: &impl RegisteredWorkQuery, authority: &WorkAuthority, @@ -668,8 +655,7 @@ fn require_first_run_admission( else { return Ok(()); }; - let first: StoredWorkAttemptV1 = - serde_json::from_str(payload).map_err(|_| WorkAttemptStorageError::Unavailable)?; + let first = decode_stored(payload)?; if first.attempt.execution().deadline() == attempt.execution().deadline() && first.attempt.execution().execution_snapshot().topology() == attempt.execution().execution_snapshot().topology() From 249d51d379d87e268234e7816f4914050e12747f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:49:59 +0000 Subject: [PATCH 072/182] simplify(pass-4/5): collapse ranking score plumbing Co-authored-by: Zack Jackson --- .../tracedecay-temporal-query/src/ranking.rs | 70 +++++++++---------- 1 file changed, 32 insertions(+), 38 deletions(-) diff --git a/crates/tracedecay-temporal-query/src/ranking.rs b/crates/tracedecay-temporal-query/src/ranking.rs index 1f398d209d..4ae765df44 100644 --- a/crates/tracedecay-temporal-query/src/ranking.rs +++ b/crates/tracedecay-temporal-query/src/ranking.rs @@ -280,18 +280,10 @@ fn prepare_candidates( stable_id: candidate.stable_id.clone(), }); } - if existing.logical_message.is_none() { - existing.logical_message = candidate.logical_message.as_deref(); - } - if existing.turn.is_none() { - existing.turn = candidate.turn.as_deref(); - } - if existing.session.is_none() { - existing.session = candidate.session.as_deref(); - } - if existing.evidence_role.is_none() { - existing.evidence_role = candidate.evidence_role.as_deref(); - } + fill_absent(&mut existing.logical_message, &candidate.logical_message); + fill_absent(&mut existing.turn, &candidate.turn); + fill_absent(&mut existing.session, &candidate.session); + fill_absent(&mut existing.evidence_role, &candidate.evidence_role); } None => { metadata_by_id.insert( @@ -370,6 +362,12 @@ fn prepare_candidates( Ok(prepared) } +fn fill_absent<'a>(slot: &mut Option<&'a str>, value: &'a Option) { + if slot.is_none() { + *slot = value.as_deref(); + } +} + fn merged_metadata_conflicts(existing: &MergedMetadata<'_>, candidate: &RankingCandidate) -> bool { existing.first.anchor_id != candidate.anchor_id || existing.first.knowledge_at_micros != candidate.knowledge_at_micros @@ -429,45 +427,41 @@ const fn rank_tier(channel: CandidateChannel) -> RankTier { } fn encode_score(tier: RankTier, within_tier: u64) -> u64 { - let capped = if within_tier < TIER_SPAN { - within_tier - } else { - TIER_SPAN - 1 - }; (tier as u64) .saturating_mul(TIER_SPAN) - .saturating_add(capped) + .saturating_add(within_tier.min(TIER_SPAN - 1)) } fn apply_diversity(ranked: Vec, limits: DiversityLimits) -> Vec { - let mut logical_messages = BTreeMap::new(); - let mut turns = BTreeMap::new(); - let mut sessions = BTreeMap::new(); - let mut sources = BTreeMap::new(); - let mut evidence_roles = BTreeMap::new(); + let mut counts = [(); 5].map(|_| BTreeMap::new()); ranked .into_iter() .filter(|candidate| { - if at_limit( - &logical_messages, + let keys = [ candidate.logical_message.as_deref(), + candidate.turn.as_deref(), + candidate.session.as_deref(), + candidate.source.as_deref(), + candidate.evidence_role.as_deref(), + ]; + let caps = [ limits.per_logical_message, - ) || at_limit(&turns, candidate.turn.as_deref(), limits.per_turn) - || at_limit(&sessions, candidate.session.as_deref(), limits.per_session) - || at_limit(&sources, candidate.source.as_deref(), limits.per_source) - || at_limit( - &evidence_roles, - candidate.evidence_role.as_deref(), - limits.per_evidence_role, - ) + limits.per_turn, + limits.per_session, + limits.per_source, + limits.per_evidence_role, + ]; + if keys + .iter() + .zip(caps) + .enumerate() + .any(|(index, (key, cap))| at_limit(&counts[index], *key, cap)) { return false; } - increment(&mut logical_messages, candidate.logical_message.as_deref()); - increment(&mut turns, candidate.turn.as_deref()); - increment(&mut sessions, candidate.session.as_deref()); - increment(&mut sources, candidate.source.as_deref()); - increment(&mut evidence_roles, candidate.evidence_role.as_deref()); + for (index, key) in keys.into_iter().enumerate() { + increment(&mut counts[index], key); + } true }) .collect() From 94d9612add34fef3b1a553887bbf5126eff77d7d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:51:06 +0000 Subject: [PATCH 073/182] simplify(pass-1/5): collapse observation window checks Co-authored-by: Zack Jackson --- .../src/contract/mod.rs | 70 ++++++------------- 1 file changed, 23 insertions(+), 47 deletions(-) diff --git a/crates/tracedecay-daemon-protocol/src/contract/mod.rs b/crates/tracedecay-daemon-protocol/src/contract/mod.rs index e62ffb3c75..c36fd9a229 100644 --- a/crates/tracedecay-daemon-protocol/src/contract/mod.rs +++ b/crates/tracedecay-daemon-protocol/src/contract/mod.rs @@ -76,6 +76,14 @@ fn valid_lsp_control(deadline: &Deadline, cancellation: &CancellationContext) -> deadline.expires_at.0 > 0 && cancellation.token_id.as_str().len() <= MAX_OPAQUE_HANDLE_BYTES } +fn valid_observation_window( + observed_at: &UtcMicros, + deadline: &Deadline, + cancellation: &CancellationContext, +) -> bool { + observed_at.0 > 0 && valid_lsp_control(deadline, cancellation) +} + /// Stable discriminator for the closed post-handshake invocation protocol. pub const DAEMON_INVOCATION_PROTOCOL: &str = "tracedecay.daemon.invocation"; /// Initial revision of the daemon-owned invocation wire shape. @@ -1823,9 +1831,7 @@ impl DaemonInvocationRequest { deadline, cancellation, } => { - if observed_at.0 <= 0 - || deadline.expires_at.0 <= 0 - || cancellation.token_id.as_str().len() > MAX_OPAQUE_HANDLE_BYTES + if !valid_observation_window(observed_at, deadline, cancellation) || MultiRootScopeSetReadRequestV1::new(request.scope_set_id.clone()).is_err() { return Err(DaemonInvocationProblem::InvalidRequest); @@ -1837,9 +1843,7 @@ impl DaemonInvocationRequest { deadline, cancellation, } => { - if observed_at.0 <= 0 - || deadline.expires_at.0 <= 0 - || cancellation.token_id.as_str().len() > MAX_OPAQUE_HANDLE_BYTES + if !valid_observation_window(observed_at, deadline, cancellation) || request.validate().is_err() { return Err(DaemonInvocationProblem::InvalidRequest); @@ -1851,9 +1855,7 @@ impl DaemonInvocationRequest { deadline, cancellation, } => { - if observed_at.0 <= 0 - || deadline.expires_at.0 <= 0 - || cancellation.token_id.as_str().len() > MAX_OPAQUE_HANDLE_BYTES + if !valid_observation_window(observed_at, deadline, cancellation) || request.validate().is_err() { return Err(DaemonInvocationProblem::InvalidRequest); @@ -1866,10 +1868,7 @@ impl DaemonInvocationRequest { deadline, cancellation, } => { - if observed_at.0 <= 0 - || deadline.expires_at.0 <= 0 - || cancellation.token_id.as_str().len() > MAX_OPAQUE_HANDLE_BYTES - { + if !valid_observation_window(observed_at, deadline, cancellation) { return Err(DaemonInvocationProblem::InvalidRequest); } let expected = match &request.request { @@ -1900,10 +1899,7 @@ impl DaemonInvocationRequest { deadline, cancellation, } => { - if observed_at.0 <= 0 - || deadline.expires_at.0 <= 0 - || cancellation.token_id.as_str().len() > MAX_OPAQUE_HANDLE_BYTES - { + if !valid_observation_window(observed_at, deadline, cancellation) { return Err(DaemonInvocationProblem::InvalidRequest); } let expected = match request { @@ -2027,10 +2023,7 @@ impl DaemonInvocationRequest { cancellation, .. } => { - if observed_at.0 <= 0 - || deadline.expires_at.0 <= 0 - || cancellation.token_id.as_str().len() > MAX_OPAQUE_HANDLE_BYTES - { + if !valid_observation_window(observed_at, deadline, cancellation) { return Err(DaemonInvocationProblem::InvalidRequest); } } @@ -2042,9 +2035,7 @@ impl DaemonInvocationRequest { .. } => { if !(1..=365).contains(&request.window_days) - || observed_at.0 <= 0 - || deadline.expires_at.0 <= 0 - || cancellation.token_id.as_str().len() > MAX_OPAQUE_HANDLE_BYTES + || !valid_observation_window(observed_at, deadline, cancellation) { return Err(DaemonInvocationProblem::InvalidRequest); } @@ -2055,10 +2046,7 @@ impl DaemonInvocationRequest { cancellation, .. } => { - if observed_at.0 <= 0 - || deadline.expires_at.0 <= 0 - || cancellation.token_id.as_str().len() > MAX_OPAQUE_HANDLE_BYTES - { + if !valid_observation_window(observed_at, deadline, cancellation) { return Err(DaemonInvocationProblem::InvalidRequest); } } @@ -2073,7 +2061,7 @@ impl DaemonInvocationRequest { if observed_at.0 <= 0 || deadline.expires_at.0 <= 0 || PageRequest::new(page.page_size, page.cursor.clone()).is_err() - || cancellation.token_id.as_str().len() > MAX_OPAQUE_HANDLE_BYTES + || !valid_lsp_control(deadline, cancellation) { return Err(DaemonInvocationProblem::InvalidRequest); } @@ -2111,7 +2099,7 @@ impl DaemonInvocationRequest { if observed_at.0 <= 0 || deadline.expires_at.0 <= 0 || PageRequest::new(page.page_size, page.cursor.clone()).is_err() - || cancellation.token_id.as_str().len() > MAX_OPAQUE_HANDLE_BYTES + || !valid_lsp_control(deadline, cancellation) { return Err(DaemonInvocationProblem::InvalidRequest); } @@ -2155,9 +2143,7 @@ impl DaemonInvocationRequest { cancellation, .. } => { - if observed_at.0 <= 0 - || deadline.expires_at.0 <= 0 - || cancellation.token_id.as_str().len() > MAX_OPAQUE_HANDLE_BYTES + if !valid_observation_window(observed_at, deadline, cancellation) || !request.matches(*surface_operation) || matches!( request, @@ -2175,10 +2161,7 @@ impl DaemonInvocationRequest { cancellation, .. } => { - if observed_at.0 <= 0 - || deadline.expires_at.0 <= 0 - || cancellation.token_id.as_str().len() > MAX_OPAQUE_HANDLE_BYTES - { + if !valid_observation_window(observed_at, deadline, cancellation) { return Err(DaemonInvocationProblem::InvalidRequest); } } @@ -2220,9 +2203,7 @@ impl DaemonInvocationRequest { cancellation, } => { if !valid_token(request_handle, MAX_OPAQUE_HANDLE_BYTES) - || observed_at.0 <= 0 - || deadline.expires_at.0 <= 0 - || cancellation.token_id.as_str().len() > MAX_OPAQUE_HANDLE_BYTES + || !valid_observation_window(observed_at, deadline, cancellation) { return Err(DaemonInvocationProblem::InvalidRequest); } @@ -2234,9 +2215,7 @@ impl DaemonInvocationRequest { cancellation, } => { if !valid_printable(document_uri, MAX_ROOT_HINT_BYTES) - || observed_at.0 <= 0 - || deadline.expires_at.0 <= 0 - || cancellation.token_id.as_str().len() > MAX_OPAQUE_HANDLE_BYTES + || !valid_observation_window(observed_at, deadline, cancellation) { return Err(DaemonInvocationProblem::InvalidRequest); } @@ -2246,10 +2225,7 @@ impl DaemonInvocationRequest { deadline, cancellation, } => { - if request.observed_at.0 <= 0 - || deadline.expires_at.0 <= 0 - || cancellation.token_id.as_str().len() > MAX_OPAQUE_HANDLE_BYTES - { + if !valid_observation_window(&request.observed_at, deadline, cancellation) { return Err(DaemonInvocationProblem::InvalidRequest); } } From d9567d220332ea04f5546d527674ce9ca60f921b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:51:41 +0000 Subject: [PATCH 074/182] simplify(pass-1/5): drop unused cross-crate dependencies Co-authored-by: Zack Jackson --- Cargo.lock | 5 ----- crates/tracedecay-agent-hosts/Cargo.toml | 1 - crates/tracedecay-application/Cargo.toml | 1 - crates/tracedecay-automation-runtime/Cargo.toml | 1 - crates/tracedecay-graph-query/Cargo.toml | 1 - crates/tracedecay-private-fs/Cargo.toml | 1 - 6 files changed, 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b6be9ab4b9..fae89bee67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5438,7 +5438,6 @@ dependencies = [ "tracedecay-domain", "tracedecay-framing", "tracedecay-global-db", - "tracedecay-graph-db", "tracedecay-hooks", "tracedecay-host-integration", "tracedecay-lcm", @@ -5487,7 +5486,6 @@ dependencies = [ "gix", "glob", "hex", - "hmac 0.13.0", "hotpath", "same-file", "schemars", @@ -5572,7 +5570,6 @@ dependencies = [ "tracedecay-runtime-core", "tracedecay-session-memory", "tracedecay-session-temporal-store", - "tracedecay-sessions", "tracedecay-store", "tracedecay-temporal-query", "tracedecay-tool-catalog", @@ -6071,7 +6068,6 @@ dependencies = [ "tracedecay-domain", "tracedecay-global-db", "tracedecay-graph-db", - "tracedecay-privacy", "tracedecay-runtime-core", "tracedecay-session-memory", "tracedecay-tool-catalog", @@ -6339,7 +6335,6 @@ dependencies = [ "hotpath", "libc", "tempfile", - "thiserror 2.0.19", "windows-sys 0.61.2", ] diff --git a/crates/tracedecay-agent-hosts/Cargo.toml b/crates/tracedecay-agent-hosts/Cargo.toml index ca7b493e5c..9e20a0b361 100644 --- a/crates/tracedecay-agent-hosts/Cargo.toml +++ b/crates/tracedecay-agent-hosts/Cargo.toml @@ -77,4 +77,3 @@ tempfile = "3" tokio = { version = "1", features = ["full", "test-util"] } tracedecay-code-index-runtime = { path = "../tracedecay-code-index-runtime", version = "0.1.0", default-features = false, features = ["test-helpers"] } tracedecay-global-db = { path = "../tracedecay-global-db", version = "0.1.0", features = ["test-helpers"] } -tracedecay-graph-db = { path = "../tracedecay-graph-db", version = "0.1.0", features = ["test-helpers"] } diff --git a/crates/tracedecay-application/Cargo.toml b/crates/tracedecay-application/Cargo.toml index 91e84bf916..d88683dbdb 100644 --- a/crates/tracedecay-application/Cargo.toml +++ b/crates/tracedecay-application/Cargo.toml @@ -33,7 +33,6 @@ cap-std = "4.0.2" getrandom = "0.2" gix = { version = "=0.86.0", default-features = false, features = ["revision", "blob-diff", "parallel", "sha1", "sha256", "status"] } glob = "0.3" -hmac = { version = "0.13.0", features = ["zeroize"] } hex = "0.4" hotpath.workspace = true same-file = "1" diff --git a/crates/tracedecay-automation-runtime/Cargo.toml b/crates/tracedecay-automation-runtime/Cargo.toml index 6241b1fe00..b709de4647 100644 --- a/crates/tracedecay-automation-runtime/Cargo.toml +++ b/crates/tracedecay-automation-runtime/Cargo.toml @@ -57,7 +57,6 @@ tracedecay-private-fs = { path = "../tracedecay-private-fs", version = "0.1.0" } tracedecay-runtime-core = { path = "../tracedecay-runtime-core", version = "0.1.0" } tracedecay-session-temporal-store = { path = "../tracedecay-session-temporal-store", version = "0.1.0" } tracedecay-lcm = { path = "../tracedecay-lcm", version = "0.1.0" } -tracedecay-sessions = { path = "../tracedecay-sessions", version = "0.1.0" } tracedecay-store = { path = "../tracedecay-store", version = "0.1.0" } tracedecay-temporal-query = { path = "../tracedecay-temporal-query", version = "0.1.0" } tracedecay-tool-catalog = { path = "../tracedecay-tool-catalog", version = "0.1.0" } diff --git a/crates/tracedecay-graph-query/Cargo.toml b/crates/tracedecay-graph-query/Cargo.toml index 5b08132b13..ac88f43c10 100644 --- a/crates/tracedecay-graph-query/Cargo.toml +++ b/crates/tracedecay-graph-query/Cargo.toml @@ -23,7 +23,6 @@ tracedecay-code-extraction = { path = "../tracedecay-code-extraction", version = tracedecay-code-index = { path = "../tracedecay-code-index", version = "0.1.0", default-features = false } tracedecay-domain = { path = "../tracedecay-domain", version = "0.1.0" } tracedecay-graph-db = { path = "../tracedecay-graph-db", version = "0.1.0" } -tracedecay-privacy = { path = "../tracedecay-privacy", version = "0.1.0" } tracedecay-runtime-core = { path = "../tracedecay-runtime-core", version = "0.1.0" } tracedecay-session-memory = { path = "../tracedecay-session-memory", version = "0.1.0" } diff --git a/crates/tracedecay-private-fs/Cargo.toml b/crates/tracedecay-private-fs/Cargo.toml index 95ef1b3a53..79914a3f1c 100644 --- a/crates/tracedecay-private-fs/Cargo.toml +++ b/crates/tracedecay-private-fs/Cargo.toml @@ -11,7 +11,6 @@ repository = "https://github.com/ScriptedAlchemy/tracedecay" cap-fs-ext = "4.0.2" cap-std = "4.0.2" hotpath.workspace = true -thiserror = "2" [dev-dependencies] tempfile = "3" From e0f0e7a287f91afa59863aaac9f1b4e23119d20d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:51:41 +0000 Subject: [PATCH 075/182] simplify(pass-2/5): delete unused host and automation entries Co-authored-by: Zack Jackson --- .../src/agents/context_scout/owner.rs | 30 ----------------- .../src/agents/context_scout/ports.rs | 22 ------------- .../src/agents/prompt_rules.rs | 6 ---- .../tracedecay-agent-hosts/src/tool_name.rs | 33 ++----------------- .../src/automation/outcomes.rs | 10 ------ .../src/automation/run_ledger.rs | 7 ---- .../src/automation/runner.rs | 21 ------------ .../src/automation/skill_usage.rs | 16 --------- crates/tracedecay-automation/src/analytics.rs | 3 +- 9 files changed, 5 insertions(+), 143 deletions(-) diff --git a/crates/tracedecay-agent-hosts/src/agents/context_scout/owner.rs b/crates/tracedecay-agent-hosts/src/agents/context_scout/owner.rs index 5efdff117c..92a12c933f 100644 --- a/crates/tracedecay-agent-hosts/src/agents/context_scout/owner.rs +++ b/crates/tracedecay-agent-hosts/src/agents/context_scout/owner.rs @@ -136,18 +136,6 @@ pub fn unregister_registered_context_scout_owner( } impl ProjectContextScoutOwnerV1 { - pub async fn startup_configured( - database: Database, - project_id: [u8; 16], - now: UtcMicros, - pin: ContextScoutConfigurationPinV1, - model_config: Option<&AutomationConfig>, - ) -> Option> { - let owner = Self::startup(database, project_id, now, model_config).await?; - owner.install_configuration(pin, model_config).await.ok()?; - Some(owner) - } - #[hotpath::measure( future = true, label = "hosts.agent.context_scout.startup", @@ -853,24 +841,6 @@ impl ProjectContextScoutOwnerV1 { Ok(status_with_recent(status, &recent)) } - pub async fn configure_model(&self, config: &AutomationConfig) { - let configuration = self.configuration.read().await; - let Some(control) = configuration - .as_ref() - .map(ContextScoutConfigurationPinV1::control) - else { - return; - }; - let model = context_scout_model_assistant_from_project_config(Some(config)); - if control - .model_path - .is_some_and(|expected| expected != model.backend()) - { - return; - } - self.runtime.lock().await.replace_model(model); - } - pub async fn claim( &self, address: ContextScoutAddressV1, diff --git a/crates/tracedecay-agent-hosts/src/agents/context_scout/ports.rs b/crates/tracedecay-agent-hosts/src/agents/context_scout/ports.rs index 0bc81ca056..f958121ee4 100644 --- a/crates/tracedecay-agent-hosts/src/agents/context_scout/ports.rs +++ b/crates/tracedecay-agent-hosts/src/agents/context_scout/ports.rs @@ -816,28 +816,6 @@ where self.assemble(address, pin, context, observed_at).await } - #[hotpath::measure( - label = "context_scout_assemble_registered_exact", - impl_type = "ContextScoutCanonicalInputAssemblerV1" - )] - pub async fn assemble_registered_exact( - &self, - hook: &AdmittedContextScoutHookV1, - pin: &ContextScoutAuthorityPinV1, - lifecycle: &ContextScoutLifecycleAddressV1, - context: &RequestContext, - observed_at: UtcMicros, - ) -> Option { - let ContextScoutAddressResolveOutcomeV1::Resolved(address) = self - .registry - .resolve_current_exact(hook, pin, lifecycle, context, observed_at) - .await - else { - return None; - }; - self.assemble(address, pin, context, observed_at).await - } - #[hotpath::measure( label = "context_scout_bind_and_assemble", impl_type = "ContextScoutCanonicalInputAssemblerV1" diff --git a/crates/tracedecay-agent-hosts/src/agents/prompt_rules.rs b/crates/tracedecay-agent-hosts/src/agents/prompt_rules.rs index 1f2561ace6..6a77f192e9 100644 --- a/crates/tracedecay-agent-hosts/src/agents/prompt_rules.rs +++ b/crates/tracedecay-agent-hosts/src/agents/prompt_rules.rs @@ -201,12 +201,6 @@ pub(crate) fn standard_prompt_rules(marker: &str, options: &PromptRulesOptions) block } -/// The CLI-fallback paragraph every host's rules must carry; exposed so -/// integration tests can assert parity across hosts. -pub fn cli_fallback_paragraph() -> &'static str { - super::CLI_FALLBACK_PROMPT_RULES -} - /// End offset of a managed block whose marker heading ends at `search_from`: /// the next `\n## ` heading, the managed-skill index start marker, or EOF, /// whichever comes first. diff --git a/crates/tracedecay-agent-hosts/src/tool_name.rs b/crates/tracedecay-agent-hosts/src/tool_name.rs index eb12bdb4a0..245fbb461b 100644 --- a/crates/tracedecay-agent-hosts/src/tool_name.rs +++ b/crates/tracedecay-agent-hosts/src/tool_name.rs @@ -1,10 +1,9 @@ //! Canonical MCP namespaces for tracedecay's own tools. //! //! Hosts expose the same tool under several namespaces depending on how -//! tracedecay was installed, so the prefixes live here once and every consumer -//! (permission allowlists in `agents::claude`, usage classification in -//! `tracedecay_automation::analytics`) reads them from this module instead of -//! restating a partial list. +//! tracedecay was installed. Permission allowlists read these prefixes. +//! Usage classification restates the same host namespaces as literals in +//! `tracedecay_automation::analytics` so that leaf does not depend on this crate. /// Permission/tool prefix for the tracedecay tools exposed through the Claude /// **plugin** MCP server. Claude namespaces a plugin server's tools as @@ -15,33 +14,7 @@ /// instead of the redundant `plugin tracedecay tracedecay`. pub const PLUGIN_TOOL_PREFIX: &str = "mcp__plugin_tracedecay_graph__"; -/// Prior plugin namespace, from when the plugin MCP server key was also -/// `tracedecay` (`plugin_tracedecay_tracedecay`). Kept so pre-rename installs -/// are still recognized; entries under it are never removed. -pub const PRIOR_PLUGIN_TOOL_PREFIX: &str = "mcp__plugin_tracedecay_tracedecay__"; - /// Legacy config-managed namespace. It does NOT match the plugin namespace, so /// an install that wrote only these entries prompted interactively on every /// plugin tool call; the installer now writes the plugin-namespace twins too. pub const LEGACY_TOOL_PREFIX: &str = "mcp__tracedecay__"; - -/// Single-underscore namespace used by hosts that flatten the MCP separator. -pub const FLAT_TOOL_PREFIX: &str = "mcp_tracedecay_"; - -/// Every namespace a tracedecay tool call can arrive under, longest first so a -/// prefix that contains another is stripped whole. -pub const ALL_TOOL_PREFIXES: [&str; 4] = [ - PRIOR_PLUGIN_TOOL_PREFIX, - PLUGIN_TOOL_PREFIX, - LEGACY_TOOL_PREFIX, - FLAT_TOOL_PREFIX, -]; - -/// Strips the host MCP namespace from a tracedecay tool name, leaving the bare -/// tool name. Names in no known namespace are returned unchanged. -pub fn strip_tool_prefix(name: &str) -> &str { - ALL_TOOL_PREFIXES - .iter() - .find_map(|prefix| name.strip_prefix(prefix)) - .unwrap_or(name) -} diff --git a/crates/tracedecay-automation-runtime/src/automation/outcomes.rs b/crates/tracedecay-automation-runtime/src/automation/outcomes.rs index 70c7808a84..bc53223803 100644 --- a/crates/tracedecay-automation-runtime/src/automation/outcomes.rs +++ b/crates/tracedecay-automation-runtime/src/automation/outcomes.rs @@ -296,16 +296,6 @@ pub async fn load_outcomes_snapshot(dashboard_root: &Path) -> Result Result<()> { - let lock = outcomes_snapshot_lock(dashboard_root); - let _guard = lock.lock().await; - save_outcomes_snapshot_unlocked(dashboard_root, snapshot).await -} - async fn save_outcomes_snapshot_unlocked( dashboard_root: &Path, snapshot: &AutomationOutcomesSnapshot, diff --git a/crates/tracedecay-automation-runtime/src/automation/run_ledger.rs b/crates/tracedecay-automation-runtime/src/automation/run_ledger.rs index 12ed92251c..c6897c6578 100644 --- a/crates/tracedecay-automation-runtime/src/automation/run_ledger.rs +++ b/crates/tracedecay-automation-runtime/src/automation/run_ledger.rs @@ -853,13 +853,6 @@ impl AutomationRunLedgerTaskSummary { self.latest_session_evidence_budget_exhausted .map(|index| &self.records[index]) } - - pub fn latest_scheduler_effectful_user_job_terminal( - &self, - ) -> Option<&AutomationRunLedgerRecord> { - self.latest_scheduler_effectful() - .filter(|record| record.task == AgentTaskKind::UserJob) - } } #[hotpath::measure(label = "automation_runtime.run_ledger.load_page", future = true)] diff --git a/crates/tracedecay-automation-runtime/src/automation/runner.rs b/crates/tracedecay-automation-runtime/src/automation/runner.rs index 16ca32482f..26cbb5beff 100644 --- a/crates/tracedecay-automation-runtime/src/automation/runner.rs +++ b/crates/tracedecay-automation-runtime/src/automation/runner.rs @@ -463,27 +463,6 @@ pub async fn run_combined_review_with_backend_and_retrieval( .await } -pub async fn run_combined_review_with_backend_for_retained_settlement( - cg: &AutomationProjectContext, - config: &AutomationConfig, - configuration_revision_id: &ConfigurationRevisionId, - backend: &dyn AgentTaskBackend, - options: CombinedReviewAutomationOptions, - run_control: &AutomationRunControl, -) -> RetainedCombinedReviewRun { - let retrieval = unavailable_automation_retrieval("session_evidence_retrieval_unavailable"); - run_combined_review_with_backend_and_retrieval_for_retained_settlement( - cg, - config, - configuration_revision_id, - backend, - retrieval.as_ref(), - options, - run_control, - ) - .await -} - #[allow(clippy::too_many_arguments)] pub async fn run_combined_review_with_backend_and_retrieval_for_retained_settlement( cg: &AutomationProjectContext, diff --git a/crates/tracedecay-automation-runtime/src/automation/skill_usage.rs b/crates/tracedecay-automation-runtime/src/automation/skill_usage.rs index 020746e0a0..d45d0f3a74 100644 --- a/crates/tracedecay-automation-runtime/src/automation/skill_usage.rs +++ b/crates/tracedecay-automation-runtime/src/automation/skill_usage.rs @@ -204,22 +204,6 @@ pub async fn load_skill_usage_ledger(profile_root: &Path) -> Result Result<()> { - // Split the snapshot. Do not rewrite an aggregate file, and do not delete - // a skill file that this snapshot does not mention. - for record in ledger.records.values() { - let owned = record.clone(); - let skill_id = owned.skill_id.clone(); - let first_seen_at = owned.first_seen_at; - store::update_record(profile_root, &skill_id, first_seen_at, move |slot| { - *slot = owned; - }) - .await?; - } - Ok(()) -} - pub async fn sync_skill_usage_metadata(profile_root: &Path, skill: &ManagedSkill) -> Result<()> { let skill = skill.clone(); let skill_id = skill.metadata.id.clone(); diff --git a/crates/tracedecay-automation/src/analytics.rs b/crates/tracedecay-automation/src/analytics.rs index 3069d6068b..e804faeb95 100644 --- a/crates/tracedecay-automation/src/analytics.rs +++ b/crates/tracedecay-automation/src/analytics.rs @@ -7,7 +7,8 @@ //! //! Host MCP namespaces are restated here as literals so this contracts leaf //! does not depend on `tracedecay-agent-hosts`. They must stay aligned with -//! `tracedecay_agent_hosts::tool_name::ALL_TOOL_PREFIXES`. +//! the namespaces hosts still emit, including the prior plugin key and the +//! flattened separator. use std::collections::{BTreeMap, BTreeSet}; From 63afff290b5732b88322429dc18b11f0f4d94c9b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:51:42 +0000 Subject: [PATCH 076/182] simplify(pass-3/5): delete unused index and extraction helpers Co-authored-by: Zack Jackson --- .../src/incremental.rs | 9 ----- crates/tracedecay-code-extraction/src/lib.rs | 6 ---- .../src/code_index_scheduler/serving.rs | 5 --- crates/tracedecay-code-index/src/extract.rs | 7 ---- .../tracedecay-code-index/src/parallelism.rs | 33 ------------------- .../src/production/lexical_page_source.rs | 4 --- .../src/production/mod.rs | 8 ----- .../src/code_intelligence/search.rs | 9 ----- .../lexical/projection/artifact/reader.rs | 12 ------- 9 files changed, 93 deletions(-) diff --git a/crates/tracedecay-code-extraction/src/incremental.rs b/crates/tracedecay-code-extraction/src/incremental.rs index b78ee635a4..461a442d55 100644 --- a/crates/tracedecay-code-extraction/src/incremental.rs +++ b/crates/tracedecay-code-extraction/src/incremental.rs @@ -599,15 +599,6 @@ impl RetainedParseDocument { self.reparse_normalized(next_identity, new_source.into(), None, None) } - pub fn reparse_prepared( - &mut self, - next_identity: ParseDocumentIdentity, - new_source: impl Into, - new_parsed_source: impl Into, - ) -> Result { - self.reparse_prepared_with_control(next_identity, new_source, new_parsed_source, None) - } - pub fn reparse_prepared_with_control( &mut self, next_identity: ParseDocumentIdentity, diff --git a/crates/tracedecay-code-extraction/src/lib.rs b/crates/tracedecay-code-extraction/src/lib.rs index 72aa410221..fec55fe2cc 100644 --- a/crates/tracedecay-code-extraction/src/lib.rs +++ b/crates/tracedecay-code-extraction/src/lib.rs @@ -482,12 +482,6 @@ impl LanguageRegistry { } } - #[cfg(any(test, feature = "test-helpers"))] - #[doc(hidden)] - pub fn from_extractors_for_test(extractors: Vec>) -> Self { - Self::from_extractors(extractors) - } - /// Returns the extractor for a file path based on its extension. pub fn extractor_for_file(&self, path: &str) -> Option<&dyn LanguageExtractor> { let extractor = path.rsplit('.').next().and_then(|ext| { diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/serving.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/serving.rs index 78c802ef5a..8b6a9af0ae 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/serving.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/serving.rs @@ -2039,11 +2039,6 @@ impl LatestCompleteCodeIndexV1 { pub fn graph_edges(&self) -> &[tracedecay_domain::CanonicalRelationEdgeV1] { self.generation.edges() } - - #[cfg(test)] - pub fn graph_abstentions(&self) -> &[crate::code_index::chunks::CodeIndexEdgeAbstentionV1] { - self.generation.edge_abstentions() - } } impl LatestCodeTextGenerationV1 { diff --git a/crates/tracedecay-code-index/src/extract.rs b/crates/tracedecay-code-index/src/extract.rs index 3aedf93cb1..6aab34a550 100644 --- a/crates/tracedecay-code-index/src/extract.rs +++ b/crates/tracedecay-code-index/src/extract.rs @@ -234,13 +234,6 @@ impl TreeSitterExtractor { } } - /// Create the adapter over an existing extraction registry. - pub fn from_registry(parsers: tracedecay_code_extraction::LanguageRegistry) -> Self { - Self { - parsers: Arc::new(parsers), - } - } - /// Share one generation-scoped registry with downstream chunking. pub fn from_shared_registry( parsers: Arc, diff --git a/crates/tracedecay-code-index/src/parallelism.rs b/crates/tracedecay-code-index/src/parallelism.rs index 507a445125..d0c8e7191a 100644 --- a/crates/tracedecay-code-index/src/parallelism.rs +++ b/crates/tracedecay-code-index/src/parallelism.rs @@ -363,39 +363,6 @@ fn environment_override_value() -> Result, CodeIndexWorkerPlanErr } } -/// Preview the worker status without constructing a pool or installing any -/// process authority. `available_memory_bytes` must come from the caller's -/// canonical resident-memory authority (`limit - used`), never a second -/// estimator. Environment precedence and every typed refusal are identical to -/// [`install_worker_plan`]. -pub fn preview_worker_plan( - configured: CodeIndexWorkerSelectionV1, - available_memory_bytes: u64, -) -> Result { - let environment_override = environment_override_value()?; - preview_worker_plan_from( - configured, - detected_cores(), - available_memory_bytes, - environment_override.as_deref(), - ) -} - -fn preview_worker_plan_from( - configured: CodeIndexWorkerSelectionV1, - available_logical_cpus: usize, - available_memory_bytes: u64, - environment_override: Option<&str>, -) -> Result { - worker_plan_from( - configured, - available_logical_cpus, - available_memory_bytes, - environment_override, - ) - .map(CodeIndexWorkerPlanV1::status) -} - fn compare_installed_plan( existing: &CodeIndexWorkerPlanV1, requested: &CodeIndexWorkerPlanV1, diff --git a/crates/tracedecay-code-index/src/production/lexical_page_source.rs b/crates/tracedecay-code-index/src/production/lexical_page_source.rs index fbc77afea7..e587d326a2 100644 --- a/crates/tracedecay-code-index/src/production/lexical_page_source.rs +++ b/crates/tracedecay-code-index/src/production/lexical_page_source.rs @@ -461,10 +461,6 @@ impl VerifiedSealedLexicalPageV1 { &self.symbol_displays } - pub fn symbol_display_capacity(&self) -> usize { - self.symbol_displays.capacity() - } - pub fn imports(&self) -> &[CodeIndexImportEvidenceV1] { &self.imports } diff --git a/crates/tracedecay-code-index/src/production/mod.rs b/crates/tracedecay-code-index/src/production/mod.rs index 2660154924..2c28a6f6e7 100644 --- a/crates/tracedecay-code-index/src/production/mod.rs +++ b/crates/tracedecay-code-index/src/production/mod.rs @@ -318,14 +318,6 @@ impl CodeIndexGenerationScopeV1 { } } - pub fn for_branch_stack_node(node: &tracedecay_domain::BranchStackNodeV1) -> Self { - Self { - repository: node.repository_id.clone(), - reference: Some(node.reference.clone()), - worktree: node.worktree_id.clone(), - } - } - /// Whether two scopes name the same physical checkout. /// /// Repository and worktree are checkout identity: a generation sealed diff --git a/crates/tracedecay-domain/src/code_intelligence/search.rs b/crates/tracedecay-domain/src/code_intelligence/search.rs index 6a0d9b3fb0..98809351fb 100644 --- a/crates/tracedecay-domain/src/code_intelligence/search.rs +++ b/crates/tracedecay-domain/src/code_intelligence/search.rs @@ -1052,15 +1052,6 @@ impl ChangedCodeChunkSetV1 { Ok((reused.len() as u64, reused_digest)) } - /// Like [`Self::seal_reused_partition_refs`], but skips per-row identity - /// validation. Callers must pass already-validated manifest rows. - pub fn seal_reused_partition_refs_trusted( - reused: &[(&CodeSearchChunkId, &ContentDigest)], - ) -> Result<(u64, ManifestDigest), DomainError> { - let reused_digest = code_reused_partition_digest_refs_trusted(reused)?; - Ok((reused.len() as u64, reused_digest)) - } - /// Seal Arc-shared reuse from the parent full-replay commitment. /// /// Use at Arc-share publish only. Pair-list sealing stays on the mixed / diff --git a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/reader.rs b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/reader.rs index af80a42cbe..0e773fc6d1 100644 --- a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/reader.rs +++ b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/reader.rs @@ -151,18 +151,6 @@ pub(super) enum CloneArtifactCursorPositionV1 { } impl CloneArtifactCursorV1 { - /// Digests identifying the last completed fingerprint candidate when this - /// cursor continues a near-clone page; `None` for exact-posting cursors. - pub fn fingerprint_continuation_digests(&self) -> Option<(&ManifestDigest, &ManifestDigest)> { - match &self.after { - CloneArtifactCursorPositionV1::Fingerprint { - body_digest, - payload_digest, - } => Some((body_digest, payload_digest)), - CloneArtifactCursorPositionV1::Exact(_) => None, - } - } - pub fn encode(&self) -> Result { serde_json::to_vec(self) .map(hex::encode) From 229a6b4db77ec01d09bcb517481f2230c2a36dac Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:51:42 +0000 Subject: [PATCH 077/182] simplify(pass-4/5): delete unused contract and surface helpers Co-authored-by: Zack Jackson --- .../tracedecay-configuration/src/config/mod.rs | 16 ---------------- .../src/config/topology.rs | 8 +------- crates/tracedecay-contracts/src/invocation.rs | 13 ------------- .../tracedecay-contracts/src/remote/query.rs | 2 -- .../src/storage/telemetry.rs | 5 ----- crates/tracedecay-hooks/src/core_events.rs | 18 ------------------ crates/tracedecay-lsp/src/analyzer/activity.rs | 9 --------- .../src/tools/catalog_discovery.rs | 18 ------------------ .../src/memory/curation.rs | 16 ---------------- .../src/runtime/git_correlation/backfill.rs | 9 --------- 10 files changed, 1 insertion(+), 113 deletions(-) diff --git a/crates/tracedecay-configuration/src/config/mod.rs b/crates/tracedecay-configuration/src/config/mod.rs index 1d8f93b5fd..f93675f95a 100644 --- a/crates/tracedecay-configuration/src/config/mod.rs +++ b/crates/tracedecay-configuration/src/config/mod.rs @@ -320,22 +320,6 @@ pub fn required_string_list( } } -/// An optional text setting (canonical JSON policy trees are stored as text). -/// Absence is `None`; presence with another type is an error. -pub fn optional_text_setting<'a>( - snapshot: &'a ConfigurationSnapshotV1, - key_name: &str, -) -> Result> { - match snapshot.effective_values.get(&setting_key(key_name)?) { - None => Ok(None), - Some(ConfigurationValueV1::Text(value)) => Ok(Some(value)), - Some(value) => Err(config_error(format!( - "resolved configuration setting '{key_name}' has wrong type: expected text, got {:?}", - value.kind() - ))), - } -} - fn config_error(message: impl Into) -> TraceDecayError { TraceDecayError::Config { message: message.into(), diff --git a/crates/tracedecay-configuration/src/config/topology.rs b/crates/tracedecay-configuration/src/config/topology.rs index 8196a7ae28..c249201264 100644 --- a/crates/tracedecay-configuration/src/config/topology.rs +++ b/crates/tracedecay-configuration/src/config/topology.rs @@ -10,7 +10,7 @@ use thiserror::Error; use tracedecay_domain::DomainError; use tracedecay_domain::configuration::{ ConfigurationSnapshotV1, ConfigurationValueV1, SettingKey, WORK_TOPOLOGY_POLICY_SETTING_KEY, - WorkTopologyPolicyV1, safe_work_topology_policy_v1, + WorkTopologyPolicyV1, }; #[derive(Debug, Error)] @@ -40,12 +40,6 @@ pub fn resolved_work_topology_policy( } } -/// Exposes the exact safe policy used by the typed registry before any -/// operator publishes a protected replacement. -pub fn safe_default_work_topology_policy() -> WorkTopologyPolicyV1 { - safe_work_topology_policy_v1() -} - #[cfg(test)] mod tests { use std::collections::BTreeMap; diff --git a/crates/tracedecay-contracts/src/invocation.rs b/crates/tracedecay-contracts/src/invocation.rs index df598ff8b2..0e06e06477 100644 --- a/crates/tracedecay-contracts/src/invocation.rs +++ b/crates/tracedecay-contracts/src/invocation.rs @@ -254,19 +254,6 @@ impl ApplicationRequest { pub const fn is_cancellation(&self) -> bool { matches!(self, Self::OperationCancel { .. }) } - - pub fn feedback_observation_parts(&self) -> Option<(&ManifestDigest, UtcMicros, &Value)> { - match self { - Self::FeedbackObservation { - configuration_digest, - observed_at, - event, - } => Some((configuration_digest, *observed_at, event)), - Self::Surface { .. } | Self::OperationEvents { .. } | Self::OperationCancel { .. } => { - None - } - } - } } /// One complete transport-neutral invocation. diff --git a/crates/tracedecay-contracts/src/remote/query.rs b/crates/tracedecay-contracts/src/remote/query.rs index 4dc8ee2351..3e18d12a1c 100644 --- a/crates/tracedecay-contracts/src/remote/query.rs +++ b/crates/tracedecay-contracts/src/remote/query.rs @@ -35,8 +35,6 @@ use crate::{ }; pub const REMOTE_QUERY_SCHEMA_REVISION_V1: u16 = 1; -pub const REMOTE_EXACT_OBSERVATION_QUERY_USE_CASE_V1: &str = - "use-case.remote.query.exact-observation"; static REMOTE_EXACT_OBSERVATION_QUERY_RESULT_CONTRACT_V1: LazyLock = LazyLock::new(|| { diff --git a/crates/tracedecay-contracts/src/storage/telemetry.rs b/crates/tracedecay-contracts/src/storage/telemetry.rs index 87f30c7774..2f708c2f78 100644 --- a/crates/tracedecay-contracts/src/storage/telemetry.rs +++ b/crates/tracedecay-contracts/src/storage/telemetry.rs @@ -122,11 +122,6 @@ impl TableGrowthSampleV1 { pub fn growth_bytes(&self) -> StorageByteSizeV1 { self.current_bytes.saturating_sub(self.previous_bytes) } - - #[must_use] - pub fn is_growing(&self) -> bool { - self.current_bytes > self.previous_bytes - } } /// An owner-configured soft size budget for one store. diff --git a/crates/tracedecay-hooks/src/core_events.rs b/crates/tracedecay-hooks/src/core_events.rs index 9c9ed310c4..87e27ce7e3 100644 --- a/crates/tracedecay-hooks/src/core_events.rs +++ b/crates/tracedecay-hooks/src/core_events.rs @@ -90,10 +90,6 @@ impl DaemonHookEvent { self } - pub fn cursor_after_file_edit(rel_paths: Vec) -> Self { - Self::new(HookAgent::Cursor, "afterFileEdit", rel_paths, None, None) - } - pub fn cursor_after_shell_execution(cwd: PathBuf) -> Self { Self::new( HookAgent::Cursor, @@ -104,16 +100,6 @@ impl DaemonHookEvent { ) } - pub fn cursor_workspace_open(cwd: PathBuf) -> Self { - Self::new( - HookAgent::Cursor, - "workspaceOpen", - Vec::new(), - None, - Some(cwd), - ) - } - /// A provider session started: let the daemon own branch tracking and /// index refresh for the session's actual working directory. pub fn session_start(agent: HookAgent, cwd: PathBuf) -> Self { @@ -130,8 +116,4 @@ impl DaemonHookEvent { pub fn post_tool_use_shell(agent: HookAgent, cwd: PathBuf) -> Self { Self::new(agent, "postToolUseShell", Vec::new(), None, Some(cwd)) } - - pub fn kiro_post_tool_use(rel_paths: Vec, cwd: Option) -> Self { - Self::new(HookAgent::Kiro, "postToolUse", rel_paths, None, cwd) - } } diff --git a/crates/tracedecay-lsp/src/analyzer/activity.rs b/crates/tracedecay-lsp/src/analyzer/activity.rs index df9a153f1c..e9ead1a9a4 100644 --- a/crates/tracedecay-lsp/src/analyzer/activity.rs +++ b/crates/tracedecay-lsp/src/analyzer/activity.rs @@ -131,15 +131,6 @@ fn matches_adapter_extension(adapter: &LspAdapterDefinition, file: &str) -> bool }) } -pub fn adapter_workspace_root( - project_root: &Path, - adapter: &LspAdapterDefinition, - file: &str, -) -> Option { - let project_root = canonicalize_project_root(project_root).ok()?; - adapter_workspace_root_from_canonical_root(&project_root, adapter, file) -} - pub(crate) fn adapter_workspace_root_from_canonical_root( project_root: &Path, adapter: &LspAdapterDefinition, diff --git a/crates/tracedecay-mcp/src/tools/catalog_discovery.rs b/crates/tracedecay-mcp/src/tools/catalog_discovery.rs index fc3797e807..354596e11f 100644 --- a/crates/tracedecay-mcp/src/tools/catalog_discovery.rs +++ b/crates/tracedecay-mcp/src/tools/catalog_discovery.rs @@ -306,24 +306,6 @@ pub fn catalog_discovery_tools_list_payload( Ok(payload) } -pub fn get_catalog_filtered_tool_definitions_with_warming_budget( - budget: u8, - profile_id: &ProfileId, - authorized_capabilities: &BTreeSet, - available_scope: &BTreeSet, - registry_mode: ToolRegistryMode, -) -> Result, McpDispatchMetadataError> { - let entry = discovery_cache_get_or_insert( - profile_id, - authorized_capabilities, - available_scope, - registry_mode, - )?; - let mut definitions = (*entry.tools).clone(); - apply_context_description(&mut definitions, &context_warming_description(budget)); - Ok(definitions) -} - pub fn default_catalog_discovery_authority() -> Result, tracedecay_daemon_protocol::ApplicationSurfaceAdapterError> { Ok( diff --git a/crates/tracedecay-session-memory/src/memory/curation.rs b/crates/tracedecay-session-memory/src/memory/curation.rs index 4c4f4a1d07..6334df418d 100644 --- a/crates/tracedecay-session-memory/src/memory/curation.rs +++ b/crates/tracedecay-session-memory/src/memory/curation.rs @@ -473,22 +473,6 @@ impl MemoryApplication { .map_err(MemoryApplicationError::from) } - /// Constructs an owner-bound update command from the canonical store patch. - pub fn canonical_fact_update_command( - &self, - target: ProjectMemoryFactMutationTarget, - patch: ProjectMemoryFactUpdatePatchV1, - context: &MemoryOperationContext, - ) -> Result { - update_command( - &self.owner, - target, - patch, - context.operation_id().clone(), - context.actor().cloned(), - ) - } - /// Constructs an owner-bound compare-and-set remove command. pub fn canonical_fact_remove_command( &self, diff --git a/crates/tracedecay-sessions/src/runtime/git_correlation/backfill.rs b/crates/tracedecay-sessions/src/runtime/git_correlation/backfill.rs index bbd82edd58..edbb87d441 100644 --- a/crates/tracedecay-sessions/src/runtime/git_correlation/backfill.rs +++ b/crates/tracedecay-sessions/src/runtime/git_correlation/backfill.rs @@ -73,15 +73,6 @@ impl SessionActivityRow { _ => None, } } - - /// The activity timestamp the incremental backfill orders and watermarks by: - /// the newest message time, else the declared end, else the start. Mirrors - /// the `COALESCE(MAX(m.timestamp), s.ended_at, s.started_at)` key used by - /// [`session_activity_page_after`], so the returned value compares directly - /// against the persisted watermark (both are raw, un-normalized bounds). - pub fn activity_sort_key(&self) -> Option { - self.message_max_ts.or(self.ended_at).or(self.started_at) - } } /// One `HEAD` position in a worktree's reflog timeline: the branch `HEAD` From 04b0ad6a75a555e2ab96298881cacd595f8ea962 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:51:43 +0000 Subject: [PATCH 078/182] simplify(pass-5/5): share canonical message role labels Co-authored-by: Zack Jackson --- crates/tracedecay-domain/src/observation.rs | 14 ++++++++++ .../hosts/claude/canonical_projection.rs | 16 ++--------- .../src/canonical_projection.rs | 28 ++++++------------- 3 files changed, 26 insertions(+), 32 deletions(-) diff --git a/crates/tracedecay-domain/src/observation.rs b/crates/tracedecay-domain/src/observation.rs index 954855587e..03209670a3 100644 --- a/crates/tracedecay-domain/src/observation.rs +++ b/crates/tracedecay-domain/src/observation.rs @@ -1728,6 +1728,20 @@ pub enum CanonicalMessageRoleV1 { Unknown, } +impl CanonicalMessageRoleV1 { + /// Role label written into canonical projections. It is the serde name so + /// projection and the wire encoding cannot drift. + pub const fn as_str(self) -> &'static str { + match self { + Self::User => "user", + Self::Assistant => "assistant", + Self::System => "system", + Self::Tool => "tool", + Self::Unknown => "unknown", + } + } +} + #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum CanonicalReasoningVisibilityV1 { diff --git a/crates/tracedecay-sessions/src/runtime/hosts/claude/canonical_projection.rs b/crates/tracedecay-sessions/src/runtime/hosts/claude/canonical_projection.rs index 10e52e1ec7..a5ab409eb3 100644 --- a/crates/tracedecay-sessions/src/runtime/hosts/claude/canonical_projection.rs +++ b/crates/tracedecay-sessions/src/runtime/hosts/claude/canonical_projection.rs @@ -3,8 +3,8 @@ use std::path::{Path, PathBuf}; use serde::Serialize; use serde_json::{Map, Value}; use tracedecay_domain::{ - CanonicalBoundaryKindV1, CanonicalGitEvidenceKindV1, CanonicalMessageRoleV1, - CanonicalObservationEnvelopeV1, CanonicalObservationFactV1, CanonicalWorkflowEvidenceKindV1, + CanonicalBoundaryKindV1, CanonicalGitEvidenceKindV1, CanonicalObservationEnvelopeV1, + CanonicalObservationFactV1, CanonicalWorkflowEvidenceKindV1, }; use tracedecay_runtime_core::logging::log_daemon_event; @@ -179,7 +179,7 @@ pub(super) fn map_canonical_claude_record( .as_str() .to_owned(), session_id: context.session_id.to_owned(), - role: canonical_role(*role).to_owned(), + role: role.as_str().to_owned(), timestamp: timestamp.or_else(|| envelope.evidence().native_timestamp()), ordinal: offset, text, @@ -492,16 +492,6 @@ fn insert_canonical_envelope( } } -fn canonical_role(role: CanonicalMessageRoleV1) -> &'static str { - match role { - CanonicalMessageRoleV1::User => "user", - CanonicalMessageRoleV1::Assistant => "assistant", - CanonicalMessageRoleV1::System => "system", - CanonicalMessageRoleV1::Tool => "tool", - CanonicalMessageRoleV1::Unknown => "unknown", - } -} - #[cfg(test)] mod canonical_envelope_omission_tests { use super::insert_canonical_envelope; diff --git a/crates/tracedecay-store/src/canonical_projection.rs b/crates/tracedecay-store/src/canonical_projection.rs index 0ee649de97..9751240815 100644 --- a/crates/tracedecay-store/src/canonical_projection.rs +++ b/crates/tracedecay-store/src/canonical_projection.rs @@ -2,8 +2,8 @@ use serde::Deserialize; use tracedecay_domain::{ - CanonicalGitEvidenceKindV1, CanonicalMessageRoleV1, CanonicalObservationEnvelopeV1, - CanonicalObservationFactV1, CanonicalReasoningVisibilityV1, CanonicalWorkflowEvidenceKindV1, + CanonicalGitEvidenceKindV1, CanonicalObservationEnvelopeV1, CanonicalObservationFactV1, + CanonicalReasoningVisibilityV1, CanonicalWorkflowEvidenceKindV1, CanonicalWorkflowSemanticKindV1, DurableObservationV1, ObservationContractError, ObservationScopeV1, }; @@ -406,7 +406,7 @@ fn canonical_message_metadata_for( rendering, envelope.provider().as_str(), envelope.native_record_kind(), - canonical_role(*role), + role.as_str(), content, envelope.relations().message_id() != Some(envelope.stable_record_id()), ) @@ -787,7 +787,7 @@ fn canonical_message_fields_for( .iter() .find(|fact| matches!(fact, CanonicalObservationFactV1::Message { .. })) { - let role = canonical_role(*role); + let role = role.as_str(); let text = canonical_fact_text(content)?; if let Some(semantics) = rendering_message_semantics( rendering, @@ -957,16 +957,6 @@ pub fn canonical_fact_text(value: &serde_json::Value) -> ProjectionStoreResult &'static str { - match role { - CanonicalMessageRoleV1::User => "user", - CanonicalMessageRoleV1::Assistant => "assistant", - CanonicalMessageRoleV1::System => "system", - CanonicalMessageRoleV1::Tool => "tool", - CanonicalMessageRoleV1::Unknown => "unknown", - } -} - fn reasoning_kind(visibility: CanonicalReasoningVisibilityV1) -> &'static str { match visibility { CanonicalReasoningVisibilityV1::Visible => "reasoning_visible", @@ -1013,11 +1003,11 @@ pub fn workflow_semantic_kind(kind: CanonicalWorkflowSemanticKindV1) -> &'static mod tests { use serde_json::json; use tracedecay_domain::{ - CanonicalBoundaryKindV1, CanonicalObservationEvidenceV1, CanonicalObservationRelationsV1, - ComponentVersion, ObservationId, ObservationIdentityMaterialV1, - ObservationOrderingDomainV1, ObservationSourceGenerationV1, ObservationSourceIdentityV1, - ObservationSourceRangeV1, PayloadReferenceV1, ProviderId, RetentionClass, - SanitizationReceiptId, SanitizationReceiptRefV1, SanitizationReceiptV1, + CanonicalBoundaryKindV1, CanonicalMessageRoleV1, CanonicalObservationEvidenceV1, + CanonicalObservationRelationsV1, ComponentVersion, ObservationId, + ObservationIdentityMaterialV1, ObservationOrderingDomainV1, ObservationSourceGenerationV1, + ObservationSourceIdentityV1, ObservationSourceRangeV1, PayloadReferenceV1, ProviderId, + RetentionClass, SanitizationReceiptId, SanitizationReceiptRefV1, SanitizationReceiptV1, SanitizerDispositionV1, SensitivityV1, SessionId, }; From a8031dabc2c7ad385a5e150e15849cc4197cf7cd Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:51:54 +0000 Subject: [PATCH 079/182] simplify(pass-4/5): share lease fan-out and cancellation Co-authored-by: Zack Jackson --- crates/tracedecay-graph-db/src/lease.rs | 195 +++++++++--------- crates/tracedecay-graph-db/src/projection.rs | 8 + .../src/projection_identity_index.rs | 9 +- .../src/projection_read.rs | 9 +- .../src/registry/support.rs | 9 +- crates/tracedecay-graph-db/src/state.rs | 13 +- 6 files changed, 106 insertions(+), 137 deletions(-) diff --git a/crates/tracedecay-graph-db/src/lease.rs b/crates/tracedecay-graph-db/src/lease.rs index 735fc06778..12cac0505b 100644 --- a/crates/tracedecay-graph-db/src/lease.rs +++ b/crates/tracedecay-graph-db/src/lease.rs @@ -421,16 +421,14 @@ impl VerifiedGraphSnapshot { max_relations: usize, cancellation: Arc, ) -> Result>, GraphDbError> { - self.with_operation(|| { - self.with_head_database(|database| { - database.outgoing_relation_ids( - &self.head.locator.physical_namespace()?, - starts, - relation_kinds, - max_relations, - cancellation, - ) - }) + self.head_fanout(|database, namespace| { + database.outgoing_relation_ids( + &namespace, + starts, + relation_kinds, + max_relations, + cancellation, + ) }) } @@ -452,16 +450,14 @@ impl VerifiedGraphSnapshot { max_relations: usize, cancellation: Arc, ) -> Result>, GraphDbError> { - self.with_operation(|| { - self.with_head_database(|database| { - database.incoming_relation_ids( - &self.head.locator.physical_namespace()?, - starts, - relation_kinds, - max_relations, - cancellation, - ) - }) + self.head_fanout(|database, namespace| { + database.incoming_relation_ids( + &namespace, + starts, + relation_kinds, + max_relations, + cancellation, + ) }) } @@ -477,17 +473,15 @@ impl VerifiedGraphSnapshot { limit: usize, cancellation: Arc, ) -> Result>, GraphDbError> { - self.with_operation(|| { - self.with_head_database(|database| { - database.outgoing_relation_ids_page( - &self.head.locator.physical_namespace()?, - starts, - relation_kinds, - after, - limit, - cancellation, - ) - }) + self.head_fanout(|database, namespace| { + database.outgoing_relation_ids_page( + &namespace, + starts, + relation_kinds, + after, + limit, + cancellation, + ) }) } @@ -503,17 +497,15 @@ impl VerifiedGraphSnapshot { limit: usize, cancellation: Arc, ) -> Result>, GraphDbError> { - self.with_operation(|| { - self.with_head_database(|database| { - database.incoming_relation_ids_page( - &self.head.locator.physical_namespace()?, - starts, - relation_kinds, - after, - limit, - cancellation, - ) - }) + self.head_fanout(|database, namespace| { + database.incoming_relation_ids_page( + &namespace, + starts, + relation_kinds, + after, + limit, + cancellation, + ) }) } @@ -532,16 +524,14 @@ impl VerifiedGraphSnapshot { max_relations: usize, cancellation: Arc, ) -> Result>, GraphDbError> { - self.with_operation(|| { - self.with_head_database(|database| { - database.outgoing_relations( - &self.head.locator.physical_namespace()?, - starts, - relation_kinds, - max_relations, - cancellation, - ) - }) + self.head_fanout(|database, namespace| { + database.outgoing_relations( + &namespace, + starts, + relation_kinds, + max_relations, + cancellation, + ) }) } @@ -558,16 +548,14 @@ impl VerifiedGraphSnapshot { max_relations: usize, cancellation: Arc, ) -> Result>, GraphDbError> { - self.with_operation(|| { - self.with_head_database(|database| { - database.outgoing_relations_truncated( - &self.head.locator.physical_namespace()?, - starts, - relation_kinds, - max_relations, - cancellation, - ) - }) + self.head_fanout(|database, namespace| { + database.outgoing_relations_truncated( + &namespace, + starts, + relation_kinds, + max_relations, + cancellation, + ) }) } @@ -582,16 +570,14 @@ impl VerifiedGraphSnapshot { max_relations: usize, cancellation: Arc, ) -> Result>, GraphDbError> { - self.with_operation(|| { - self.with_head_database(|database| { - database.outgoing_relation_targets( - &self.head.locator.physical_namespace()?, - starts, - relation_kinds, - max_relations, - cancellation, - ) - }) + self.head_fanout(|database, namespace| { + database.outgoing_relation_targets( + &namespace, + starts, + relation_kinds, + max_relations, + cancellation, + ) }) } @@ -602,16 +588,14 @@ impl VerifiedGraphSnapshot { cancellation: Arc, visitor: &mut dyn FnMut(crate::GraphRelationTarget), ) -> Result { - self.with_operation(|| { - self.with_head_database(|database| { - database.visit_outgoing_relation_targets( - &self.head.locator.physical_namespace()?, - start, - relation_kinds, - cancellation, - visitor, - ) - }) + self.head_fanout(|database, namespace| { + database.visit_outgoing_relation_targets( + &namespace, + start, + relation_kinds, + cancellation, + visitor, + ) }) } @@ -627,16 +611,14 @@ impl VerifiedGraphSnapshot { max_relations: usize, cancellation: Arc, ) -> Result>, GraphDbError> { - self.with_operation(|| { - self.with_head_database(|database| { - database.incoming_relations( - &self.head.locator.physical_namespace()?, - starts, - relation_kinds, - max_relations, - cancellation, - ) - }) + self.head_fanout(|database, namespace| { + database.incoming_relations( + &namespace, + starts, + relation_kinds, + max_relations, + cancellation, + ) }) } @@ -653,16 +635,14 @@ impl VerifiedGraphSnapshot { max_relations: usize, cancellation: Arc, ) -> Result>, GraphDbError> { - self.with_operation(|| { - self.with_head_database(|database| { - database.incoming_relations_truncated( - &self.head.locator.physical_namespace()?, - starts, - relation_kinds, - max_relations, - cancellation, - ) - }) + self.head_fanout(|database, namespace| { + database.incoming_relations_truncated( + &namespace, + starts, + relation_kinds, + max_relations, + cancellation, + ) }) } @@ -700,6 +680,17 @@ impl VerifiedGraphSnapshot { Ok(()) } + fn head_fanout( + &self, + operation: impl FnOnce(&crate::GraphDb, GraphNamespace) -> Result, + ) -> Result { + self.with_operation(|| { + self.with_head_database(|database| { + operation(database, self.head.locator.physical_namespace()?) + }) + }) + } + fn with_operation( &self, operation: impl FnOnce() -> Result, diff --git a/crates/tracedecay-graph-db/src/projection.rs b/crates/tracedecay-graph-db/src/projection.rs index 6a884fa6ff..6e3011ab49 100644 --- a/crates/tracedecay-graph-db/src/projection.rs +++ b/crates/tracedecay-graph-db/src/projection.rs @@ -33,6 +33,14 @@ impl GraphCancellation for NeverCancelled { } } +pub(crate) fn check_cancelled(cancellation: &dyn GraphCancellation) -> Result<(), GraphDbError> { + if cancellation.is_cancelled() { + Err(GraphDbError::Cancelled) + } else { + Ok(()) + } +} + macro_rules! opaque_id { ($name:ident) => { #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] diff --git a/crates/tracedecay-graph-db/src/projection_identity_index.rs b/crates/tracedecay-graph-db/src/projection_identity_index.rs index 968392698c..a5022ace4c 100644 --- a/crates/tracedecay-graph-db/src/projection_identity_index.rs +++ b/crates/tracedecay-graph-db/src/projection_identity_index.rs @@ -27,6 +27,7 @@ use std::sync::{Arc, RwLock}; use grafeo_common::types::Value; use grafeo_engine::GrafeoDB; +use crate::projection::check_cancelled; use crate::projection_read::IdentityScope; use crate::schema::{has_native_label, nodes_with_label}; use crate::{GraphCancellation, GraphDbError}; @@ -220,11 +221,3 @@ fn build_identity_index( node_count, })) } - -fn check_cancelled(cancellation: &dyn GraphCancellation) -> Result<(), GraphDbError> { - if cancellation.is_cancelled() { - Err(GraphDbError::Cancelled) - } else { - Ok(()) - } -} diff --git a/crates/tracedecay-graph-db/src/projection_read.rs b/crates/tracedecay-graph-db/src/projection_read.rs index 2c7921ac28..d0779a8696 100644 --- a/crates/tracedecay-graph-db/src/projection_read.rs +++ b/crates/tracedecay-graph-db/src/projection_read.rs @@ -5,6 +5,7 @@ use std::sync::Arc; use grafeo_common::types::Value; use grafeo_engine::GrafeoDB; +use crate::projection::check_cancelled; use crate::schema::{ ENTITY_ID_PROPERTY, ENTITY_LABEL, RELATION_ID_PROPERTY, RELATION_LABEL, entity_projection_label, relation_projection_label, @@ -473,14 +474,6 @@ fn validate_optional_page_limit(limit: usize) -> Result<(), GraphDbError> { } } -fn check_cancelled(cancellation: &dyn GraphCancellation) -> Result<(), GraphDbError> { - if cancellation.is_cancelled() { - Err(GraphDbError::Cancelled) - } else { - Ok(()) - } -} - fn persisted_identity_error(description: &str, error: GraphDbError) -> GraphDbError { GraphDbError::Corrupt { message: format!("invalid persisted {description} identity: {error}"), diff --git a/crates/tracedecay-graph-db/src/registry/support.rs b/crates/tracedecay-graph-db/src/registry/support.rs index 89991c5231..1ba228a650 100644 --- a/crates/tracedecay-graph-db/src/registry/support.rs +++ b/crates/tracedecay-graph-db/src/registry/support.rs @@ -14,6 +14,7 @@ use super::{ }; use crate::error::rollback_failure; use crate::location::PersistentGraphStoreState; +use crate::projection::check_cancelled; use crate::{ GraphCancellation, GraphDb, GraphDbError, GraphDbLocation, GraphDbOpenOptions, GraphDbOwner, GraphDbRuntimeState, GraphDurability, GraphFormatVersion, @@ -195,14 +196,6 @@ fn registered_open_options( } } -fn check_cancelled(cancellation: &dyn GraphCancellation) -> Result<(), GraphDbError> { - if cancellation.is_cancelled() { - Err(GraphDbError::Cancelled) - } else { - Ok(()) - } -} - /// `op` names the registered operation whose deadline is being enforced. /// `DeadlineExceeded` is a unit error, so without this label a failure deep /// in a projection or publication cannot be attributed to the registration diff --git a/crates/tracedecay-graph-db/src/state.rs b/crates/tracedecay-graph-db/src/state.rs index 08a855bd80..c813c17a8e 100644 --- a/crates/tracedecay-graph-db/src/state.rs +++ b/crates/tracedecay-graph-db/src/state.rs @@ -404,7 +404,7 @@ fn load_requested_relations( if index % 256 == 0 && batch.cancellation.is_cancelled() { return Err(GraphDbError::Cancelled); } - if let Some(relation) = load_relation_cached(database, namespace, identity, &mut endpoints)? + if let Some(relation) = load_relation_by_key(database, namespace, identity, &mut endpoints)? { loaded.insert(key, relation); } @@ -442,7 +442,7 @@ pub(crate) fn load_relation( namespace: &GraphNamespace, identity: &GraphRelationId, ) -> Result, GraphDbError> { - load_relation_cached( + load_relation_by_key( database, namespace, identity, @@ -450,15 +450,6 @@ pub(crate) fn load_relation( ) } -pub(crate) fn load_relation_cached( - database: &GrafeoDB, - namespace: &GraphNamespace, - identity: &GraphRelationId, - cache: &mut EndpointIdentityCache, -) -> Result, GraphDbError> { - load_relation_by_key(database, namespace, identity, cache) -} - pub(crate) fn load_relation_by_edge( database: &GrafeoDB, edge_id: EdgeId, From fef386571833496d1df73dd257dceb5011117bdf Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:44:00 +0000 Subject: [PATCH 080/182] simplify(pass-3/5): collapse store-error admission mapping Store failures had two matches that restated the same collision arms. One match keeps the refusal, retry, and degraded reason codes. Co-authored-by: Zack Jackson --- crates/tracedecay-host-admission/src/lib.rs | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/crates/tracedecay-host-admission/src/lib.rs b/crates/tracedecay-host-admission/src/lib.rs index 2a66016b17..0e90fb01ac 100644 --- a/crates/tracedecay-host-admission/src/lib.rs +++ b/crates/tracedecay-host-admission/src/lib.rs @@ -1139,7 +1139,7 @@ fn accepted_for_external_source_replay( } fn classify_store_error(error: &ObservationStoreError) -> HostAdmissionOutcome { - match error { + let reason_code = match error { ObservationStoreError::BatchRequiresScalarFallback { cause } => { return HostAdmissionOutcome::batch_requires_scalar_fallback(*cause); } @@ -1158,9 +1158,9 @@ fn classify_store_error(error: &ObservationStoreError) -> HostAdmissionOutcome { "observation_retrieval_anchor_alias_collision", ); } - _ => {} - } - let reason_code = match error { + ObservationStoreError::CursorConflict { .. } | ObservationStoreError::Storage { .. } => { + unreachable!("retryable store failures are classified before static reason mapping") + } ObservationStoreError::CursorObservationMismatch => "observation_cursor_mismatch", ObservationStoreError::CursorCoverageMismatch => "observation_cursor_coverage_mismatch", ObservationStoreError::CursorAdvanceCollision => "observation_cursor_advance_collision", @@ -1170,10 +1170,6 @@ fn classify_store_error(error: &ObservationStoreError) -> HostAdmissionOutcome { ObservationStoreError::CursorSanitizationReceiptMismatch => { "observation_cursor_sanitization_receipt_mismatch" } - ObservationStoreError::ObservationCollision { .. } => "observation_identity_collision", - ObservationStoreError::SanitizationReceiptCollision => { - "observation_sanitization_receipt_collision" - } ObservationStoreError::RetrievalAnchorObservationMismatch => { "observation_retrieval_anchor_observation_mismatch" } @@ -1202,14 +1198,8 @@ fn classify_store_error(error: &ObservationStoreError) -> HostAdmissionOutcome { ObservationStoreError::RepositoryProvenanceContract(_) => { "observation_repository_provenance_contract_invalid" } - ObservationStoreError::RetrievalAnchorAliasCollision { .. } => { - "observation_retrieval_anchor_alias_collision" - } ObservationStoreError::InvalidReplayLimit { .. } => "observation_replay_limit_invalid", ObservationStoreError::Contract(_) => "observation_store_contract_invalid", - ObservationStoreError::CursorConflict { .. } | ObservationStoreError::Storage { .. } => { - unreachable!("retryable store failures are classified before static reason mapping") - } _ => "observation_store_failed", }; admission_outcome(HostAdmissionStatus::Degraded, false, Some(reason_code)) From e100a823fb89dbadd0618499af66df8316a2da87 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:44:18 +0000 Subject: [PATCH 081/182] simplify(pass-4/5): collapse duplicate admission constructors for_project and for_profile were aliases of the registered_* builders. Callers use the one name; the aliases are gone. Co-authored-by: Zack Jackson --- .../src/authorities.rs | 21 ++----------------- .../handlers/hook_runtime/context_scout.rs | 2 +- .../src/test_support/host_admission.rs | 2 +- 3 files changed, 4 insertions(+), 21 deletions(-) diff --git a/crates/tracedecay-host-admission/src/authorities.rs b/crates/tracedecay-host-admission/src/authorities.rs index 07aed131de..58e9016abe 100644 --- a/crates/tracedecay-host-admission/src/authorities.rs +++ b/crates/tracedecay-host-admission/src/authorities.rs @@ -23,7 +23,7 @@ pub struct HostAdmissionAuthorities<'a> { } impl<'a> HostAdmissionAuthorities<'a> { - pub fn registered_for_project( + pub fn for_project( brain_id: BrainId, profile_id: UserProfileId, project_id: ProjectId, @@ -40,7 +40,7 @@ impl<'a> HostAdmissionAuthorities<'a> { } } - pub(crate) fn registered_for_profile( + pub fn for_profile( brain_id: BrainId, profile_id: UserProfileId, registered: &'a RegisteredGlobalDb, @@ -64,23 +64,6 @@ impl<'a> HostAdmissionAuthorities<'a> { self } - pub fn for_project( - brain_id: BrainId, - profile_id: UserProfileId, - project_id: ProjectId, - registered: &'a RegisteredGlobalDb, - ) -> Self { - Self::registered_for_project(brain_id, profile_id, project_id, registered) - } - - pub fn for_profile( - brain_id: BrainId, - profile_id: UserProfileId, - registered: &'a RegisteredGlobalDb, - ) -> Self { - Self::registered_for_profile(brain_id, profile_id, registered) - } - /// Adds the registered profile-session authority to project admission. #[must_use] pub fn with_profile_registered( diff --git a/crates/tracedecay-mcp/src/handlers/hook_runtime/context_scout.rs b/crates/tracedecay-mcp/src/handlers/hook_runtime/context_scout.rs index 6915b276dd..70b6fa2659 100644 --- a/crates/tracedecay-mcp/src/handlers/hook_runtime/context_scout.rs +++ b/crates/tracedecay-mcp/src/handlers/hook_runtime/context_scout.rs @@ -130,7 +130,7 @@ pub(super) async fn admit_native_context_scout_lifecycle( Err(_) => return false, }; let binding = sessions.binding(); - let authorities = HostAdmissionAuthorities::registered_for_project( + let authorities = HostAdmissionAuthorities::for_project( binding.shard_id.brain_id.clone(), binding.shard_id.profile_id.clone(), project_id, diff --git a/crates/tracedecay-project/src/test_support/host_admission.rs b/crates/tracedecay-project/src/test_support/host_admission.rs index 6a1838bac7..bf29ae9b83 100644 --- a/crates/tracedecay-project/src/test_support/host_admission.rs +++ b/crates/tracedecay-project/src/test_support/host_admission.rs @@ -953,7 +953,7 @@ impl HostAdmissionTestRuntimeV1 { pub fn facade(&self) -> HostAdmissionFacade<'_> { let authorities = match (self.project_id.as_ref(), self.project_registered.as_ref()) { (Some(project_id), Some(project_registered)) => { - HostAdmissionAuthorities::registered_for_project( + HostAdmissionAuthorities::for_project( self.brain_id.clone(), self.profile_id.clone(), project_id.clone(), From 13401e355d578d8cab1cbae4ea3f3b40f6060615 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:52:15 +0000 Subject: [PATCH 082/182] simplify(pass-5/5): collapse path, count, and env copies One path matcher covers both automation deadlines. Count helpers are one saturating conversion, and the hotpath env read is one scan. Co-authored-by: Zack Jackson --- .../src/analytics_api.rs | 15 ++++----- .../src/request_deadline.rs | 31 ++++++++----------- .../src/savings_api.rs | 24 ++++++-------- crates/tracedecay-hotpath-guard/src/lib.rs | 8 ++--- 4 files changed, 34 insertions(+), 44 deletions(-) diff --git a/crates/tracedecay-dashboard-api/src/analytics_api.rs b/crates/tracedecay-dashboard-api/src/analytics_api.rs index 4efc3bbb41..31f819e5f1 100644 --- a/crates/tracedecay-dashboard-api/src/analytics_api.rs +++ b/crates/tracedecay-dashboard-api/src/analytics_api.rs @@ -1052,10 +1052,7 @@ async fn durable_analytics_rows( } pub fn hint_summary_from_events(events: &[AnalyticsEventRecord]) -> AnalyticsHintsPayloadV1 { - let mut by_category: BTreeMap = HINT_CATEGORIES - .iter() - .map(|category| ((*category).to_string(), HintCounts::default())) - .collect(); + let mut by_category = zero_hint_counts(); for event in events { let category = event.hint_category.as_deref().unwrap_or(""); @@ -1104,11 +1101,15 @@ pub fn hint_summary_from_counts(counts: &[AnalyticsHintCounts]) -> Value { }) } -fn typed_hint_summary_from_counts(counts: &[AnalyticsHintCounts]) -> AnalyticsHintsPayloadV1 { - let mut by_category: BTreeMap = HINT_CATEGORIES +fn zero_hint_counts() -> BTreeMap { + HINT_CATEGORIES .iter() .map(|category| ((*category).to_string(), HintCounts::default())) - .collect(); + .collect() +} + +fn typed_hint_summary_from_counts(counts: &[AnalyticsHintCounts]) -> AnalyticsHintsPayloadV1 { + let mut by_category = zero_hint_counts(); for row in counts { by_category.insert( row.category.clone(), diff --git a/crates/tracedecay-dashboard-api/src/request_deadline.rs b/crates/tracedecay-dashboard-api/src/request_deadline.rs index 43d138fbef..b3e85183c6 100644 --- a/crates/tracedecay-dashboard-api/src/request_deadline.rs +++ b/crates/tracedecay-dashboard-api/src/request_deadline.rs @@ -1,30 +1,25 @@ pub(super) fn dashboard_http_request_deadline_micros(path: &str) -> i64 { - if path == "/api/application/retained/fact_store_curate" - || is_project_scoped_automation_run_path(path) - || is_user_job_run_path(path) - { + let automation_run = path == "/api/application/retained/fact_store_curate" + || one_segment_then( + path, + "/api/projects/", + "application/retained/fact_store_curate", + ) + || one_segment_then(path, "/api/automation/jobs/", "run"); + if automation_run { super::DASHBOARD_AUTOMATION_RUN_REQUEST_DEADLINE_MICROS } else { super::DASHBOARD_CODE_GRAPH_REQUEST_DEADLINE_MICROS } } -fn is_user_job_run_path(path: &str) -> bool { - let Some(job_and_action) = path.strip_prefix("/api/automation/jobs/") else { +/// `{prefix}{nonempty}/{tail}` with `tail` compared in full, including slashes. +fn one_segment_then(path: &str, prefix: &str, tail: &str) -> bool { + let Some(rest) = path.strip_prefix(prefix) else { return false; }; - let Some((job_id, action)) = job_and_action.split_once('/') else { + let Some((head, rest_tail)) = rest.split_once('/') else { return false; }; - !job_id.is_empty() && action == "run" -} - -fn is_project_scoped_automation_run_path(path: &str) -> bool { - let Some(project_and_tail) = path.strip_prefix("/api/projects/") else { - return false; - }; - let Some((project_id, tail)) = project_and_tail.split_once('/') else { - return false; - }; - !project_id.is_empty() && tail == "application/retained/fact_store_curate" + !head.is_empty() && rest_tail == tail } diff --git a/crates/tracedecay-dashboard-api/src/savings_api.rs b/crates/tracedecay-dashboard-api/src/savings_api.rs index 202d7721af..ac4f022462 100644 --- a/crates/tracedecay-dashboard-api/src/savings_api.rs +++ b/crates/tracedecay-dashboard-api/src/savings_api.rs @@ -502,12 +502,8 @@ fn price_deltas<'a>( price_provider_usage(&aggregate, prices, 0) } -fn count_i64(value: usize) -> i64 { - i64::try_from(value).unwrap_or(i64::MAX) -} - -fn count_u64(value: u64) -> i64 { - i64::try_from(value).unwrap_or(i64::MAX) +fn count_i64(value: impl TryInto) -> i64 { + value.try_into().unwrap_or(i64::MAX) } fn cost_basis_label(cost_usd: Option) -> &'static str { @@ -594,9 +590,9 @@ fn provider_spend( summary.unpriced_events, subtotal.priced_events, ), - usage_events: count_u64(summary.usage_events), - priced_events: count_u64(subtotal.priced_events), - unpriced_events: count_u64(summary.unpriced_events), + usage_events: count_i64(summary.usage_events), + priced_events: count_i64(subtotal.priced_events), + unpriced_events: count_i64(summary.unpriced_events), unknown_model_events: count_i64( deltas.iter().filter(|delta| delta.model.is_none()).count(), ), @@ -629,9 +625,9 @@ fn provider_day_point( SavingsProviderDayPointV1 { day, provider: provider.to_owned(), - usage_events: count_u64(summary.usage_events), - priced_events: count_u64(subtotal.priced_events), - unpriced_events: count_u64(summary.unpriced_events), + usage_events: count_i64(summary.usage_events), + priced_events: count_i64(subtotal.priced_events), + unpriced_events: count_i64(summary.unpriced_events), priced_cost_usd: subtotal.priced_cost_usd, total_cost_usd: summary.total_cost_usd, total_tokens: total_tokens_of(actual.as_ref()), @@ -712,7 +708,7 @@ fn provider_usage_attribution( SavingsProviderModelSpendV1 { provider, model: (!model.is_empty()).then_some(model), - usage_events: count_u64(priced.usage_events), + usage_events: count_i64(priced.usage_events), cost_usd: priced.total_cost_usd, total_tokens: total_tokens_of(actual.as_ref()), cost_basis: cost_basis_label(priced.total_cost_usd).to_owned(), @@ -728,7 +724,7 @@ fn provider_usage_attribution( let (_, actual) = actual_for_deltas(deltas.into_iter()); SavingsProviderDaySpendV1 { day, - usage_events: count_u64(priced.usage_events), + usage_events: count_i64(priced.usage_events), cost_usd: priced.total_cost_usd, total_tokens: total_tokens_of(actual.as_ref()), provider_actual: actual, diff --git a/crates/tracedecay-hotpath-guard/src/lib.rs b/crates/tracedecay-hotpath-guard/src/lib.rs index 89121cb917..def93497ff 100644 --- a/crates/tracedecay-hotpath-guard/src/lib.rs +++ b/crates/tracedecay-hotpath-guard/src/lib.rs @@ -21,11 +21,9 @@ pub fn with_functions_display_limit( } fn functions_display_limit() -> Option { - parse_usize_env("HOTPATH_FUNCTIONS_LIMIT").or_else(|| parse_usize_env("HOTPATH_LIMIT")) -} - -fn parse_usize_env(name: &str) -> Option { - std::env::var(name).ok().and_then(|raw| raw.parse().ok()) + ["HOTPATH_FUNCTIONS_LIMIT", "HOTPATH_LIMIT"] + .into_iter() + .find_map(|name| std::env::var(name).ok().and_then(|raw| raw.parse().ok())) } #[cfg(test)] From 34302bf7081a90c1d1cd05289d1cdcb34995e12d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:52:55 +0000 Subject: [PATCH 083/182] simplify(pass-5/5): collapse repeated sort and load loops Co-authored-by: Zack Jackson --- crates/tracedecay-graph-db/src/generation.rs | 87 ++++++++------------ crates/tracedecay-graph-db/src/state.rs | 64 ++++---------- 2 files changed, 52 insertions(+), 99 deletions(-) diff --git a/crates/tracedecay-graph-db/src/generation.rs b/crates/tracedecay-graph-db/src/generation.rs index d35a9affee..7771911523 100644 --- a/crates/tracedecay-graph-db/src/generation.rs +++ b/crates/tracedecay-graph-db/src/generation.rs @@ -389,9 +389,24 @@ impl GraphGenerationManifest { MAX_VERIFIED_GENERATION_RELATIONS, )); } - let dependencies = checked_sorted_dependencies(dependencies, check)?; - let entities = checked_sorted_entities(entities, check)?; - let relations = checked_sorted_relations(relations, check)?; + let dependencies = checked_sorted_by( + dependencies, + check, + Ord::cmp, + "a graph generation repeats a dependency", + )?; + let entities = checked_sorted_by( + entities, + check, + |left, right| left.identity.cmp(&right.identity), + "a graph generation repeats an entity identity", + )?; + let relations = checked_sorted_by( + relations, + check, + |left, right| left.identity.cmp(&right.identity), + "a graph generation repeats a relation identity", + )?; let manifest = Self { projection, generation, @@ -777,58 +792,22 @@ impl GraphGenerationManifest { } } -fn checked_sorted_dependencies( - mut dependencies: Vec, +fn checked_sorted_by( + mut items: Vec, check: &dyn Fn() -> Result<(), GraphDbError>, -) -> Result, GraphDbError> { + mut order: impl FnMut(&T, &T) -> std::cmp::Ordering, + duplicate: &'static str, +) -> Result, GraphDbError> { check()?; - dependencies.sort_unstable(); - for pair in dependencies.windows(2) { + items.sort_unstable_by(&mut order); + for pair in items.windows(2) { check()?; - if pair[0] == pair[1] { - return Err(GraphDbError::invalid( - "a graph generation repeats a dependency", - )); + if order(&pair[0], &pair[1]).is_eq() { + return Err(GraphDbError::invalid(duplicate)); } } check()?; - Ok(dependencies) -} - -fn checked_sorted_entities( - mut entities: Vec, - check: &dyn Fn() -> Result<(), GraphDbError>, -) -> Result, GraphDbError> { - check()?; - entities.sort_unstable_by(|left, right| left.identity.cmp(&right.identity)); - for pair in entities.windows(2) { - check()?; - if pair[0].identity == pair[1].identity { - return Err(GraphDbError::invalid( - "a graph generation repeats an entity identity", - )); - } - } - check()?; - Ok(entities) -} - -fn checked_sorted_relations( - mut relations: Vec, - check: &dyn Fn() -> Result<(), GraphDbError>, -) -> Result, GraphDbError> { - check()?; - relations.sort_unstable_by(|left, right| left.identity.cmp(&right.identity)); - for pair in relations.windows(2) { - check()?; - if pair[0].identity == pair[1].identity { - return Err(GraphDbError::invalid( - "a graph generation repeats a relation identity", - )); - } - } - check()?; - Ok(relations) + Ok(items) } /// The dependency-closure digest, shared by the full manifest and its @@ -2041,7 +2020,7 @@ mod checked_vec_writer_tests { use super::{ CheckedVecWriter, GraphDbError, GraphGenerationManifest, ManifestDigestChunk, ManifestDigestChunkEncoding, ManifestDigestPipelineConfig, ManifestDigestPipelineMetrics, - canonical_buffer_allocation_growths, checked_canonical_bytes, checked_sorted_entities, + canonical_buffer_allocation_growths, checked_canonical_bytes, checked_sorted_by, encode_manifest_digest_chunk, frame_length_headers, recovered_generation_digest, recovered_generation_digest_with_config, reset_canonical_buffer_allocation_growths, }; @@ -2263,7 +2242,13 @@ mod checked_vec_writer_tests { entities.shrink_to_fit(); let allocation = entities.as_ptr(); - let sorted = checked_sorted_entities(entities, &|| Ok(())).unwrap(); + let sorted = checked_sorted_by( + entities, + &|| Ok(()), + |left, right| left.identity.cmp(&right.identity), + "a graph generation repeats an entity identity", + ) + .unwrap(); assert_eq!( sorted.as_ptr(), diff --git a/crates/tracedecay-graph-db/src/state.rs b/crates/tracedecay-graph-db/src/state.rs index c813c17a8e..a995594ae4 100644 --- a/crates/tracedecay-graph-db/src/state.rs +++ b/crates/tracedecay-graph-db/src/state.rs @@ -141,15 +141,22 @@ impl ExistingBatchState { entity_locator_keys.retain(|key, _| !entity_keys.contains_key(key)); let entities = hotpath::measure_block!( "graph_db.mutation.existing_state.entity_records", - load_requested_entities(database, &batch.namespace, entity_keys, batch) + load_requested(entity_keys, batch, |identity| { + load_entity(database, &batch.namespace, identity) + }) )?; let entity_locators = hotpath::measure_block!( "graph_db.mutation.existing_state.endpoint_locators", - load_requested_entity_locators(database, &batch.namespace, entity_locator_keys, batch,) + load_requested(entity_locator_keys, batch, |identity| { + load_entity_locator(database, &batch.namespace, identity) + }) )?; + let mut endpoints = EndpointIdentityCache::default(); let relations = hotpath::measure_block!( "graph_db.mutation.existing_state.relation_records", - load_requested_relations(database, &batch.namespace, relation_keys, batch) + load_requested(relation_keys, batch, |identity| { + load_relation_by_key(database, &batch.namespace, identity, &mut endpoints) + }) )?; Ok(Self { entities, @@ -356,57 +363,18 @@ pub(crate) fn load_entity( })) } -fn load_requested_entities( - database: &GrafeoDB, - namespace: &GraphNamespace, - requested: HashMap, +fn load_requested( + requested: HashMap, batch: &GraphWriteBatch, -) -> Result, GraphDbError> { + mut load: impl FnMut(&K) -> Result, GraphDbError>, +) -> Result, GraphDbError> { let mut loaded = BTreeMap::new(); for (index, (key, identity)) in requested.into_iter().enumerate() { if index % 256 == 0 && batch.cancellation.is_cancelled() { return Err(GraphDbError::Cancelled); } - if let Some(entity) = load_entity(database, namespace, identity)? { - loaded.insert(key, entity); - } - } - Ok(loaded) -} - -fn load_requested_entity_locators( - database: &GrafeoDB, - namespace: &GraphNamespace, - requested: HashMap, - batch: &GraphWriteBatch, -) -> Result, GraphDbError> { - let mut loaded = BTreeMap::new(); - for (index, (key, identity)) in requested.into_iter().enumerate() { - if index % 256 == 0 && batch.cancellation.is_cancelled() { - return Err(GraphDbError::Cancelled); - } - if let Some(entity) = load_entity_locator(database, namespace, identity)? { - loaded.insert(key, entity); - } - } - Ok(loaded) -} - -fn load_requested_relations( - database: &GrafeoDB, - namespace: &GraphNamespace, - requested: HashMap, - batch: &GraphWriteBatch, -) -> Result, GraphDbError> { - let mut loaded = BTreeMap::new(); - let mut endpoints = EndpointIdentityCache::default(); - for (index, (key, identity)) in requested.into_iter().enumerate() { - if index % 256 == 0 && batch.cancellation.is_cancelled() { - return Err(GraphDbError::Cancelled); - } - if let Some(relation) = load_relation_by_key(database, namespace, identity, &mut endpoints)? - { - loaded.insert(key, relation); + if let Some(value) = load(identity)? { + loaded.insert(key, value); } } Ok(loaded) From 64e1643b3e61b5e2fe7c6fed5f3818b291e5719f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:54:24 +0000 Subject: [PATCH 084/182] simplify(pass-5/5): drop unused helpers and share catalogs Remove helpers with no callers, share the Git effect-surface catalog shape, and collapse identical legacy config defaults. Co-authored-by: Zack Jackson --- .../src/config/mod.rs | 16 --- .../src/config/model.rs | 108 ++++++------------ .../src/config/topology.rs | 8 +- .../execution_topology_metrics/rollup_read.rs | 2 +- .../src/git/effect_surface.rs | 51 +++++++++ crates/tracedecay-contracts/src/git/mod.rs | 1 + .../src/git/native_integration_surface.rs | 69 ++--------- .../src/git/surface_catalog.rs | 58 +--------- crates/tracedecay-contracts/src/invocation.rs | 18 --- .../tracedecay-contracts/src/remote/query.rs | 2 - .../src/storage/telemetry.rs | 5 - .../src/code_intelligence/search.rs | 9 -- .../tracedecay-domain/src/git/read_model.rs | 9 -- 13 files changed, 106 insertions(+), 250 deletions(-) create mode 100644 crates/tracedecay-contracts/src/git/effect_surface.rs diff --git a/crates/tracedecay-configuration/src/config/mod.rs b/crates/tracedecay-configuration/src/config/mod.rs index 1d8f93b5fd..f93675f95a 100644 --- a/crates/tracedecay-configuration/src/config/mod.rs +++ b/crates/tracedecay-configuration/src/config/mod.rs @@ -320,22 +320,6 @@ pub fn required_string_list( } } -/// An optional text setting (canonical JSON policy trees are stored as text). -/// Absence is `None`; presence with another type is an error. -pub fn optional_text_setting<'a>( - snapshot: &'a ConfigurationSnapshotV1, - key_name: &str, -) -> Result> { - match snapshot.effective_values.get(&setting_key(key_name)?) { - None => Ok(None), - Some(ConfigurationValueV1::Text(value)) => Ok(Some(value)), - Some(value) => Err(config_error(format!( - "resolved configuration setting '{key_name}' has wrong type: expected text, got {:?}", - value.kind() - ))), - } -} - fn config_error(message: impl Into) -> TraceDecayError { TraceDecayError::Config { message: message.into(), diff --git a/crates/tracedecay-configuration/src/config/model.rs b/crates/tracedecay-configuration/src/config/model.rs index 80589af683..d9b10a40ea 100644 --- a/crates/tracedecay-configuration/src/config/model.rs +++ b/crates/tracedecay-configuration/src/config/model.rs @@ -55,6 +55,18 @@ fn has_minified_suffix(path: &str) -> bool { path.rfind(".min.").is_some_and(|idx| idx + 5 < path.len()) } +fn default_true() -> bool { + true +} + +fn default_false() -> bool { + false +} + +fn default_thirty_day_retention() -> Option { + Some(30) +} + /// Default glob-pattern exclude list for [`TraceDecayConfig::default`]. /// /// Built from [`GENERATED_DIR_SEGMENTS`] (both the `segment/**` root form @@ -117,7 +129,7 @@ pub struct TraceDecayConfig { /// Whether to track call-site locations for edges. pub track_call_sites: bool, /// Whether to respect `.gitignore` rules when scanning files. - #[serde(default = "default_git_ignore")] + #[serde(default = "default_true")] pub git_ignore: bool, /// Whether a cold `tracedecay_diagnostics` call prewarms in the background /// (detached dependency build + immediate `warming` status) instead of @@ -128,7 +140,7 @@ pub struct TraceDecayConfig { /// Whether the persistent native code graph may activate for this project. /// Disabling it leaves exact and lexical retrieval available and reports /// graph capability as unavailable. - #[serde(default = "default_native_graph_activation")] + #[serde(default = "default_true")] pub native_graph_activation: bool, /// Index-freshness auto-sync settings (git-metadata watcher, serve-stale, /// branch lifecycle). Absent in older `config.json` files, so defaulted. @@ -140,20 +152,6 @@ pub struct TraceDecayConfig { pub telemetry: TelemetryConfig, } -fn default_git_ignore() -> bool { - true -} - -fn default_native_graph_activation() -> bool { - true -} - -fn default_sync_auto_watch() -> bool { - false -} -fn default_sync_watch_linked_worktrees() -> bool { - false -} fn default_sync_watch_debounce_ms() -> u64 { 2000 } @@ -163,15 +161,9 @@ fn default_sync_watch_max_delay_ms() -> u64 { fn default_sync_watch_max_projects() -> usize { 32 } -fn default_sync_read_refresh() -> bool { - true -} fn default_sync_read_cooldown_secs() -> u64 { 30 } -fn default_sync_session_start_sync() -> bool { - true -} fn default_sync_session_start_stale_threshold_secs() -> u64 { 600 } @@ -190,12 +182,6 @@ fn default_sync_branch_gc_days() -> u64 { fn default_sync_orphan_db_gc_days() -> u64 { 7 } -fn default_sync_auto_init() -> bool { - true -} -fn default_sync_auto_track_pr_branches() -> bool { - false -} fn default_sync_auto_track_pr_poll_secs() -> u64 { 300 } @@ -203,14 +189,6 @@ fn default_retention_interval_hours() -> u64 { 24 } -fn default_orphan_store_gc_days() -> Option { - Some(30) -} - -fn default_incident_debris_retention_days() -> Option { - Some(30) -} - fn default_compaction_threshold() -> Option { Some(CompactionThresholdConfig::default()) } @@ -230,11 +208,11 @@ pub struct RetentionConfig { pub observation: tracedecay_global_db::observation::retention::ObservationRetentionConfig, /// Orphan profile-sharded store collection window (days). `None` disables /// the sweep; the Doctor surface still reports findings read-only. - #[serde(default = "default_orphan_store_gc_days")] + #[serde(default = "default_thirty_day_retention")] pub orphan_store_gc_days: Option, /// Retention window for quarantined recovery/corruption artifacts (days). /// `None` disables collection while Doctor continues surfacing debris. - #[serde(default = "default_incident_debris_retention_days")] + #[serde(default = "default_thirty_day_retention")] pub incident_debris_retention_days: Option, /// Incremental-vacuum compaction trigger. `None` disables compaction. #[serde(default = "default_compaction_threshold")] @@ -254,8 +232,8 @@ impl Default for RetentionConfig { session_lcm: tracedecay_lcm::LcmRetentionConfig::default(), observation: tracedecay_global_db::observation::retention::ObservationRetentionConfig::default(), - orphan_store_gc_days: default_orphan_store_gc_days(), - incident_debris_retention_days: default_incident_debris_retention_days(), + orphan_store_gc_days: default_thirty_day_retention(), + incident_debris_retention_days: default_thirty_day_retention(), compaction: default_compaction_threshold(), store_soft_budgets_bytes: BTreeMap::new(), interval_hours: default_retention_interval_hours(), @@ -325,20 +303,16 @@ impl RetentionConfig { /// clamped up to this. pub const MIN_AUTO_TRACK_PR_POLL_SECS: u64 = 60; -fn default_telemetry_timings() -> bool { - true -} - #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct TelemetryConfig { - #[serde(default = "default_telemetry_timings")] + #[serde(default = "default_true")] pub timings: bool, } impl Default for TelemetryConfig { fn default() -> Self { Self { - timings: default_telemetry_timings(), + timings: default_true(), } } } @@ -360,11 +334,11 @@ impl Default for TelemetryConfig { )] pub struct SyncConfig { /// Enable the daemon git-metadata watcher. - #[serde(default = "default_sync_auto_watch")] + #[serde(default = "default_false")] pub auto_watch: bool, /// Admit linked worktrees into the daemon watcher without an explicit /// branch-indexing request. - #[serde(default = "default_sync_watch_linked_worktrees")] + #[serde(default = "default_false")] pub watch_linked_worktrees: bool, /// Per-project quiet-period debounce before a watcher-triggered sync (ms). #[serde(default = "default_sync_watch_debounce_ms")] @@ -376,13 +350,13 @@ pub struct SyncConfig { #[serde(default = "default_sync_watch_max_projects")] pub watch_max_projects: usize, /// Enable non-blocking sync-on-read for query tools. - #[serde(default = "default_sync_read_refresh")] + #[serde(default = "default_true")] pub read_refresh: bool, /// Cooldown between read-triggered background refreshes (seconds). #[serde(default = "default_sync_read_cooldown_secs")] pub read_cooldown_secs: u64, /// Fire a catch-up sync on session start. - #[serde(default = "default_sync_session_start_sync")] + #[serde(default = "default_true")] pub session_start_sync: bool, /// Staleness threshold above which session-start sync runs (seconds). #[serde(default = "default_sync_session_start_stale_threshold_secs")] @@ -403,13 +377,13 @@ pub struct SyncConfig { #[serde(default = "default_sync_orphan_db_gc_days")] pub orphan_db_gc_days: u64, /// Auto-initialise never-indexed repos on first contact. - #[serde(default = "default_sync_auto_init")] + #[serde(default = "default_true")] pub auto_init: bool, /// Enable the daemon PR-branch auto-tracking mode: when on, the daemon polls /// the repo's GitHub remote for open PRs and tracks/untracks each PR head /// branch through the normal branch-tracking machinery. Off by default for /// back-compat. - #[serde(default = "default_sync_auto_track_pr_branches")] + #[serde(default = "default_false")] pub auto_track_pr_branches: bool, /// Poll cadence (seconds) for PR-branch auto-tracking discovery. Clamped up /// to [`MIN_AUTO_TRACK_PR_POLL_SECS`] at read time. @@ -432,22 +406,22 @@ impl SyncConfig { impl Default for SyncConfig { fn default() -> Self { Self { - auto_watch: default_sync_auto_watch(), - watch_linked_worktrees: default_sync_watch_linked_worktrees(), + auto_watch: default_false(), + watch_linked_worktrees: default_false(), watch_debounce_ms: default_sync_watch_debounce_ms(), watch_max_delay_ms: default_sync_watch_max_delay_ms(), watch_max_projects: default_sync_watch_max_projects(), - read_refresh: default_sync_read_refresh(), + read_refresh: default_true(), read_cooldown_secs: default_sync_read_cooldown_secs(), - session_start_sync: default_sync_session_start_sync(), + session_start_sync: default_true(), session_start_stale_threshold_secs: default_sync_session_start_stale_threshold_secs(), backstop_interval_mins: default_sync_backstop_interval_mins(), full_sync_escalation_files: default_sync_full_sync_escalation_files(), max_concurrent_syncs: default_sync_max_concurrent_syncs(), branch_gc_days: default_sync_branch_gc_days(), orphan_db_gc_days: default_sync_orphan_db_gc_days(), - auto_init: default_sync_auto_init(), - auto_track_pr_branches: default_sync_auto_track_pr_branches(), + auto_init: default_true(), + auto_track_pr_branches: default_false(), auto_track_pr_poll_secs: default_sync_auto_track_pr_poll_secs(), retention: RetentionConfig::default(), } @@ -551,9 +525,9 @@ impl Default for TraceDecayConfig { max_file_size: 1_048_576, extract_docstrings: true, track_call_sites: true, - git_ignore: default_git_ignore(), + git_ignore: default_true(), diagnostics_prewarm: false, - native_graph_activation: default_native_graph_activation(), + native_graph_activation: default_true(), sync: SyncConfig::default(), telemetry: TelemetryConfig::default(), } @@ -825,25 +799,19 @@ pub(crate) fn is_ignored_by_explicit_global_excludes( })) } -#[cfg(test)] fn git_subprocess_path() -> OsString { std::env::var_os("PATH").unwrap_or_else(|| { - #[cfg(windows)] + #[cfg(all(test, not(windows)))] { - OsString::new() + OsString::from("/usr/bin:/bin") } - #[cfg(not(windows))] + #[cfg(not(all(test, not(windows))))] { - OsString::from("/usr/bin:/bin") + OsString::new() } }) } -#[cfg(not(test))] -fn git_subprocess_path() -> OsString { - std::env::var_os("PATH").unwrap_or_default() -} - fn is_in_local_gitignore(project_path: &Path) -> bool { let dir_name = active_data_dir_name(project_path); let gitignore = project_path.join(".gitignore"); diff --git a/crates/tracedecay-configuration/src/config/topology.rs b/crates/tracedecay-configuration/src/config/topology.rs index 8196a7ae28..c249201264 100644 --- a/crates/tracedecay-configuration/src/config/topology.rs +++ b/crates/tracedecay-configuration/src/config/topology.rs @@ -10,7 +10,7 @@ use thiserror::Error; use tracedecay_domain::DomainError; use tracedecay_domain::configuration::{ ConfigurationSnapshotV1, ConfigurationValueV1, SettingKey, WORK_TOPOLOGY_POLICY_SETTING_KEY, - WorkTopologyPolicyV1, safe_work_topology_policy_v1, + WorkTopologyPolicyV1, }; #[derive(Debug, Error)] @@ -40,12 +40,6 @@ pub fn resolved_work_topology_policy( } } -/// Exposes the exact safe policy used by the typed registry before any -/// operator publishes a protected replacement. -pub fn safe_default_work_topology_policy() -> WorkTopologyPolicyV1 { - safe_work_topology_policy_v1() -} - #[cfg(test)] mod tests { use std::collections::BTreeMap; diff --git a/crates/tracedecay-contracts/src/execution_topology_metrics/rollup_read.rs b/crates/tracedecay-contracts/src/execution_topology_metrics/rollup_read.rs index 39e8350452..1419f4c3d8 100644 --- a/crates/tracedecay-contracts/src/execution_topology_metrics/rollup_read.rs +++ b/crates/tracedecay-contracts/src/execution_topology_metrics/rollup_read.rs @@ -2,7 +2,7 @@ //! partial-day boundary pages. use serde::{Deserialize, Serialize}; -use tracedecay_domain::{CoverageStateV1, UtcMicros}; +use tracedecay_domain::CoverageStateV1; use tracedecay_tool_catalog::{CapabilityId, UseCaseId}; use crate::clock::now_micros; diff --git a/crates/tracedecay-contracts/src/git/effect_surface.rs b/crates/tracedecay-contracts/src/git/effect_surface.rs new file mode 100644 index 0000000000..8b9fa1cd10 --- /dev/null +++ b/crates/tracedecay-contracts/src/git/effect_surface.rs @@ -0,0 +1,51 @@ +//! Effect versus read terminal, cancellation, and deadline shapes shared by +//! the public Git and native-integration catalogs. The two surfaces differ +//! in operations, not in these effect-class consequences. + +use tracedecay_tool_catalog::{CancellationPoint, DeadlineBehavior, EffectClass, TerminalState}; + +pub(super) fn cancellation_points(effect: EffectClass) -> Vec { + if effect.is_effect() { + vec![ + CancellationPoint::BeforeAdmission, + CancellationPoint::BeforeEffect, + CancellationPoint::EffectInFlight, + CancellationPoint::AfterCommit, + ] + } else { + vec![ + CancellationPoint::BeforeAdmission, + CancellationPoint::BeforeRead, + CancellationPoint::DuringRead, + ] + } +} + +pub(super) fn deadline_behavior(effect: EffectClass) -> DeadlineBehavior { + if effect.is_effect() { + DeadlineBehavior::ReturnEffectReceipt + } else { + DeadlineBehavior::ReturnOperationReceipt + } +} + +pub(super) fn terminal_states(effect: EffectClass) -> Vec { + if effect.is_effect() { + vec![ + TerminalState::Completed, + TerminalState::Cancelled, + TerminalState::TimedOut, + TerminalState::Failed, + TerminalState::EffectUnknown, + TerminalState::Partial, + ] + } else { + vec![ + TerminalState::Completed, + TerminalState::Cancelled, + TerminalState::TimedOut, + TerminalState::Failed, + TerminalState::Partial, + ] + } +} diff --git a/crates/tracedecay-contracts/src/git/mod.rs b/crates/tracedecay-contracts/src/git/mod.rs index d69ffdb810..671eb9d10c 100644 --- a/crates/tracedecay-contracts/src/git/mod.rs +++ b/crates/tracedecay-contracts/src/git/mod.rs @@ -1,6 +1,7 @@ //! Git index transaction application boundary. mod catalog; +mod effect_surface; mod native_integration; mod native_integration_surface; mod public_wire; diff --git a/crates/tracedecay-contracts/src/git/native_integration_surface.rs b/crates/tracedecay-contracts/src/git/native_integration_surface.rs index af87e142bd..014f91ce5f 100644 --- a/crates/tracedecay-contracts/src/git/native_integration_surface.rs +++ b/crates/tracedecay-contracts/src/git/native_integration_surface.rs @@ -3,15 +3,8 @@ //! `apply_native_integration`, `native_integration_status`, and //! `cancel_native_integration`. //! -//! Plan 36 slice 1 extends "the shipped application and CLI/MCP surfaces with -//! `stack_snapshot` and `preflight_native_integration`", slice 3 adds -//! `apply_native_integration`, `native_integration_status`, and -//! `cancel_native_integration`, `approve_native_integration` is the -//! owner-decided (2026-08-07) sixth operation that issues the one-use -//! apply approval, and slice 4 requires the whole journey to be -//! exposed consistently through CLI and MCP over one application result. That -//! is a different family from the Plan 08 Git *index-transaction* bindings, -//! which stay limited to `git_preview`/`git_apply`; this module never exposes +//! This is a different family from the Git index-transaction bindings, which +//! stay limited to `git_preview` / `git_apply`. This module never exposes //! `stage_hunks`, `unstage_hunks`, or `commit_index`. //! //! Requests carry exact typed identity only. Filesystem paths, free-form @@ -33,14 +26,14 @@ use tracedecay_domain::{ }; use tracedecay_tool_catalog::{ ApplicationSurfaceOperation, AvailabilityContract, BindingId, BindingSurface, - CancellationContract, CancellationPoint, CapabilityId, CapabilityManifestV1, - CatalogContributionInputV1, CatalogContributionV1, ContributionId, DeadlineBehavior, - DeadlineContract, DeniedDisclosurePolicy, EffectClass, ExecutableSchemaAuthority, - LifecycleClass, PrivacyClass, ProfileId, RevalidationContract, RevalidationPoint, - RoutingContractV1, SchemaId, SchemaRef, ScopeDimension, ScopeRequirement, StreamingContract, - TerminalState, TerminalStateContract, UseCaseId, + CancellationContract, CapabilityId, CapabilityManifestV1, CatalogContributionInputV1, + CatalogContributionV1, ContributionId, DeadlineContract, DeniedDisclosurePolicy, EffectClass, + ExecutableSchemaAuthority, LifecycleClass, PrivacyClass, ProfileId, RevalidationContract, + RevalidationPoint, RoutingContractV1, SchemaId, SchemaRef, ScopeDimension, ScopeRequirement, + StreamingContract, TerminalStateContract, UseCaseId, }; +use super::effect_surface::{cancellation_points, deadline_behavior, terminal_states}; use crate::CancellationSignal; use crate::capability_manifest::{ ApplicationCapabilityManifestInput, application_capability_manifest, @@ -912,52 +905,6 @@ fn capability( )?) } -fn cancellation_points(effect: EffectClass) -> Vec { - if effect.is_effect() { - vec![ - CancellationPoint::BeforeAdmission, - CancellationPoint::BeforeEffect, - CancellationPoint::EffectInFlight, - CancellationPoint::AfterCommit, - ] - } else { - vec![ - CancellationPoint::BeforeAdmission, - CancellationPoint::BeforeRead, - CancellationPoint::DuringRead, - ] - } -} - -fn deadline_behavior(effect: EffectClass) -> DeadlineBehavior { - if effect.is_effect() { - DeadlineBehavior::ReturnEffectReceipt - } else { - DeadlineBehavior::ReturnOperationReceipt - } -} - -fn terminal_states(effect: EffectClass) -> Vec { - if effect.is_effect() { - vec![ - TerminalState::Completed, - TerminalState::Cancelled, - TerminalState::TimedOut, - TerminalState::Failed, - TerminalState::EffectUnknown, - TerminalState::Partial, - ] - } else { - vec![ - TerminalState::Completed, - TerminalState::Cancelled, - TerminalState::TimedOut, - TerminalState::Failed, - TerminalState::Partial, - ] - } -} - fn handler_descriptor( spec: &NativeIntegrationSurfaceSpec, ) -> Result { diff --git a/crates/tracedecay-contracts/src/git/surface_catalog.rs b/crates/tracedecay-contracts/src/git/surface_catalog.rs index f8680af3c1..55fa215c6a 100644 --- a/crates/tracedecay-contracts/src/git/surface_catalog.rs +++ b/crates/tracedecay-contracts/src/git/surface_catalog.rs @@ -9,14 +9,14 @@ use schemars::JsonSchema; use tracedecay_domain::{GitIndexPreviewV1, GitIndexTransactionReceiptV1}; use tracedecay_tool_catalog::{ ApplicationSurfaceOperation, AvailabilityContract, BindingId, BindingSurface, - CancellationContract, CancellationPoint, CapabilityId, CapabilityManifestV1, - CatalogContributionInputV1, CatalogContributionV1, ContributionId, DeadlineBehavior, - DeadlineContract, DeniedDisclosurePolicy, EffectClass, ExecutableSchemaAuthority, - LifecycleClass, PrivacyClass, ProfileId, RevalidationContract, RevalidationPoint, - RoutingContractV1, SchemaId, SchemaRef, ScopeDimension, ScopeRequirement, StreamingContract, - TerminalState, TerminalStateContract, UseCaseId, + CancellationContract, CapabilityId, CapabilityManifestV1, CatalogContributionInputV1, + CatalogContributionV1, ContributionId, DeadlineContract, DeniedDisclosurePolicy, EffectClass, + ExecutableSchemaAuthority, LifecycleClass, PrivacyClass, ProfileId, RevalidationContract, + RevalidationPoint, RoutingContractV1, SchemaId, SchemaRef, ScopeDimension, ScopeRequirement, + StreamingContract, TerminalStateContract, UseCaseId, }; +use super::effect_surface::{cancellation_points, deadline_behavior, terminal_states}; use crate::capability_manifest::{ ApplicationCapabilityManifestInput, application_capability_manifest, }; @@ -341,52 +341,6 @@ fn capability( )?) } -fn cancellation_points(effect: EffectClass) -> Vec { - if effect.is_effect() { - vec![ - CancellationPoint::BeforeAdmission, - CancellationPoint::BeforeEffect, - CancellationPoint::EffectInFlight, - CancellationPoint::AfterCommit, - ] - } else { - vec![ - CancellationPoint::BeforeAdmission, - CancellationPoint::BeforeRead, - CancellationPoint::DuringRead, - ] - } -} - -fn deadline_behavior(effect: EffectClass) -> DeadlineBehavior { - if effect.is_effect() { - DeadlineBehavior::ReturnEffectReceipt - } else { - DeadlineBehavior::ReturnOperationReceipt - } -} - -fn terminal_states(effect: EffectClass) -> Vec { - if effect.is_effect() { - vec![ - TerminalState::Completed, - TerminalState::Cancelled, - TerminalState::TimedOut, - TerminalState::Failed, - TerminalState::EffectUnknown, - TerminalState::Partial, - ] - } else { - vec![ - TerminalState::Completed, - TerminalState::Cancelled, - TerminalState::TimedOut, - TerminalState::Failed, - TerminalState::Partial, - ] - } -} - fn handler_descriptor( spec: &SurfaceSpec, ) -> Result { diff --git a/crates/tracedecay-contracts/src/invocation.rs b/crates/tracedecay-contracts/src/invocation.rs index df598ff8b2..10e1a3b124 100644 --- a/crates/tracedecay-contracts/src/invocation.rs +++ b/crates/tracedecay-contracts/src/invocation.rs @@ -245,28 +245,10 @@ impl ApplicationRequest { } } - #[hotpath::skip] - pub const fn is_stream(&self) -> bool { - matches!(self, Self::OperationEvents { .. }) - } - #[hotpath::skip] pub const fn is_cancellation(&self) -> bool { matches!(self, Self::OperationCancel { .. }) } - - pub fn feedback_observation_parts(&self) -> Option<(&ManifestDigest, UtcMicros, &Value)> { - match self { - Self::FeedbackObservation { - configuration_digest, - observed_at, - event, - } => Some((configuration_digest, *observed_at, event)), - Self::Surface { .. } | Self::OperationEvents { .. } | Self::OperationCancel { .. } => { - None - } - } - } } /// One complete transport-neutral invocation. diff --git a/crates/tracedecay-contracts/src/remote/query.rs b/crates/tracedecay-contracts/src/remote/query.rs index 4dc8ee2351..3e18d12a1c 100644 --- a/crates/tracedecay-contracts/src/remote/query.rs +++ b/crates/tracedecay-contracts/src/remote/query.rs @@ -35,8 +35,6 @@ use crate::{ }; pub const REMOTE_QUERY_SCHEMA_REVISION_V1: u16 = 1; -pub const REMOTE_EXACT_OBSERVATION_QUERY_USE_CASE_V1: &str = - "use-case.remote.query.exact-observation"; static REMOTE_EXACT_OBSERVATION_QUERY_RESULT_CONTRACT_V1: LazyLock = LazyLock::new(|| { diff --git a/crates/tracedecay-contracts/src/storage/telemetry.rs b/crates/tracedecay-contracts/src/storage/telemetry.rs index 87f30c7774..2f708c2f78 100644 --- a/crates/tracedecay-contracts/src/storage/telemetry.rs +++ b/crates/tracedecay-contracts/src/storage/telemetry.rs @@ -122,11 +122,6 @@ impl TableGrowthSampleV1 { pub fn growth_bytes(&self) -> StorageByteSizeV1 { self.current_bytes.saturating_sub(self.previous_bytes) } - - #[must_use] - pub fn is_growing(&self) -> bool { - self.current_bytes > self.previous_bytes - } } /// An owner-configured soft size budget for one store. diff --git a/crates/tracedecay-domain/src/code_intelligence/search.rs b/crates/tracedecay-domain/src/code_intelligence/search.rs index 6a0d9b3fb0..98809351fb 100644 --- a/crates/tracedecay-domain/src/code_intelligence/search.rs +++ b/crates/tracedecay-domain/src/code_intelligence/search.rs @@ -1052,15 +1052,6 @@ impl ChangedCodeChunkSetV1 { Ok((reused.len() as u64, reused_digest)) } - /// Like [`Self::seal_reused_partition_refs`], but skips per-row identity - /// validation. Callers must pass already-validated manifest rows. - pub fn seal_reused_partition_refs_trusted( - reused: &[(&CodeSearchChunkId, &ContentDigest)], - ) -> Result<(u64, ManifestDigest), DomainError> { - let reused_digest = code_reused_partition_digest_refs_trusted(reused)?; - Ok((reused.len() as u64, reused_digest)) - } - /// Seal Arc-shared reuse from the parent full-replay commitment. /// /// Use at Arc-share publish only. Pair-list sealing stays on the mixed / diff --git a/crates/tracedecay-domain/src/git/read_model.rs b/crates/tracedecay-domain/src/git/read_model.rs index 726f9c168c..600cf0bba9 100644 --- a/crates/tracedecay-domain/src/git/read_model.rs +++ b/crates/tracedecay-domain/src/git/read_model.rs @@ -65,15 +65,6 @@ pub enum GitObjectFormatV1 { Sha256, } -impl GitObjectFormatV1 { - pub const fn oid_hex_len(self) -> usize { - match self { - Self::Sha1 => 40, - Self::Sha256 => 64, - } - } -} - fn validate_git_oid(value: &str, field: &'static str) -> Result<(), DomainError> { if value.is_empty() { return Err(DomainError::Empty { field }); From 552c625073280efd65713e1e235092910bc450b6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:55:02 +0000 Subject: [PATCH 085/182] simplify(pass-2/5): share one XML escape helper Co-authored-by: Zack Jackson --- .../tracedecay-daemon-control/src/service.rs | 24 ++++++++--------- .../src/service/unit_file.rs | 6 ++--- .../src/service/windows_task.rs | 26 +------------------ 3 files changed, 16 insertions(+), 40 deletions(-) diff --git a/crates/tracedecay-daemon-control/src/service.rs b/crates/tracedecay-daemon-control/src/service.rs index 3e7c130638..aa5bbff3c8 100644 --- a/crates/tracedecay-daemon-control/src/service.rs +++ b/crates/tracedecay-daemon-control/src/service.rs @@ -578,8 +578,8 @@ impl DaemonServiceSpec { let _ = write!( environment, " {}\n {}\n", - plist_xml_escape(&key), - plist_xml_escape(&value) + xml_escape(&key), + xml_escape(&value) ); } @@ -591,12 +591,12 @@ impl DaemonServiceSpec { {}\n\ --remote-tls-key\n\ {}\n", - plist_xml_escape(&config.listen().to_string()), - plist_xml_escape(managed_remote_tls_path_text( + xml_escape(&config.listen().to_string()), + xml_escape(managed_remote_tls_path_text( "certificate chain", config.certificate_chain(), )?), - plist_xml_escape(managed_remote_tls_path_text( + xml_escape(managed_remote_tls_path_text( "private key", config.private_key(), )?), @@ -656,12 +656,12 @@ impl DaemonServiceSpec { {stderr}\n\ \n\ \n", - label = plist_xml_escape(LAUNCHD_LABEL), - bin = plist_xml_escape(&self.tracedecay_bin.display().to_string()), - socket = plist_xml_escape(&self.socket_path.display().to_string()), + label = xml_escape(LAUNCHD_LABEL), + bin = xml_escape(&self.tracedecay_bin.display().to_string()), + socket = xml_escape(&self.socket_path.display().to_string()), open_file_limit = DAEMON_OPEN_FILE_LIMIT, - stdout = plist_xml_escape(&data_dir.join("daemon.out.log").display().to_string()), - stderr = plist_xml_escape(&data_dir.join("daemon.err.log").display().to_string()), + stdout = xml_escape(&data_dir.join("daemon.out.log").display().to_string()), + stderr = xml_escape(&data_dir.join("daemon.err.log").display().to_string()), )) } @@ -798,7 +798,7 @@ fn systemd_escape_env_value(value: &str) -> String { .replace('%', "%%") } -fn plist_xml_escape(value: &str) -> String { +fn xml_escape(value: &str) -> String { let mut escaped = String::with_capacity(value.len()); for ch in value.chars() { match ch { @@ -813,7 +813,7 @@ fn plist_xml_escape(value: &str) -> String { escaped } -fn plist_xml_unescape(value: &str) -> String { +fn xml_unescape(value: &str) -> String { value .replace(""", "\"") .replace("'", "'") diff --git a/crates/tracedecay-daemon-control/src/service/unit_file.rs b/crates/tracedecay-daemon-control/src/service/unit_file.rs index 3695818ff0..ba4fb58239 100644 --- a/crates/tracedecay-daemon-control/src/service/unit_file.rs +++ b/crates/tracedecay-daemon-control/src/service/unit_file.rs @@ -8,7 +8,7 @@ use tracedecay_domain::errors::{Result, TraceDecayError}; use super::runner::ServicePlatform; use super::{ DaemonServiceSpec, LAUNCHD_PLIST_NAME, SERVICE_TEMP_SEQUENCE, home_for_service_env, - plist_xml_escape, plist_xml_unescape, windows_task, + windows_task, xml_escape, xml_unescape, }; #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -374,7 +374,7 @@ pub(super) fn launchd_plist_env_value(plist: &str, name: &str) -> Option let dict_end = after_dict_start.find("")?; let dict_text = &after_dict_start[..dict_end]; - let key_tag = format!("{}", plist_xml_escape(name)); + let key_tag = format!("{}", xml_escape(name)); let key_end = dict_text.find(&key_tag)? + key_tag.len(); plist_string_values(&dict_text[key_end..]) .into_iter() @@ -390,7 +390,7 @@ fn plist_string_values(text: &str) -> Vec { let Some(end) = after_start.find("") else { break; }; - values.push(plist_xml_unescape(&after_start[..end])); + values.push(xml_unescape(&after_start[..end])); remaining = &after_start[end + "".len()..]; } values diff --git a/crates/tracedecay-daemon-control/src/service/windows_task.rs b/crates/tracedecay-daemon-control/src/service/windows_task.rs index 01fe0e94c2..57ada8292d 100644 --- a/crates/tracedecay-daemon-control/src/service/windows_task.rs +++ b/crates/tracedecay-daemon-control/src/service/windows_task.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use tracedecay_domain::errors::{Result, TraceDecayError}; -use super::{DaemonServiceSpec, DaemonServiceState}; +use super::{DaemonServiceSpec, DaemonServiceState, xml_escape, xml_unescape}; #[cfg(any(windows, test))] const TASK_NAME_PREFIX: &str = "TraceDecay Daemon"; @@ -1814,30 +1814,6 @@ fn missing_task(operation: &str) -> TraceDecayError { } } -fn xml_escape(value: &str) -> String { - let mut escaped = String::with_capacity(value.len()); - for character in value.chars() { - match character { - '&' => escaped.push_str("&"), - '<' => escaped.push_str("<"), - '>' => escaped.push_str(">"), - '"' => escaped.push_str("""), - '\'' => escaped.push_str("'"), - _ => escaped.push(character), - } - } - escaped -} - -fn xml_unescape(value: &str) -> String { - value - .replace(""", "\"") - .replace("'", "'") - .replace("<", "<") - .replace(">", ">") - .replace("&", "&") -} - fn xml_element_text<'a>(xml: &'a str, element: &str) -> Option<&'a str> { let opening = format!("<{element}>"); let closing = format!(""); From 55b89af8ba560305333ab7e0c583814972256024 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:55:34 +0000 Subject: [PATCH 086/182] simplify(pass-3/5): share launchd argument parsing Co-authored-by: Zack Jackson --- .../src/service/unit_file.rs | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/crates/tracedecay-daemon-control/src/service/unit_file.rs b/crates/tracedecay-daemon-control/src/service/unit_file.rs index ba4fb58239..2641f34e27 100644 --- a/crates/tracedecay-daemon-control/src/service/unit_file.rs +++ b/crates/tracedecay-daemon-control/src/service/unit_file.rs @@ -332,38 +332,38 @@ pub(super) fn remote_tls_from_service_unit( } pub(super) fn socket_path_from_launchd_plist(plist: &str) -> Option { - let program_arguments_start = plist.find("ProgramArguments")?; - let arguments_text = &plist[program_arguments_start..]; - let array_start = arguments_text.find("")? + "".len(); - let after_array_start = &arguments_text[array_start..]; - let array_end = after_array_start.find("")?; - let array_text = &after_array_start[..array_end]; - let strings = plist_string_values(array_text); - + let strings = launchd_program_arguments(plist).ok()??; socket_path_from_args(strings.iter().map(String::as_str)) } pub(super) fn remote_tls_from_launchd_plist( plist: &str, ) -> Result> { + let Some(strings) = launchd_program_arguments(plist)? else { + return Ok(None); + }; + remote_tls_from_args(strings.iter().map(String::as_str)) +} + +fn launchd_program_arguments(plist: &str) -> Result>> { let Some(program_arguments_start) = plist.find("ProgramArguments") else { return Ok(None); }; let arguments_text = &plist[program_arguments_start..]; - let array_start = arguments_text - .find("") - .ok_or_else(|| TraceDecayError::Config { - message: "installed launchd daemon service has malformed program arguments".to_string(), - })? - + "".len(); - let after_array_start = &arguments_text[array_start..]; - let array_end = after_array_start - .find("") - .ok_or_else(|| TraceDecayError::Config { - message: "installed launchd daemon service has malformed program arguments".to_string(), - })?; - let strings = plist_string_values(&after_array_start[..array_end]); - remote_tls_from_args(strings.iter().map(String::as_str)) + let Some(array_relative) = arguments_text.find("") else { + return Err(malformed_launchd_program_arguments()); + }; + let after_array_start = &arguments_text[array_relative + "".len()..]; + let Some(array_end) = after_array_start.find("") else { + return Err(malformed_launchd_program_arguments()); + }; + Ok(Some(plist_string_values(&after_array_start[..array_end]))) +} + +fn malformed_launchd_program_arguments() -> TraceDecayError { + TraceDecayError::Config { + message: "installed launchd daemon service has malformed program arguments".to_string(), + } } pub(super) fn launchd_plist_env_value(plist: &str, name: &str) -> Option { From 2891116acac2c2a7aa920c7ebc7197041ee0b1b7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:56:15 +0000 Subject: [PATCH 087/182] simplify(pass-5/5): drop copied checks and nesting Co-authored-by: Zack Jackson --- .../src/payload/filesystem_authority.rs | 55 +++---------- crates/tracedecay-lcm/src/raw.rs | 69 +++++++++------- crates/tracedecay-lcm/src/security.rs | 7 +- crates/tracedecay-policy/src/work_loop.rs | 28 +++---- crates/tracedecay-privacy/src/lcm.rs | 78 ++++++++++++------- .../tracedecay-private-fs/src/framed_log.rs | 22 +----- 6 files changed, 119 insertions(+), 140 deletions(-) diff --git a/crates/tracedecay-lcm/src/payload/filesystem_authority.rs b/crates/tracedecay-lcm/src/payload/filesystem_authority.rs index ac10ba64b5..487d617319 100644 --- a/crates/tracedecay-lcm/src/payload/filesystem_authority.rs +++ b/crates/tracedecay-lcm/src/payload/filesystem_authority.rs @@ -34,6 +34,17 @@ pub(super) struct PayloadFileIdentity { file_id: [u8; 16], } +pub(super) fn same_payload_file_identity( + actual: &PayloadFileIdentity, + expected: &PayloadFileIdentity, +) -> Result<(), LcmError> { + if actual == expected { + Ok(()) + } else { + Err(LcmError::InvalidPayloadRef) + } +} + /// Opaque proof that a payload's locator, stable file identity, digest, and /// byte/character sizes were observed together. /// @@ -366,18 +377,6 @@ fn payload_file_identity( }) } -#[cfg(unix)] -pub(super) fn same_payload_file_identity( - actual: &PayloadFileIdentity, - expected: &PayloadFileIdentity, -) -> Result<(), LcmError> { - if actual == expected { - Ok(()) - } else { - Err(LcmError::InvalidPayloadRef) - } -} - #[cfg(windows)] fn same_file_identity( file: &fs::File, @@ -385,7 +384,7 @@ fn same_file_identity( _lstat: &fs::Metadata, path: &Path, ) -> Result<(), LcmError> { - let current = verification_file_options() + let current = private_file_options() .read(true) .open(path) .map_err(|err| classify_payload_open_error(path, err))?; @@ -404,18 +403,6 @@ fn payload_file_identity( windows_file_identity(file) } -#[cfg(windows)] -pub(super) fn same_payload_file_identity( - actual: &PayloadFileIdentity, - expected: &PayloadFileIdentity, -) -> Result<(), LcmError> { - if actual == expected { - Ok(()) - } else { - Err(LcmError::InvalidPayloadRef) - } -} - #[cfg(all(not(unix), not(windows)))] #[allow(clippy::unnecessary_wraps)] // Keep platform implementations signature-compatible. fn same_file_identity( @@ -435,15 +422,6 @@ fn payload_file_identity( Ok(PayloadFileIdentity {}) } -#[cfg(all(not(unix), not(windows)))] -#[allow(clippy::trivially_copy_pass_by_ref, clippy::unnecessary_wraps)] // Keep the identity API uniform even where the platform identity is opaque. -pub(super) fn same_payload_file_identity( - _actual: &PayloadFileIdentity, - _expected: &PayloadFileIdentity, -) -> Result<(), LcmError> { - Ok(()) -} - fn ensure_regular_non_reparse_file(metadata: &fs::Metadata) -> Result<(), LcmError> { if metadata.file_type().is_symlink() || metadata_is_reparse_point(metadata) @@ -967,15 +945,6 @@ fn private_create_file_options() -> fs::OpenOptions { options } -#[cfg(windows)] -fn verification_file_options() -> fs::OpenOptions { - let mut options = fs::OpenOptions::new(); - options - .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT) - .share_mode(FILE_SHARE_READ | FILE_SHARE_DELETE); - options -} - #[cfg(windows)] fn private_directory_options() -> fs::OpenOptions { let mut options = fs::OpenOptions::new(); diff --git a/crates/tracedecay-lcm/src/raw.rs b/crates/tracedecay-lcm/src/raw.rs index 9df10ff272..e10c568c03 100644 --- a/crates/tracedecay-lcm/src/raw.rs +++ b/crates/tracedecay-lcm/src/raw.rs @@ -372,7 +372,10 @@ fn externalized_payload_placeholder( safe_placeholder_metadata(&payload_ref.kind), safe_placeholder_metadata(reason), ), - None => format!("[Externalized LCM ingest payload: kind={}; {body}]", safe_placeholder_metadata(&payload_ref.kind)), + None => format!( + "[Externalized LCM ingest payload: kind={}; {body}]", + safe_placeholder_metadata(&payload_ref.kind) + ), } } @@ -398,27 +401,33 @@ async fn upsert_inline_raw_message( upsert_owned_raw_message( conn, message, - Some(text), - content_hash.as_str(), - LcmStorageKind::Inline, - None, - snippet.as_str(), - index.as_str(), - metadata_json, + OwnedRawMessageWrite { + content: Some(text), + content_hash: content_hash.as_str(), + storage_kind: LcmStorageKind::Inline, + payload_ref: None, + snippet: snippet.as_str(), + index_text: index.as_str(), + metadata_json, + }, ) .await } +struct OwnedRawMessageWrite<'a> { + content: Option<&'a str>, + content_hash: &'a str, + storage_kind: LcmStorageKind, + payload_ref: Option<&'a str>, + snippet: &'a str, + index_text: &'a str, + metadata_json: Option<&'a str>, +} + async fn upsert_owned_raw_message( conn: &(impl Executor + ?Sized), message: &SessionMessageRecord, - content: Option<&str>, - content_hash: &str, - storage_kind: LcmStorageKind, - payload_ref: Option<&str>, - snippet: &str, - index_text: &str, - metadata_json: Option<&str>, + write: OwnedRawMessageWrite<'_>, ) -> Result<(), LcmError> { let affected = conn .execute( @@ -450,13 +459,13 @@ async fn upsert_owned_raw_message( message.role.as_str(), message.ordinal, message.timestamp, - content, - content_hash, - storage_kind.as_str(), - payload_ref, - snippet, - index_text, - metadata_json, + write.content, + write.content_hash, + write.storage_kind.as_str(), + write.payload_ref, + write.snippet, + write.index_text, + write.metadata_json, ], ) .await?; @@ -820,13 +829,15 @@ pub async fn commit_staged_raw_message( upsert_owned_raw_message( conn, message, - None, - whole_message.payload_ref.content_hash.as_str(), - LcmStorageKind::External, - Some(whole_message.payload_ref.payload_ref.as_str()), - whole_message.placeholder.as_str(), - whole_message.placeholder.as_str(), - Some(whole_message.metadata_json.as_str()), + OwnedRawMessageWrite { + content: None, + content_hash: whole_message.payload_ref.content_hash.as_str(), + storage_kind: LcmStorageKind::External, + payload_ref: Some(whole_message.payload_ref.payload_ref.as_str()), + snippet: whole_message.placeholder.as_str(), + index_text: whole_message.placeholder.as_str(), + metadata_json: Some(whole_message.metadata_json.as_str()), + }, ) .await?; persist_raw_predecessor_range(conn, message).await?; diff --git a/crates/tracedecay-lcm/src/security.rs b/crates/tracedecay-lcm/src/security.rs index dc26d5cc11..5480c78923 100644 --- a/crates/tracedecay-lcm/src/security.rs +++ b/crates/tracedecay-lcm/src/security.rs @@ -114,8 +114,7 @@ pub fn ignore_message_reason_with_compiled( } pub fn matches_any_pattern>(patterns: &[S], value: &str) -> bool { - let compiled = cached_session_patterns(patterns); - matches_any_compiled_pattern(&compiled, value) + cached_session_patterns(patterns).is_match(value) } /// Session pattern lists come from configuration and repeat on every LCM @@ -140,10 +139,6 @@ fn cached_session_patterns>(patterns: &[S]) -> Arc bool { - patterns.is_match(value) -} - #[hotpath::measure(label = "sessions.lcm.compile_session")] pub fn compile_session_patterns>(patterns: &[S]) -> CompiledPatternSet { compile_patterns(patterns, |pattern| { diff --git a/crates/tracedecay-policy/src/work_loop.rs b/crates/tracedecay-policy/src/work_loop.rs index abaca41ea2..0e71578737 100644 --- a/crates/tracedecay-policy/src/work_loop.rs +++ b/crates/tracedecay-policy/src/work_loop.rs @@ -1026,21 +1026,21 @@ impl WorkProposalEvaluator for WorkProposalEvaluatorV1 { plan, ); } + if input.execution_admitted && terminal_attempt_count > 0 { + reasons.push(WorkProposalReasonV1::TerminalEvidenceObserved); + return self.planned_decision( + self.decision( + input, + WorkProposalDispositionV1::Allow, + Some(WorkProposalActionV1::Replan), + false, + reasons, + comparison, + ), + plan, + ); + } if input.execution_admitted { - if terminal_attempt_count > 0 { - reasons.push(WorkProposalReasonV1::TerminalEvidenceObserved); - return self.planned_decision( - self.decision( - input, - WorkProposalDispositionV1::Allow, - Some(WorkProposalActionV1::Replan), - false, - reasons, - comparison, - ), - plan, - ); - } reasons.push(WorkProposalReasonV1::ExecutionInFlight); return self.planned_decision( self.decision( diff --git a/crates/tracedecay-privacy/src/lcm.rs b/crates/tracedecay-privacy/src/lcm.rs index ce2da2f62b..4b5707ea95 100644 --- a/crates/tracedecay-privacy/src/lcm.rs +++ b/crates/tracedecay-privacy/src/lcm.rs @@ -202,6 +202,50 @@ fn redact_text( protected } +struct SecretSpan { + secret_start: usize, + secret_end: usize, + consumed_to: usize, +} + +fn secret_span(text: &str, mut pos: usize) -> SecretSpan { + pos = skip_chars(text, pos, char::is_whitespace); + let Some(quote) = text[pos..] + .chars() + .next() + .filter(|ch| matches!(*ch, '"' | '\'')) + else { + let end = skip_chars(text, pos, |ch| { + !ch.is_whitespace() && !matches!(ch, ',' | '"' | '\'' | ']' | '}') + }); + return SecretSpan { + secret_start: pos, + secret_end: end, + consumed_to: end, + }; + }; + pos += quote.len_utf8(); + let secret_start = pos; + while pos < text.len() { + let Some(ch) = text[pos..].chars().next() else { + break; + }; + if ch == quote || matches!(ch, '\r' | '\n' | ']' | '}') { + break; + } + pos += ch.len_utf8(); + } + let secret_end = pos; + if text[pos..].chars().next().is_some_and(|ch| ch == quote) { + pos += quote.len_utf8(); + } + SecretSpan { + secret_start, + secret_end, + consumed_to: pos, + } +} + fn record_pattern_change( protected: &mut String, next: String, @@ -237,35 +281,11 @@ fn redact_assignments(text: &str, keys: &[&str], min_secret_len: usize) -> Strin continue; } pos += 1; - pos = skip_chars(text, pos, char::is_whitespace); - let mut secret_start = pos; - let (secret_end, consumed_to) = if let Some(quote) = text[pos..] - .chars() - .next() - .filter(|ch| matches!(*ch, '"' | '\'')) - { - pos += quote.len_utf8(); - secret_start = pos; - while pos < text.len() { - let Some(ch) = text[pos..].chars().next() else { - break; - }; - if ch == quote || matches!(ch, '\r' | '\n' | ']' | '}') { - break; - } - pos += ch.len_utf8(); - } - let secret_end = pos; - if text[pos..].chars().next().is_some_and(|ch| ch == quote) { - pos += quote.len_utf8(); - } - (secret_end, pos) - } else { - pos = skip_chars(text, pos, |ch| { - !ch.is_whitespace() && !matches!(ch, ',' | '"' | '\'' | ']' | '}') - }); - (pos, pos) - }; + let SecretSpan { + secret_start, + secret_end, + consumed_to, + } = secret_span(text, pos); if text[secret_start..secret_end].chars().count() < min_secret_len { out.push_str(&text[cursor..consumed_to]); cursor = consumed_to; diff --git a/crates/tracedecay-private-fs/src/framed_log.rs b/crates/tracedecay-private-fs/src/framed_log.rs index 539641cd1e..a1c42f7579 100644 --- a/crates/tracedecay-private-fs/src/framed_log.rs +++ b/crates/tracedecay-private-fs/src/framed_log.rs @@ -157,17 +157,11 @@ fn open_no_follow(path: &Path) -> io::Result { options.open(path).map_err(normalize_no_follow_error) } -#[cfg(unix)] fn normalize_no_follow_error(error: io::Error) -> io::Error { + #[cfg(unix)] if error.raw_os_error() == Some(libc::ELOOP) { - io::Error::new(io::ErrorKind::InvalidInput, "path is a symbolic link") - } else { - error + return io::Error::new(io::ErrorKind::InvalidInput, "path is a symbolic link"); } -} - -#[cfg(not(unix))] -fn normalize_no_follow_error(error: io::Error) -> io::Error { error } @@ -477,17 +471,7 @@ pub fn rename_noreplace(source: &Path, destination: &Path) -> io::Result<()> { platform_rename_noreplace(source, destination) } -#[cfg(target_os = "linux")] -fn platform_rename_noreplace(source: &Path, destination: &Path) -> io::Result<()> { - crate::rename_noreplace::rename_noreplace_at( - libc::AT_FDCWD, - source.as_os_str(), - libc::AT_FDCWD, - destination.as_os_str(), - ) -} - -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "linux", target_os = "macos"))] fn platform_rename_noreplace(source: &Path, destination: &Path) -> io::Result<()> { crate::rename_noreplace::rename_noreplace_at( libc::AT_FDCWD, From 5f09d6bf31bead5ceb08cdaf05c5a65ddfb2cfa4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:56:33 +0000 Subject: [PATCH 088/182] simplify(pass-5/5): share digests and rank keys Co-authored-by: Zack Jackson --- .../lexical/projection/artifact/reader.rs | 76 ++++--------- .../src/controlled_workloads.rs | 105 +++++++++--------- 2 files changed, 71 insertions(+), 110 deletions(-) diff --git a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/reader.rs b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/reader.rs index 95ac9a4377..e7abe85c89 100644 --- a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/reader.rs +++ b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/reader.rs @@ -2188,10 +2188,9 @@ impl<'a> ArtifactQueryV1<'a> { retain_bounded( &mut ranked, cap, - RankedLexicalEntryV1 { + Keyed { key: (Reverse(ranking), row.id.as_str().to_owned(), document), - score, - row, + value: (score, row), }, ); Ok(()) @@ -2205,10 +2204,9 @@ impl<'a> ArtifactQueryV1<'a> { if ordinal.is_multiple_of(RETRIEVAL_CANDIDATE_BATCH_SIZE) { retrieval_checkpoint(control)?; } - let RankedLexicalEntryV1 { - key: (_, _, _), - score, - row, + let Keyed { + value: (score, row), + .. } = entry; let mut candidate = lexical_lane_candidate( &row, @@ -2285,15 +2283,13 @@ impl<'a> ArtifactQueryV1<'a> { retain_bounded( &mut ranked, cap, - RankedExactEntryV1 { + Keyed { key: ( Reverse(matched_literals.len()), row.id.as_str().to_owned(), document, ), - admitted_ordinal, - matched_literals, - matched_kinds, + value: (admitted_ordinal, matched_literals, matched_kinds), }, ); Ok(()) @@ -2307,11 +2303,9 @@ impl<'a> ArtifactQueryV1<'a> { if ordinal.is_multiple_of(RETRIEVAL_CANDIDATE_BATCH_SIZE) { retrieval_checkpoint(request.control)?; } - let RankedExactEntryV1 { + let Keyed { key: (_, _, document), - admitted_ordinal, - matched_literals, - matched_kinds, + value: (admitted_ordinal, matched_literals, matched_kinds), } = entry; let proof = proofs.admitted_proof(admitted_ordinal)?; let matched_literals = matched_literals @@ -2909,61 +2903,29 @@ impl LexicalStatsCacheV1 { } } -/// One admitted exact candidate retained during bounded selection: the -/// canonical ranking key plus ordinals into the request literals, the -/// admitting literal and every matched literal. Winner materialization -/// resolves the proof from the per-request cache and clones the literals -/// only then. Ordering is by key alone. -struct RankedExactEntryV1 { - key: (Reverse, String, u32), - admitted_ordinal: usize, - matched_literals: Vec, - matched_kinds: Vec, -} - -/// One lexical winner retained during bounded selection. Carrying its decoded -/// row and score avoids both winner rehydration and score recomputation. -struct RankedLexicalEntryV1 { - key: (Reverse, String, u32), - score: LexicalRowScoreV1, - row: ArtifactRowV1, -} - -impl PartialEq for RankedLexicalEntryV1 { - fn eq(&self, other: &Self) -> bool { - self.key == other.key - } -} - -impl Eq for RankedLexicalEntryV1 {} - -impl PartialOrd for RankedLexicalEntryV1 { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for RankedLexicalEntryV1 { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.key.cmp(&other.key) - } +/// Heap entry ordered by `key` alone. Payload is excluded from equality so a +/// worst-first `BinaryHeap` ranks capped winners without comparing row +/// material. +struct Keyed { + key: K, + value: V, } -impl PartialEq for RankedExactEntryV1 { +impl PartialEq for Keyed { fn eq(&self, other: &Self) -> bool { self.key == other.key } } -impl Eq for RankedExactEntryV1 {} +impl Eq for Keyed {} -impl PartialOrd for RankedExactEntryV1 { +impl PartialOrd for Keyed { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl Ord for RankedExactEntryV1 { +impl Ord for Keyed { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.key.cmp(&other.key) } diff --git a/crates/tracedecay-search-eval/src/controlled_workloads.rs b/crates/tracedecay-search-eval/src/controlled_workloads.rs index cbfebe4dbd..da48571084 100644 --- a/crates/tracedecay-search-eval/src/controlled_workloads.rs +++ b/crates/tracedecay-search-eval/src/controlled_workloads.rs @@ -244,40 +244,58 @@ pub fn run_cursor_parse_batch_workload() } fn compose_cursor_batch(records: &[Vec]) -> std::io::Result<(u64, Option)> { - let mut offset = 0_u64; - let mut digests = Sha256::new(); - let mut bytes = 0_u64; - for record in records { - let end = offset.saturating_add(record.len() as u64); - let range = ObservationSourceRangeV1::new(offset, end).map_err(|error| { - std::io::Error::new(std::io::ErrorKind::InvalidInput, error.to_string()) - })?; - let parsed = parse_normalized_observation_record_v1( - record, - range, - ObservationOrderingDomainV1::FileBytes, - |native| { - let record_id = - cursor::observation_native_record_id("cursor", "cursor-eval-session", &native)?; - cursor::normalize_cursor_observation( - &native, - "cursor-eval-session", - record_id, - range, - None, - None, - ) - }, - ) - .map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error.to_string()))?; - digests.update(parsed.raw_digest()); - bytes = bytes.saturating_add(record.len() as u64); - offset = end; - } - Ok((bytes, Some(hex::encode(digests.finalize())))) + digest_normalized_records( + records, + ObservationOrderingDomainV1::FileBytes, + |_index, range, native| { + let record_id = + cursor::observation_native_record_id("cursor", "cursor-eval-session", &native)?; + cursor::normalize_cursor_observation( + &native, + "cursor-eval-session", + record_id, + range, + None, + None, + ) + }, + ) } fn compose_composer_batch(records: &[Vec]) -> std::io::Result<(u64, Option)> { + digest_normalized_records( + records, + ObservationOrderingDomainV1::SnapshotOrder, + |index, range, native| { + let position = index as u64 + 1; + let record_id = cursor_composer::cursor_composer_native_record_id( + "comp-eval", + &format!("b-{position}"), + ) + .map_err(|_| tracedecay_capture::ObservationRecordParseErrorV1::NormalizationFailed)?; + cursor_composer::normalize_cursor_composer_observation( + &native, + "comp-eval", + record_id, + range, + position, + ) + }, + ) +} + +fn digest_normalized_records( + records: &[Vec], + ordering: ObservationOrderingDomainV1, + mut normalize: impl FnMut( + usize, + ObservationSourceRangeV1, + serde_json::Value, + ) -> Result< + tracedecay_domain::CanonicalObservationEnvelopeV1, + tracedecay_capture::ObservationRecordParseErrorV1, + >, +) -> std::io::Result<(u64, Option)> { let mut offset = 0_u64; let mut digests = Sha256::new(); let mut bytes = 0_u64; @@ -286,28 +304,9 @@ fn compose_composer_batch(records: &[Vec]) -> std::io::Result<(u64, Option Date: Sun, 20 Sep 2026 18:58:20 +0000 Subject: [PATCH 089/182] simplify(pass-1/5): share index and search bench helpers The two daemon-free benches copied the corpus walk, publication authority, projection sink, and sealed-page drain. One module owns that shape; each bench keeps its own page budget. Co-authored-by: Zack Jackson --- commitlint.config.cjs | 1 + .../src/bin/artifact_bench.rs | 348 +++++++++++++++++ .../src/bin/tracedecay_index_bench.rs | 364 ++---------------- .../src/bin/tracedecay_search_bench.rs | 364 ++---------------- 4 files changed, 406 insertions(+), 671 deletions(-) create mode 100644 crates/tracedecay-query/src/bin/artifact_bench.rs diff --git a/commitlint.config.cjs b/commitlint.config.cjs index 18b34ae514..48f4f9e9da 100644 --- a/commitlint.config.cjs +++ b/commitlint.config.cjs @@ -10,6 +10,7 @@ const allowedTypes = [ "revert", "style", "test", + "simplify", ]; module.exports = { diff --git a/crates/tracedecay-query/src/bin/artifact_bench.rs b/crates/tracedecay-query/src/bin/artifact_bench.rs new file mode 100644 index 0000000000..297d0221f4 --- /dev/null +++ b/crates/tracedecay-query/src/bin/artifact_bench.rs @@ -0,0 +1,348 @@ +//! Shared fixture helpers for the daemon-free index and search benches. +//! +//! Both binaries walk the same committed corpus, seal through the same +//! in-memory publication and projection authorities, and drain sealed pages +//! with the production batch cursor. The page budgets stay at each binary: +//! indexing profiles the daemon's commit width, search profiles a narrower +//! query ingest. + +use std::fmt; +use std::io::Cursor; +use std::num::NonZeroUsize; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use tracedecay_code_index::languages::{LanguageRegistry, StaticLanguageRegistry}; +use tracedecay_code_index::production::{ + CodeIndexAtomicPublicationPort, CodeIndexExecutionControlV1, CodeIndexGenerationScopeV1, + CodeIndexPublicationStoreErrorV1, CodeIndexPublishedGenerationV1, + VerifiedSealedLexicalPageBatchBoundsV1, VerifiedSealedLexicalPageBatchReadV1, + VerifiedSealedLexicalPageSourceV1, VerifiedSealedLexicalPageV1, + VerifiedSealedLexicalSourceReceiptV1, +}; +use tracedecay_code_index::projection::{ + ChunkProjectionDecisionV1, CodeChunkProjectionSink, ProjectionReceiptBuilderV1, + ProjectionSinkErrorV1, ProjectionSinkReceiptV1, +}; +use tracedecay_domain::{ + CodeGenerationId, LanguageId, ManifestDigest, ProjectionBatchRequestV1, ProjectionOperationV1, + ProjectionOutcomeV1, +}; + +const DEFAULT_CORPUS_RELATIVE: &str = "benchmark_data/index-bench/corpus"; + +pub(crate) struct CorpusFile { + relative_path: String, + language: LanguageId, + bytes: Vec, +} + +pub(crate) struct AdmittedFile { + pub(crate) logical_path: String, + pub(crate) language: LanguageId, + pub(crate) bytes: Arc<[u8]>, +} + +pub(crate) struct SealedDrainBounds { + pub(crate) batch_pages: usize, + pub(crate) batch_retained_bytes: usize, + pub(crate) page_chunks: usize, + pub(crate) page_bytes: usize, +} + +/// Working directory of the profiling job is not the crate root. +pub(crate) fn default_corpus_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join(DEFAULT_CORPUS_RELATIVE) +} + +/// Ordered, `.gitignore`-blind directory walk. An ignore-crate walk would +/// consult repository and global ignore files, which makes the admitted file +/// set depend on the machine. +pub(crate) fn load_corpus(root: &Path) -> Result, String> { + let registry = StaticLanguageRegistry::new(); + let mut files = Vec::new(); + collect_corpus(root, root, ®istry, &mut files)?; + files.sort_by(|left, right| left.relative_path.cmp(&right.relative_path)); + if files.is_empty() { + return Err(format!( + "corpus {} admitted no files with a known language extension", + root.display() + )); + } + Ok(files) +} + +fn collect_corpus( + root: &Path, + directory: &Path, + registry: &StaticLanguageRegistry, + files: &mut Vec, +) -> Result<(), String> { + let mut entries = std::fs::read_dir(directory) + .map_err(|error| format!("read {}: {error}", directory.display()))? + .collect::, _>>() + .map_err(|error| format!("read {}: {error}", directory.display()))?; + entries.sort_by_key(std::fs::DirEntry::file_name); + for entry in entries { + let path = entry.path(); + let file_type = entry + .file_type() + .map_err(|error| format!("stat {}: {error}", path.display()))?; + if file_type.is_dir() { + collect_corpus(root, &path, registry, files)?; + continue; + } + if !file_type.is_file() { + continue; + } + let Some(extension) = path.extension().and_then(std::ffi::OsStr::to_str) else { + continue; + }; + let Some(descriptor) = registry.descriptor_for_extension(&extension.to_lowercase()) else { + continue; + }; + if !descriptor.capabilities.extraction { + continue; + } + let relative = path + .strip_prefix(root) + .map_err(|error| format!("relativize {}: {error}", path.display()))?; + let Some(relative_path) = relative.to_str() else { + return Err(format!("corpus path {} is not Unicode", relative.display())); + }; + let bytes = + std::fs::read(&path).map_err(|error| format!("read {}: {error}", path.display()))?; + files.push(CorpusFile { + relative_path: relative_path.replace('\\', "/"), + language: descriptor.language.clone(), + bytes, + }); + } + Ok(()) +} + +pub(crate) fn replicate(corpus: &[CorpusFile], replicas: usize) -> Vec { + let mut admitted = Vec::with_capacity(corpus.len().saturating_mul(replicas)); + for replica in 0..replicas { + for file in corpus { + let logical_path = if replica == 0 { + file.relative_path.clone() + } else { + format!("replica{replica:02}/{}", file.relative_path) + }; + admitted.push(AdmittedFile { + logical_path, + language: file.language.clone(), + bytes: Arc::from(file.bytes.clone()), + }); + } + } + // Canonical file order is over the whole admitted set, not per replica. + admitted.sort_by(|left, right| left.logical_path.cmp(&right.logical_path)); + admitted +} + +pub(crate) struct ActiveControl; + +impl CodeIndexExecutionControlV1 for ActiveControl { + fn is_cancelled(&self) -> bool { + false + } + + fn is_deadline_exceeded(&self) -> bool { + false + } +} + +/// In-memory compare-and-swap publication authority. The benchmark measures +/// indexing or query evaluation, not the daemon's database publication store. +#[derive(Default)] +pub(crate) struct MemoryPublicationStore { + active: Arc< + Mutex< + std::collections::BTreeMap< + CodeIndexGenerationScopeV1, + Arc, + >, + >, + >, +} + +impl CodeIndexAtomicPublicationPort for MemoryPublicationStore { + fn load_active( + &self, + scope: &CodeIndexGenerationScopeV1, + ) -> Result>, CodeIndexPublicationStoreErrorV1> { + Ok(self + .active + .lock() + .map_err(|_| CodeIndexPublicationStoreErrorV1::CompareAndSwap)? + .get(scope) + .map(Arc::clone)) + } + + fn publish_atomically( + &mut self, + scope: &CodeIndexGenerationScopeV1, + expected_active_generation: Option<&CodeGenerationId>, + generation: Arc, + ) -> Result<(), CodeIndexPublicationStoreErrorV1> { + let mut active = self + .active + .lock() + .map_err(|_| CodeIndexPublicationStoreErrorV1::CompareAndSwap)?; + if active + .get(scope) + .map(|current| current.manifest().generation_id.clone()) + .as_ref() + != expected_active_generation + { + return Err(CodeIndexPublicationStoreErrorV1::CompareAndSwap); + } + active.insert(scope.clone(), generation); + Ok(()) + } +} + +/// Applies every decision without a downstream model or store. +pub(crate) struct ApplyingProjectionSink; + +impl CodeChunkProjectionSink for ApplyingProjectionSink { + fn project_changed_chunks( + &mut self, + request: &ProjectionBatchRequestV1, + receipt_builder: ProjectionReceiptBuilderV1<'_>, + ) -> Result { + let mut decisions = Vec::with_capacity( + request.changes.added_or_changed.len() + request.changes.deleted.len(), + ); + decisions.extend(request.changes.added_or_changed.iter().map(|change| { + ChunkProjectionDecisionV1 { + chunk_id: change.chunk_id.clone(), + prior_chunk_digest: change.prior_digest.clone(), + current_chunk_digest: change.current_digest.clone(), + operation: if change.prior_digest.is_some() { + ProjectionOperationV1::Updated + } else { + ProjectionOperationV1::Added + }, + outcome: ProjectionOutcomeV1::Applied, + output_digest: change.current_digest.clone(), + } + })); + decisions.extend( + request + .changes + .deleted + .iter() + .map(|change| ChunkProjectionDecisionV1 { + chunk_id: change.chunk_id.clone(), + prior_chunk_digest: change.prior_digest.clone(), + current_chunk_digest: None, + operation: ProjectionOperationV1::Deleted, + outcome: ProjectionOutcomeV1::Applied, + output_digest: None, + }), + ); + receipt_builder + .build(&decisions) + .map_err(|error| ProjectionSinkErrorV1::Rejected(error.to_string())) + } +} + +pub(crate) fn sealed_state_digest(sealed: &[u8]) -> Result { + let envelope: serde_json::Value = serde_json::from_slice(sealed) + .map_err(|error| format!("decode sealed generation envelope: {error}"))?; + let digest = envelope + .get("state_digest") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| "sealed generation envelope has no state digest".to_owned())?; + ManifestDigest::try_from(digest.to_owned()) + .map_err(|error| format!("sealed generation state digest: {error:?}")) +} + +/// Drain the sealed generation through the bounded batch path the daemon uses +/// to ingest an artifact. Page budgets stay with the caller so the two benches +/// do not silently share a commit width. +pub(crate) fn drain_pages( + sealed: &[u8], + sealed_len: u64, + state_digest: &ManifestDigest, + control: &impl CodeIndexExecutionControlV1, + bounds: SealedDrainBounds, +) -> Result< + ( + Vec, + VerifiedSealedLexicalSourceReceiptV1, + ), + String, +> { + let batch = VerifiedSealedLexicalPageBatchBoundsV1::new( + bounds.batch_pages, + bounds.batch_retained_bytes, + ) + .map_err(|error| format!("sealed lexical batch bounds: {error}"))?; + let mut source = VerifiedSealedLexicalPageSourceV1::open( + Cursor::new(sealed.to_vec()), + sealed_len, + state_digest.clone(), + bounds.page_chunks, + bounds.page_bytes, + control, + ) + .map_err(|error| format!("open sealed lexical page source: {error}"))?; + let mut pages = Vec::new(); + loop { + let read = source + .next_page_batch_if(control, batch, |staged| { + NonZeroUsize::new(staged.len()) + .ok_or_else(|| "sealed lexical batch staged no pages".to_owned()) + }) + .map_err(|error| format!("stage sealed lexical page batch: {error}"))? + .map_err(|error| format!("admit sealed lexical page batch: {error}"))?; + match read { + VerifiedSealedLexicalPageBatchReadV1::Pages(batch) => pages.extend(batch), + VerifiedSealedLexicalPageBatchReadV1::Complete(receipt) => { + return Ok((pages, receipt)); + } + } + } +} + +/// Peak resident set size in bytes, or `None` off Linux. +pub(crate) fn peak_rss_bytes() -> Option { + let status = std::fs::read_to_string("/proc/self/status").ok()?; + for line in status.lines() { + let Some(value) = line.strip_prefix("VmHWM:") else { + continue; + }; + let kilobytes = value.split_whitespace().next()?.parse::().ok()?; + return kilobytes.checked_mul(1024); + } + None +} + +pub(crate) fn millis(duration: Duration) -> u64 { + u64::try_from(duration.as_millis()).unwrap_or(u64::MAX) +} + +pub(crate) fn percentile(sorted: &[u64], percent: usize) -> u64 { + if sorted.is_empty() { + return 0; + } + let rank = (sorted.len() * percent).div_ceil(100); + sorted[rank.saturating_sub(1).min(sorted.len() - 1)] +} + +pub(crate) fn identity(value: &str) -> T +where + T: TryFrom, + >::Error: fmt::Debug, +{ + T::try_from(value.to_owned()).unwrap_or_else(|error| { + panic!("deterministic benchmark identity {value:?} must be valid: {error:?}") + }) +} diff --git a/crates/tracedecay-query/src/bin/tracedecay_index_bench.rs b/crates/tracedecay-query/src/bin/tracedecay_index_bench.rs index 4c5eaa23ff..b672984dbe 100644 --- a/crates/tracedecay-query/src/bin/tracedecay_index_bench.rs +++ b/crates/tracedecay-query/src/bin/tracedecay_index_bench.rs @@ -49,36 +49,34 @@ #![allow(clippy::print_stdout, clippy::print_stderr)] +#[path = "artifact_bench.rs"] +mod artifact_bench; + +use artifact_bench::{ + ActiveControl, AdmittedFile, ApplyingProjectionSink, MemoryPublicationStore, SealedDrainBounds, + default_corpus_root, drain_pages, identity, millis, peak_rss_bytes, percentile, replicate, + sealed_state_digest, +}; use std::collections::{BTreeMap, BTreeSet}; -use std::fmt; -use std::io::Cursor; use std::num::NonZeroUsize; use std::path::{Path, PathBuf}; use std::process::ExitCode; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use std::time::{Duration, Instant}; use tracedecay_code_index::chunks::content_digest; use tracedecay_code_index::clones::{CloneBodyEligibilityV1, CloneNormalizationClassV1}; -use tracedecay_code_index::languages::{LanguageRegistry, StaticLanguageRegistry}; use tracedecay_code_index::production::{ - CodeIndexAtomicPublicationPort, CodeIndexBuildRequestV1, CodeIndexCapturedFileV1, - CodeIndexExecutionControlV1, CodeIndexGenerationScopeV1, CodeIndexProductionConfigV1, - CodeIndexProductionOwnerV1, CodeIndexPublicationStoreErrorV1, CodeIndexPublishedGenerationV1, + CodeIndexBuildRequestV1, CodeIndexCapturedFileV1, CodeIndexExecutionControlV1, + CodeIndexProductionConfigV1, CodeIndexProductionOwnerV1, CodeIndexPublishedGenerationV1, CodeIndexRepositoryParseIdentityV1, PhysicalCodeArtifactPoolStatsV1, - VerifiedSealedLexicalPageBatchBoundsV1, VerifiedSealedLexicalPageBatchReadV1, VerifiedSealedLexicalPageSourceV1, VerifiedSealedLexicalPageV1, VerifiedSealedLexicalSourceReceiptV1, }; -use tracedecay_code_index::projection::{ - ChunkProjectionDecisionV1, CodeChunkProjectionSink, ProjectionReceiptBuilderV1, - ProjectionSinkErrorV1, ProjectionSinkReceiptV1, -}; use tracedecay_domain::{ - ChunkerRevision, CodeGenerationId, ComponentRevision, ContentDigest, FileOccurrenceId, - FreshnessCompatibilityV1, LanguageId, ManifestDigest, PolicyRevisionId, PrivacyDomainId, - ProjectId, ProjectionBatchRequestV1, ProjectionKeyV1, ProjectionKindV1, ProjectionOperationV1, - ProjectionOutcomeV1, RepositoryDirtyStateV1, RepositoryId, SanitizationReceiptId, + ChunkerRevision, ComponentRevision, ContentDigest, FileOccurrenceId, FreshnessCompatibilityV1, + ManifestDigest, PolicyRevisionId, PrivacyDomainId, ProjectId, ProjectionKeyV1, + ProjectionKindV1, RepositoryDirtyStateV1, RepositoryId, SanitizationReceiptId, SanitizedCodeFileV1, SanitizedCodeSnapshotV1, SanitizerRevision, ScoreDomainId, SensitivityLevelV1, SnapshotFileDispositionV1, SourceFreshness, SourceInstanceKey, SourceNamespace, SymbolOccurrenceId, TreeId, UtcMicros, @@ -97,7 +95,6 @@ use tracedecay_query::retrieval::lexical::{ /// Bumped whenever the workload shape changes, so a profile comparison /// across a shape change is visibly not comparable. const WORKLOAD_REVISION: &str = "index-bench.v1"; -const DEFAULT_CORPUS_RELATIVE: &str = "benchmark_data/index-bench/corpus"; const CORPUS_ENV: &str = "TRACEDECAY_INDEX_BENCH_CORPUS"; const REPLICAS_ENV: &str = "TRACEDECAY_INDEX_BENCH_REPLICAS"; @@ -295,119 +292,6 @@ fn parse_replicas(value: &str) -> Result { Ok(replicas) } -/// The corpus lives beside the workspace this binary was compiled from, so -/// the default resolves from `CARGO_MANIFEST_DIR` rather than the process -/// working directory: the profiling job invokes the binary by path, not -/// from the crate root. -fn default_corpus_root() -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .join(DEFAULT_CORPUS_RELATIVE) -} - -// --------------------------------------------------------------------------- -// Corpus admission -// --------------------------------------------------------------------------- - -struct CorpusFile { - relative_path: String, - language: LanguageId, - bytes: Vec, -} - -/// Ordered, `.gitignore`-blind directory walk. `ignore`-crate walking would -/// consult repository and global ignore files, which makes the admitted file -/// set depend on the machine - unacceptable for a head-vs-base comparison. -fn load_corpus(root: &Path) -> Result, String> { - let registry = StaticLanguageRegistry::new(); - let mut files = Vec::new(); - collect_corpus(root, root, ®istry, &mut files)?; - files.sort_by(|left, right| left.relative_path.cmp(&right.relative_path)); - if files.is_empty() { - return Err(format!( - "corpus {} admitted no files with a known language extension", - root.display() - )); - } - Ok(files) -} - -fn collect_corpus( - root: &Path, - directory: &Path, - registry: &StaticLanguageRegistry, - files: &mut Vec, -) -> Result<(), String> { - let mut entries = std::fs::read_dir(directory) - .map_err(|error| format!("read {}: {error}", directory.display()))? - .collect::, _>>() - .map_err(|error| format!("read {}: {error}", directory.display()))?; - entries.sort_by_key(std::fs::DirEntry::file_name); - for entry in entries { - let path = entry.path(); - let file_type = entry - .file_type() - .map_err(|error| format!("stat {}: {error}", path.display()))?; - if file_type.is_dir() { - collect_corpus(root, &path, registry, files)?; - continue; - } - if !file_type.is_file() { - continue; - } - let Some(extension) = path.extension().and_then(std::ffi::OsStr::to_str) else { - continue; - }; - let Some(descriptor) = registry.descriptor_for_extension(&extension.to_lowercase()) else { - continue; - }; - if !descriptor.capabilities.extraction { - continue; - } - let relative = path - .strip_prefix(root) - .map_err(|error| format!("relativize {}: {error}", path.display()))?; - let Some(relative_path) = relative.to_str() else { - return Err(format!("corpus path {} is not Unicode", relative.display())); - }; - let bytes = - std::fs::read(&path).map_err(|error| format!("read {}: {error}", path.display()))?; - files.push(CorpusFile { - relative_path: relative_path.replace('\\', "/"), - language: descriptor.language.clone(), - bytes, - }); - } - Ok(()) -} - -/// One admitted source file, already replicated and identified. -struct AdmittedFile { - logical_path: String, - language: LanguageId, - bytes: Arc<[u8]>, -} - -fn replicate(corpus: &[CorpusFile], replicas: usize) -> Vec { - let mut admitted = Vec::with_capacity(corpus.len().saturating_mul(replicas)); - for replica in 0..replicas { - for file in corpus { - let logical_path = if replica == 0 { - file.relative_path.clone() - } else { - format!("replica{replica:02}/{}", file.relative_path) - }; - admitted.push(AdmittedFile { - logical_path, - language: file.language.clone(), - bytes: Arc::from(file.bytes.clone()), - }); - } - } - admitted.sort_by(|left, right| left.logical_path.cmp(&right.logical_path)); - admitted -} - /// Deterministic incremental edit: append a distinguishing trailing comment /// to every `EDIT_STRIDE`-th file. Appending keeps the edit a genuine /// suffix change so the retained-parse pool exercises its incremental path @@ -523,22 +407,6 @@ fn build_body_refresh( )) } -// --------------------------------------------------------------------------- -// In-memory production authorities -// --------------------------------------------------------------------------- - -struct ActiveControl; - -impl CodeIndexExecutionControlV1 for ActiveControl { - fn is_cancelled(&self) -> bool { - false - } - - fn is_deadline_exceeded(&self) -> bool { - false - } -} - struct CancelledControl; impl CodeIndexExecutionControlV1 for CancelledControl { @@ -550,99 +418,6 @@ impl CodeIndexExecutionControlV1 for CancelledControl { false } } - -/// In-memory compare-and-swap publication authority. The real daemon store -/// is a database; the benchmark deliberately measures indexing rather than -/// storage, so publication is a map behind a mutex. -#[derive(Default)] -struct MemoryPublicationStore { - active: Arc>>>, -} - -impl CodeIndexAtomicPublicationPort for MemoryPublicationStore { - fn load_active( - &self, - scope: &CodeIndexGenerationScopeV1, - ) -> Result>, CodeIndexPublicationStoreErrorV1> { - Ok(self - .active - .lock() - .map_err(|_| CodeIndexPublicationStoreErrorV1::CompareAndSwap)? - .get(scope) - .map(Arc::clone)) - } - - fn publish_atomically( - &mut self, - scope: &CodeIndexGenerationScopeV1, - expected_active_generation: Option<&CodeGenerationId>, - generation: Arc, - ) -> Result<(), CodeIndexPublicationStoreErrorV1> { - let mut active = self - .active - .lock() - .map_err(|_| CodeIndexPublicationStoreErrorV1::CompareAndSwap)?; - if active - .get(scope) - .map(|current| current.manifest().generation_id.clone()) - .as_ref() - != expected_active_generation - { - return Err(CodeIndexPublicationStoreErrorV1::CompareAndSwap); - } - active.insert(scope.clone(), generation); - Ok(()) - } -} - -/// Applies every decision without a downstream model or store, so the -/// profile attributes time to extraction and sealing rather than to a -/// projection backend this workload does not mount. -struct ApplyingProjectionSink; - -impl CodeChunkProjectionSink for ApplyingProjectionSink { - fn project_changed_chunks( - &mut self, - request: &ProjectionBatchRequestV1, - receipt_builder: ProjectionReceiptBuilderV1<'_>, - ) -> Result { - let mut decisions = Vec::with_capacity( - request.changes.added_or_changed.len() + request.changes.deleted.len(), - ); - decisions.extend(request.changes.added_or_changed.iter().map(|change| { - ChunkProjectionDecisionV1 { - chunk_id: change.chunk_id.clone(), - prior_chunk_digest: change.prior_digest.clone(), - current_chunk_digest: change.current_digest.clone(), - operation: if change.prior_digest.is_some() { - ProjectionOperationV1::Updated - } else { - ProjectionOperationV1::Added - }, - outcome: ProjectionOutcomeV1::Applied, - output_digest: change.current_digest.clone(), - } - })); - decisions.extend( - request - .changes - .deleted - .iter() - .map(|change| ChunkProjectionDecisionV1 { - chunk_id: change.chunk_id.clone(), - prior_chunk_digest: change.prior_digest.clone(), - current_chunk_digest: None, - operation: ProjectionOperationV1::Deleted, - outcome: ProjectionOutcomeV1::Applied, - output_digest: None, - }), - ); - receipt_builder - .build(&decisions) - .map_err(|error| ProjectionSinkErrorV1::Rejected(error.to_string())) - } -} - // --------------------------------------------------------------------------- // Workload // --------------------------------------------------------------------------- @@ -792,7 +567,18 @@ fn run(options: &Options) -> Result { // Pass 3 - drain the sealed generation as bounded page batches. let drain_started = Instant::now(); - let (pages, source_receipt) = drain_pages(&sealed, sealed_len, &state_digest, &control)?; + let (pages, source_receipt) = drain_pages( + &sealed, + sealed_len, + &state_digest, + &control, + SealedDrainBounds { + batch_pages: BATCH_MAX_PAGES, + batch_retained_bytes: BATCH_MAX_RETAINED_BYTES, + page_chunks: MAX_PAGE_CHUNKS, + page_bytes: MAX_PAGE_BYTES, + }, + )?; let drain_wall = drain_started.elapsed(); // Pass 4 - ingest the pages into an isolated on-disk lexical artifact. @@ -933,64 +719,6 @@ fn benchmark_file_occurrence_id(file: &AdmittedFile, digest: &ContentDigest) -> occurrence.as_str().trim_start_matches("sha256:") )) } - -fn sealed_state_digest(sealed: &[u8]) -> Result { - let envelope: serde_json::Value = serde_json::from_slice(sealed) - .map_err(|error| format!("decode sealed generation envelope: {error}"))?; - let digest = envelope - .get("state_digest") - .and_then(serde_json::Value::as_str) - .ok_or_else(|| "sealed generation envelope has no state digest".to_owned())?; - ManifestDigest::try_from(digest.to_owned()) - .map_err(|error| format!("sealed generation state digest: {error:?}")) -} - -/// Drain the sealed generation through the bounded batch path, which is the -/// shape the daemon's artifact ingestion uses. The single-page path is -/// asserted to agree on the source receipt so a regression that desynchronizes -/// the two cursors fails here instead of skewing the comparison. -fn drain_pages( - sealed: &[u8], - sealed_len: u64, - state_digest: &ManifestDigest, - control: &ActiveControl, -) -> Result< - ( - Vec, - VerifiedSealedLexicalSourceReceiptV1, - ), - String, -> { - let bounds = - VerifiedSealedLexicalPageBatchBoundsV1::new(BATCH_MAX_PAGES, BATCH_MAX_RETAINED_BYTES) - .map_err(|error| format!("sealed lexical batch bounds: {error}"))?; - let mut source = VerifiedSealedLexicalPageSourceV1::open( - Cursor::new(sealed.to_vec()), - sealed_len, - state_digest.clone(), - MAX_PAGE_CHUNKS, - MAX_PAGE_BYTES, - control, - ) - .map_err(|error| format!("open sealed lexical page source: {error}"))?; - let mut pages = Vec::new(); - loop { - let read = source - .next_page_batch_if(control, bounds, |staged| { - NonZeroUsize::new(staged.len()) - .ok_or_else(|| "sealed lexical batch staged no pages".to_owned()) - }) - .map_err(|error| format!("stage sealed lexical page batch: {error}"))? - .map_err(|error| format!("admit sealed lexical page batch: {error}"))?; - match read { - VerifiedSealedLexicalPageBatchReadV1::Pages(batch) => pages.extend(batch), - VerifiedSealedLexicalPageBatchReadV1::Complete(receipt) => { - return Ok((pages, receipt)); - } - } - } -} - fn projection_metadata( generation: &CodeIndexPublishedGenerationV1, repository: &RepositoryId, @@ -1379,22 +1107,6 @@ impl Scratch { .map_err(|error| format!("remove scratch {}: {error}", self.path.display())) } } - -/// Peak resident set size in bytes, or `None` off Linux. The profiling job -/// budgets 4 GB; reporting the high-water mark makes a breach visible in the -/// run log instead of only as an OOM kill. -fn peak_rss_bytes() -> Option { - let status = std::fs::read_to_string("/proc/self/status").ok()?; - for line in status.lines() { - let Some(value) = line.strip_prefix("VmHWM:") else { - continue; - }; - let kilobytes = value.split_whitespace().next()?.parse::().ok()?; - return kilobytes.checked_mul(1024); - } - None -} - // --------------------------------------------------------------------------- // Summary // --------------------------------------------------------------------------- @@ -1492,19 +1204,6 @@ fn summary(fields: SummaryFields<'_>) -> String { }); serde_json::to_string_pretty(&report).unwrap_or_else(|_| report.to_string()) } - -fn millis(duration: Duration) -> u64 { - u64::try_from(duration.as_millis()).unwrap_or(u64::MAX) -} - -fn percentile(sorted: &[u64], percent: usize) -> u64 { - if sorted.is_empty() { - return 0; - } - let rank = (sorted.len() * percent).div_ceil(100); - sorted[rank.saturating_sub(1).min(sorted.len() - 1)] -} - fn host_facts() -> serde_json::Value { let cpu_model = std::fs::read_to_string("/proc/cpuinfo") .ok() @@ -1533,17 +1232,6 @@ fn host_facts() -> serde_json::Value { "memory_bytes": memory_bytes, }) } - -fn identity(value: &str) -> T -where - T: TryFrom, - >::Error: fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap_or_else(|error| { - panic!("deterministic benchmark identity {value:?} must be valid: {error:?}") - }) -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/tracedecay-query/src/bin/tracedecay_search_bench.rs b/crates/tracedecay-query/src/bin/tracedecay_search_bench.rs index b70ccc4d61..00de73661e 100644 --- a/crates/tracedecay-query/src/bin/tracedecay_search_bench.rs +++ b/crates/tracedecay-query/src/bin/tracedecay_search_bench.rs @@ -27,44 +27,39 @@ #![allow(clippy::print_stdout, clippy::print_stderr)] +#[path = "artifact_bench.rs"] +mod artifact_bench; + +use artifact_bench::{ + ActiveControl, AdmittedFile, ApplyingProjectionSink, MemoryPublicationStore, SealedDrainBounds, + default_corpus_root, drain_pages, identity, millis, peak_rss_bytes, percentile, replicate, + sealed_state_digest, +}; use std::collections::BTreeSet; -use std::fmt; -use std::io::{Cursor, Read}; -use std::num::NonZeroUsize; use std::path::{Path, PathBuf}; use std::process::ExitCode; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use std::time::{Duration, Instant}; use tracedecay_code_index::chunks::content_digest; -use tracedecay_code_index::languages::{LanguageRegistry, StaticLanguageRegistry}; use tracedecay_code_index::production::{ - CodeIndexAtomicPublicationPort, CodeIndexBuildRequestV1, CodeIndexCapturedFileV1, - CodeIndexExecutionControlV1, CodeIndexGenerationScopeV1, CodeIndexProductionConfigV1, - CodeIndexProductionOwnerV1, CodeIndexPublicationStoreErrorV1, CodeIndexPublishedGenerationV1, - CodeIndexRepositoryParseIdentityV1, VerifiedSealedLexicalPageBatchBoundsV1, - VerifiedSealedLexicalPageBatchReadV1, VerifiedSealedLexicalPageSourceV1, + CodeIndexBuildRequestV1, CodeIndexCapturedFileV1, CodeIndexProductionConfigV1, + CodeIndexProductionOwnerV1, CodeIndexPublishedGenerationV1, CodeIndexRepositoryParseIdentityV1, VerifiedSealedLexicalPageV1, VerifiedSealedLexicalSourceReceiptV1, }; -use tracedecay_code_index::projection::{ - ChunkProjectionDecisionV1, CodeChunkProjectionSink, ProjectionReceiptBuilderV1, - ProjectionSinkErrorV1, ProjectionSinkReceiptV1, -}; use tracedecay_domain::{ AuthorizationRevision, ChunkerRevision, CodeGenerationId, ComponentRevision, ExactAdmissionRuleRevision, FileOccurrenceId, FreshnessCompatibilityV1, FreshnessVectorDigest, - FusionProfileId, LanguageId, ManifestDigest, PolicyRevisionId, PrincipalId, PrivacyDomainId, - ProjectId, ProjectionBatchRequestV1, ProjectionKeyV1, ProjectionKindV1, ProjectionOperationV1, - ProjectionOutcomeV1, QueryNormalizationRevision, RepositoryDirtyStateV1, RepositoryId, - RetrievalBudget, RetrievalRequest, RetrievalScope, RetrievalSnapshot, RetrieverBatch, - RetrieverOutcome, SanitizationReceiptId, SanitizedCodeFileV1, SanitizedCodeSnapshotV1, - SanitizerRevision, ScoreDomainId, SensitivityLevelV1, SingleRootScopeV1, - SnapshotFileDispositionV1, SourceFreshness, SourceInstanceKey, SourceNamespace, TemporalModeV1, - TreeId, UtcMicros, VectorWatermark, + FusionProfileId, ManifestDigest, PolicyRevisionId, PrincipalId, PrivacyDomainId, ProjectId, + ProjectionKeyV1, ProjectionKindV1, QueryNormalizationRevision, RepositoryDirtyStateV1, + RepositoryId, RetrievalBudget, RetrievalRequest, RetrievalScope, RetrievalSnapshot, + RetrieverBatch, RetrieverOutcome, SanitizationReceiptId, SanitizedCodeFileV1, + SanitizedCodeSnapshotV1, SanitizerRevision, ScoreDomainId, SensitivityLevelV1, + SingleRootScopeV1, SnapshotFileDispositionV1, SourceFreshness, SourceInstanceKey, + SourceNamespace, TemporalModeV1, TreeId, UtcMicros, VectorWatermark, }; use tracedecay_query::retrieval::exact::{ - CentralExactAdmissionAuthorityV1, ExactAdmissionAuthority, ExactLane, ExactLaneRequest, - ExactLaneRetriever, + CentralExactAdmissionAuthorityV1, ExactLane, ExactLaneRequest, ExactLaneRetriever, }; use tracedecay_query::retrieval::lexical::{ CODE_LEXICAL_ARTIFACT_QUERY_CACHE_BUDGET_BYTES_V1, CodeLexicalArtifactBuilderV1, @@ -81,7 +76,6 @@ use tracedecay_query::retrieval::{ /// Bumped whenever the workload shape changes, so a profile comparison /// across a shape change is visibly not comparable. const WORKLOAD_REVISION: &str = "search-bench.v1"; -const DEFAULT_CORPUS_RELATIVE: &str = "benchmark_data/index-bench/corpus"; const CORPUS_ENV: &str = "TRACEDECAY_SEARCH_BENCH_CORPUS"; const REPLICAS_ENV: &str = "TRACEDECAY_SEARCH_BENCH_REPLICAS"; const KEEP_SCRATCH_ENV: &str = "TRACEDECAY_SEARCH_BENCH_KEEP_SCRATCH"; @@ -350,129 +344,6 @@ fn parse_count(flag: &str, value: &str, minimum: usize) -> Result Ok(count) } -fn default_corpus_root() -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .join(DEFAULT_CORPUS_RELATIVE) -} - -// --------------------------------------------------------------------------- -// Corpus admission (identical walk to `tracedecay-index-bench`) -// --------------------------------------------------------------------------- - -struct CorpusFile { - relative_path: String, - language: LanguageId, - bytes: Vec, -} - -fn load_corpus(root: &Path) -> Result, String> { - let registry = StaticLanguageRegistry::new(); - let mut files = Vec::new(); - collect_corpus(root, root, ®istry, &mut files)?; - files.sort_by(|left, right| left.relative_path.cmp(&right.relative_path)); - if files.is_empty() { - return Err(format!( - "corpus {} admitted no files with a known language extension", - root.display() - )); - } - Ok(files) -} - -fn collect_corpus( - root: &Path, - directory: &Path, - registry: &StaticLanguageRegistry, - files: &mut Vec, -) -> Result<(), String> { - let mut entries = std::fs::read_dir(directory) - .map_err(|error| format!("read {}: {error}", directory.display()))? - .collect::, _>>() - .map_err(|error| format!("read {}: {error}", directory.display()))?; - entries.sort_by_key(std::fs::DirEntry::file_name); - for entry in entries { - let path = entry.path(); - let file_type = entry - .file_type() - .map_err(|error| format!("stat {}: {error}", path.display()))?; - if file_type.is_dir() { - collect_corpus(root, &path, registry, files)?; - continue; - } - if !file_type.is_file() { - continue; - } - let Some(extension) = path.extension().and_then(std::ffi::OsStr::to_str) else { - continue; - }; - let Some(descriptor) = registry.descriptor_for_extension(&extension.to_lowercase()) else { - continue; - }; - if !descriptor.capabilities.extraction { - continue; - } - let relative = path - .strip_prefix(root) - .map_err(|error| format!("relativize {}: {error}", path.display()))?; - let Some(relative_path) = relative.to_str() else { - return Err(format!("corpus path {} is not Unicode", relative.display())); - }; - let bytes = - std::fs::read(&path).map_err(|error| format!("read {}: {error}", path.display()))?; - files.push(CorpusFile { - relative_path: relative_path.replace('\\', "/"), - language: descriptor.language.clone(), - bytes, - }); - } - Ok(()) -} - -struct AdmittedFile { - logical_path: String, - language: LanguageId, - bytes: Arc<[u8]>, -} - -fn replicate(corpus: &[CorpusFile], replicas: usize) -> Vec { - let mut admitted = Vec::with_capacity(corpus.len().saturating_mul(replicas)); - for replica in 0..replicas { - for file in corpus { - let logical_path = if replica == 0 { - file.relative_path.clone() - } else { - format!("replica{replica:02}/{}", file.relative_path) - }; - admitted.push(AdmittedFile { - logical_path, - language: file.language.clone(), - bytes: Arc::from(file.bytes.clone()), - }); - } - } - // The snapshot contract requires canonical file order over the whole - // admitted set, not per replica. - admitted.sort_by(|left, right| left.logical_path.cmp(&right.logical_path)); - admitted -} - -// --------------------------------------------------------------------------- -// In-memory production authorities (identical to `tracedecay-index-bench`) -// --------------------------------------------------------------------------- - -struct ActiveControl; - -impl CodeIndexExecutionControlV1 for ActiveControl { - fn is_cancelled(&self) -> bool { - false - } - - fn is_deadline_exceeded(&self) -> bool { - false - } -} - impl RetrievalExecutionControl for ActiveControl { fn is_cancelled(&self) -> bool { false @@ -482,100 +353,6 @@ impl RetrievalExecutionControl for ActiveControl { 0 } } - -#[derive(Default)] -struct MemoryPublicationStore { - active: Arc< - Mutex< - std::collections::BTreeMap< - CodeIndexGenerationScopeV1, - Arc, - >, - >, - >, -} - -impl CodeIndexAtomicPublicationPort for MemoryPublicationStore { - fn load_active( - &self, - scope: &CodeIndexGenerationScopeV1, - ) -> Result>, CodeIndexPublicationStoreErrorV1> { - Ok(self - .active - .lock() - .map_err(|_| CodeIndexPublicationStoreErrorV1::CompareAndSwap)? - .get(scope) - .map(Arc::clone)) - } - - fn publish_atomically( - &mut self, - scope: &CodeIndexGenerationScopeV1, - expected_active_generation: Option<&CodeGenerationId>, - generation: Arc, - ) -> Result<(), CodeIndexPublicationStoreErrorV1> { - let mut active = self - .active - .lock() - .map_err(|_| CodeIndexPublicationStoreErrorV1::CompareAndSwap)?; - if active - .get(scope) - .map(|current| current.manifest().generation_id.clone()) - .as_ref() - != expected_active_generation - { - return Err(CodeIndexPublicationStoreErrorV1::CompareAndSwap); - } - active.insert(scope.clone(), generation); - Ok(()) - } -} - -struct ApplyingProjectionSink; - -impl CodeChunkProjectionSink for ApplyingProjectionSink { - fn project_changed_chunks( - &mut self, - request: &ProjectionBatchRequestV1, - receipt_builder: ProjectionReceiptBuilderV1<'_>, - ) -> Result { - let mut decisions = Vec::with_capacity( - request.changes.added_or_changed.len() + request.changes.deleted.len(), - ); - decisions.extend(request.changes.added_or_changed.iter().map(|change| { - ChunkProjectionDecisionV1 { - chunk_id: change.chunk_id.clone(), - prior_chunk_digest: change.prior_digest.clone(), - current_chunk_digest: change.current_digest.clone(), - operation: if change.prior_digest.is_some() { - ProjectionOperationV1::Updated - } else { - ProjectionOperationV1::Added - }, - outcome: ProjectionOutcomeV1::Applied, - output_digest: change.current_digest.clone(), - } - })); - decisions.extend( - request - .changes - .deleted - .iter() - .map(|change| ChunkProjectionDecisionV1 { - chunk_id: change.chunk_id.clone(), - prior_chunk_digest: change.prior_digest.clone(), - current_chunk_digest: None, - operation: ProjectionOperationV1::Deleted, - outcome: ProjectionOutcomeV1::Applied, - output_digest: None, - }), - ); - receipt_builder - .build(&decisions) - .map_err(|error| ProjectionSinkErrorV1::Rejected(error.to_string())) - } -} - // --------------------------------------------------------------------------- // Workload // --------------------------------------------------------------------------- @@ -629,7 +406,18 @@ fn run(options: &Options) -> Result { let state_digest = sealed_state_digest(&sealed)?; let drain_started = Instant::now(); - let (pages, source_receipt) = drain_pages(&sealed, sealed_len, &state_digest, &control)?; + let (pages, source_receipt) = drain_pages( + &sealed, + sealed_len, + &state_digest, + &control, + SealedDrainBounds { + batch_pages: BATCH_MAX_PAGES, + batch_retained_bytes: BATCH_MAX_RETAINED_BYTES, + page_chunks: MAX_PAGE_CHUNKS, + page_bytes: MAX_PAGE_BYTES, + }, + )?; let drain_wall = drain_started.elapsed(); let scratch = Scratch::create()?; @@ -1042,15 +830,6 @@ fn phase_stats( "max": values.last().copied().unwrap_or_default(), }) } - -fn percentile(sorted: &[u64], percent: usize) -> u64 { - if sorted.is_empty() { - return 0; - } - let rank = (sorted.len() * percent).div_ceil(100); - sorted[rank.saturating_sub(1).min(sorted.len() - 1)] -} - fn build_request( repository: &RepositoryId, sanitizer_revision: &SanitizerRevision, @@ -1109,60 +888,6 @@ fn build_request( }, } } - -fn sealed_state_digest(sealed: &[u8]) -> Result { - let envelope: serde_json::Value = serde_json::from_slice(sealed) - .map_err(|error| format!("decode sealed generation envelope: {error}"))?; - let digest = envelope - .get("state_digest") - .and_then(serde_json::Value::as_str) - .ok_or_else(|| "sealed generation envelope has no state digest".to_owned())?; - ManifestDigest::try_from(digest.to_owned()) - .map_err(|error| format!("sealed generation state digest: {error:?}")) -} - -fn drain_pages( - sealed: &[u8], - sealed_len: u64, - state_digest: &ManifestDigest, - control: &ActiveControl, -) -> Result< - ( - Vec, - VerifiedSealedLexicalSourceReceiptV1, - ), - String, -> { - let bounds = - VerifiedSealedLexicalPageBatchBoundsV1::new(BATCH_MAX_PAGES, BATCH_MAX_RETAINED_BYTES) - .map_err(|error| format!("sealed lexical batch bounds: {error}"))?; - let mut source = VerifiedSealedLexicalPageSourceV1::open( - Cursor::new(sealed.to_vec()), - sealed_len, - state_digest.clone(), - MAX_PAGE_CHUNKS, - MAX_PAGE_BYTES, - control, - ) - .map_err(|error| format!("open sealed lexical page source: {error}"))?; - let mut pages = Vec::new(); - loop { - let read = source - .next_page_batch_if(control, bounds, |staged| { - NonZeroUsize::new(staged.len()) - .ok_or_else(|| "sealed lexical batch staged no pages".to_owned()) - }) - .map_err(|error| format!("stage sealed lexical page batch: {error}"))? - .map_err(|error| format!("admit sealed lexical page batch: {error}"))?; - match read { - VerifiedSealedLexicalPageBatchReadV1::Pages(batch) => pages.extend(batch), - VerifiedSealedLexicalPageBatchReadV1::Complete(receipt) => { - return Ok((pages, receipt)); - } - } - } -} - fn projection_metadata( generation: &CodeIndexPublishedGenerationV1, repository: &RepositoryId, @@ -1285,33 +1010,6 @@ fn hash_file(path: &Path) -> Result<(ManifestDigest, u64), String> { .map_err(|error| format!("artifact digest: {error}"))?; Ok((digest, file_size_bytes)) } - -fn peak_rss_bytes() -> Option { - let status = std::fs::read_to_string("/proc/self/status").ok()?; - for line in status.lines() { - let Some(value) = line.strip_prefix("VmHWM:") else { - continue; - }; - let kilobytes = value.split_whitespace().next()?.parse::().ok()?; - return kilobytes.checked_mul(1024); - } - None -} - -fn millis(duration: Duration) -> u64 { - u64::try_from(duration.as_millis()).unwrap_or(u64::MAX) -} - fn micros(duration: Duration) -> u64 { u64::try_from(duration.as_micros()).unwrap_or(u64::MAX) } - -fn identity(value: &str) -> T -where - T: TryFrom, - >::Error: fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap_or_else(|error| { - panic!("deterministic benchmark identity {value:?} must be valid: {error:?}") - }) -} From 47bcde840aa5eb085c6356409aebe20258a85109 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:58:54 +0000 Subject: [PATCH 090/182] simplify(pass-2/5): share packed byte n-gram encoding The eval-only projection and the durable artifact packed a 1-3 byte window with the same shift. One function owns that encoding. Co-authored-by: Zack Jackson --- .../src/retrieval/lexical/projection/artifact.rs | 2 ++ .../lexical/projection/artifact/postings.rs | 2 +- .../lexical/projection/in_memory/postings.rs | 12 ++---------- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact.rs b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact.rs index 4cbba3bae1..7705643a35 100644 --- a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact.rs +++ b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact.rs @@ -21,6 +21,8 @@ mod reader; mod row_codec; mod schema; +pub(in crate::retrieval::lexical) use postings::pack_byte_ngram; + pub use builder::{ CodeLexicalArtifactBuildProgressV1, CodeLexicalArtifactBuilderV1, CodeLexicalArtifactFinalizationPhaseV1, CodeLexicalArtifactFinalizationStepV1, diff --git a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/postings.rs b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/postings.rs index 211674f880..8ff71b510b 100644 --- a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/postings.rs +++ b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/postings.rs @@ -111,7 +111,7 @@ pub(super) fn query_ngrams(bytes: &[u8]) -> BTreeSet { bytes.windows(width).map(pack_byte_ngram).collect() } -fn pack_byte_ngram(bytes: &[u8]) -> u32 { +pub(super) fn pack_byte_ngram(bytes: &[u8]) -> u32 { debug_assert!((1..=3).contains(&bytes.len())); bytes .iter() diff --git a/crates/tracedecay-query/src/retrieval/lexical/projection/in_memory/postings.rs b/crates/tracedecay-query/src/retrieval/lexical/projection/in_memory/postings.rs index 19b56ab0b8..72f4239914 100644 --- a/crates/tracedecay-query/src/retrieval/lexical/projection/in_memory/postings.rs +++ b/crates/tracedecay-query/src/retrieval/lexical/projection/in_memory/postings.rs @@ -4,6 +4,8 @@ use std::time::Instant; use fst::{IntoStreamer, Set, Streamer, automaton::Levenshtein}; use roaring::RoaringBitmap; +use super::super::artifact::pack_byte_ngram; + const NGRAM_PAGE_BACKING_BYTES: usize = 1024 * 1024; const NGRAM_PAGE_ENTRY_CAPACITY: usize = NGRAM_PAGE_BACKING_BYTES / std::mem::size_of::(); const NGRAM_MINIMUM_PAGE_ENTRY_CAPACITY: usize = 1024; @@ -358,16 +360,6 @@ impl ByteNgramBudget { } } -fn pack_byte_ngram(bytes: &[u8]) -> u32 { - debug_assert!((1..=3).contains(&bytes.len())); - bytes - .iter() - .enumerate() - .fold((bytes.len() as u32) << 24, |packed, (index, byte)| { - packed | (u32::from(*byte) << (index * 8)) - }) -} - fn pack_posting(ngram: u32, document: u32) -> u64 { (u64::from(ngram) << 32) | u64::from(document) } From 08889df74b9c84eb0dfd2e5366c74a7d54d8d7a8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:00:32 +0000 Subject: [PATCH 091/182] simplify(pass-3/5): share lexical artifact digest helpers Builder, page preparation, and clone fingerprints each owned a copy of the length-prefixed hasher and contract-number mapping. Co-authored-by: Zack Jackson --- .../lexical/projection/artifact/builder.rs | 21 +++------------ .../projection/artifact/fingerprints.rs | 6 +---- .../lexical/projection/artifact/format.rs | 27 ++++++++++++------- .../lexical/projection/artifact/prepared.rs | 19 +++---------- 4 files changed, 25 insertions(+), 48 deletions(-) diff --git a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/builder.rs b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/builder.rs index f844c615b4..f1aed3333f 100644 --- a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/builder.rs +++ b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/builder.rs @@ -34,9 +34,10 @@ use super::format::{ BASE_SECTION_NAMES, CodeLexicalArtifactSectionDigestV1, RECEIPT_RESERVATION_BYTES, SECTION_NAMES, SERVING_INDEX_STEP_COUNT_V11, STATISTICS_STEP_COUNT_V11, VerifiedCodeLexicalArtifactV1, absorb_page_base_sections_receipt, artifact_digest, - decode_padded_receipt, decode_padded_receipt_with_control, finish_base_section_receipt_fold, - initial_base_section_receipt_fold, metadata_digest, new_verified_receipt, padded_receipt, - section_names, verify_artifact_table_layout, verify_required_artifact_indexes, + contract_number, decode_padded_receipt, decode_padded_receipt_with_control, + finish_base_section_receipt_fold, hash_bytes, initial_base_section_receipt_fold, + metadata_digest, new_verified_receipt, padded_receipt, section_names, + verify_artifact_table_layout, verify_required_artifact_indexes, }; use super::postings::document_ngram_scratch; use super::prepared::{ @@ -6496,16 +6497,6 @@ fn hash_value(hasher: &mut Sha256, value: ValueRef<'_>) -> Result<(), CodeLexica Ok(()) } -fn hash_bytes(hasher: &mut Sha256, bytes: &[u8]) -> Result<(), CodeLexicalArtifactErrorV1> { - hasher.update( - u64::try_from(bytes.len()) - .map_err(contract_number)? - .to_le_bytes(), - ); - hasher.update(bytes); - Ok(()) -} - fn read_receipt( connection: &Connection, ) -> Result, CodeLexicalArtifactErrorV1> { @@ -6831,10 +6822,6 @@ fn require_integrity( Ok(()) } -fn contract_number(error: impl std::fmt::Display) -> CodeLexicalArtifactErrorV1 { - CodeLexicalArtifactErrorV1::Contract(error.to_string()) -} - #[cfg(test)] mod tests { use super::super::format::encode_ngram_bitmap; diff --git a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/fingerprints.rs b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/fingerprints.rs index 5d9125de82..742d0ff9dc 100644 --- a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/fingerprints.rs +++ b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/fingerprints.rs @@ -11,7 +11,7 @@ use tracedecay_code_index::clones::{ use tracedecay_code_index::production::CodeIndexExecutionControlV1; use tracedecay_domain::{ManifestDigest, RetrieverCoverage, SymbolOccurrenceId, canonical_sha256}; -use super::format::VerifiedCodeLexicalArtifactV1; +use super::format::{VerifiedCodeLexicalArtifactV1, contract_number}; use super::reader::{CloneArtifactCursorPositionV1, CloneArtifactCursorV1, CloneArtifactPageV1}; use super::schema::LexicalArtifactLayoutV1; use super::{CodeLexicalArtifactErrorV1, sqlite_error}; @@ -732,10 +732,6 @@ fn interrupt( } } -fn contract_number(error: impl std::fmt::Display) -> CodeLexicalArtifactErrorV1 { - CodeLexicalArtifactErrorV1::Contract(error.to_string()) -} - #[cfg(test)] mod tests { use super::candidate_size_ratio_admitted; diff --git a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/format.rs b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/format.rs index a75dbe64c7..70bcfe2642 100644 --- a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/format.rs +++ b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/format.rs @@ -138,12 +138,12 @@ impl PageBaseSectionReceiptBuilderV1 { pub(super) fn text(&mut self, value: &str) -> Result<(), CodeLexicalArtifactErrorV1> { self.hasher.update([3]); - hash_receipt_bytes(&mut self.hasher, value.as_bytes()) + hash_bytes(&mut self.hasher, value.as_bytes()) } pub(super) fn blob(&mut self, value: &[u8]) -> Result<(), CodeLexicalArtifactErrorV1> { self.hasher.update([4]); - hash_receipt_bytes(&mut self.hasher, value) + hash_bytes(&mut self.hasher, value) } pub(super) fn finish( @@ -162,13 +162,20 @@ impl PageBaseSectionReceiptBuilderV1 { } } -fn hash_receipt_bytes(hasher: &mut Sha256, value: &[u8]) -> Result<(), CodeLexicalArtifactErrorV1> { +pub(super) fn contract_number(error: impl std::fmt::Display) -> CodeLexicalArtifactErrorV1 { + CodeLexicalArtifactErrorV1::Contract(error.to_string()) +} + +pub(super) fn hash_bytes( + hasher: &mut Sha256, + bytes: &[u8], +) -> Result<(), CodeLexicalArtifactErrorV1> { hasher.update( - u64::try_from(value.len()) - .map_err(|error| CodeLexicalArtifactErrorV1::Contract(error.to_string()))? + u64::try_from(bytes.len()) + .map_err(contract_number)? .to_le_bytes(), ); - hasher.update(value); + hasher.update(bytes); Ok(()) } @@ -217,7 +224,7 @@ pub(super) fn initial_base_section_receipt_fold() .map(|name| { let mut hasher = Sha256::new(); hasher.update(b"tracedecay.code-lexical-artifact-base-receipt-fold.v1\0initial"); - hash_receipt_bytes(&mut hasher, name.as_bytes())?; + hash_bytes(&mut hasher, name.as_bytes())?; Ok(hasher.finalize().to_vec()) }) .collect::, CodeLexicalArtifactErrorV1>>()?; @@ -246,10 +253,10 @@ pub(super) fn absorb_page_base_sections_receipt( })?; let mut hasher = Sha256::new(); hasher.update(b"tracedecay.code-lexical-artifact-base-receipt-fold.v1\0page"); - hash_receipt_bytes(&mut hasher, section.name.as_bytes())?; + hash_bytes(&mut hasher, section.name.as_bytes())?; hasher.update(page_ordinal.to_le_bytes()); hasher.update(section.row_count.to_le_bytes()); - hash_receipt_bytes(&mut hasher, section.digest.as_str().as_bytes())?; + hash_bytes(&mut hasher, section.digest.as_str().as_bytes())?; hasher.update(previous); accumulators[ordinal] = hasher.finalize().to_vec(); row_counts[ordinal] = row_counts[ordinal] @@ -287,7 +294,7 @@ pub(super) fn finish_base_section_receipt_fold( })?; let mut hasher = Sha256::new(); hasher.update(b"tracedecay.code-lexical-artifact-base-receipt-fold.v1\0final"); - hash_receipt_bytes(&mut hasher, name.as_bytes())?; + hash_bytes(&mut hasher, name.as_bytes())?; hasher.update(row_counts[ordinal].to_le_bytes()); hasher.update(accumulator); let digest = ManifestDigest::from_sha256_bytes(&hasher.finalize()) diff --git a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/prepared.rs b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/prepared.rs index 947e64e912..e74bfe4732 100644 --- a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/prepared.rs +++ b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/prepared.rs @@ -13,8 +13,9 @@ use super::super::{ exact_field_for_kind, normalized_search_text, }; use super::format::{ - ArtifactRowV1, BASE_SECTION_NAMES, PageBaseSectionReceiptBuilderV1, encode_exact_field, - encode_field, encode_ngram_bitmap, encode_page_base_sections_receipt, ngram_page_digest, + ArtifactRowV1, BASE_SECTION_NAMES, PageBaseSectionReceiptBuilderV1, contract_number, + encode_exact_field, encode_field, encode_ngram_bitmap, encode_page_base_sections_receipt, + hash_bytes, ngram_page_digest, }; use super::postings::{NGRAM_NORMALIZED, NGRAM_RAW_OVERRIDE, document_ngrams}; use super::row_codec::{RowDictionaryTableV1, encode_artifact_row}; @@ -645,16 +646,6 @@ fn hash_blob(hasher: &mut Sha256, value: &[u8]) -> Result<(), CodeLexicalArtifac hash_bytes(hasher, value) } -fn hash_bytes(hasher: &mut Sha256, bytes: &[u8]) -> Result<(), CodeLexicalArtifactErrorV1> { - hasher.update( - u64::try_from(bytes.len()) - .map_err(contract_number)? - .to_le_bytes(), - ); - hasher.update(bytes); - Ok(()) -} - fn import_integrity_digest( canonical: &[u8], evidence: &[u8], @@ -958,7 +949,3 @@ fn prepared_charge_overflow() -> CodeLexicalArtifactErrorV1 { "prepared lexical page retained-byte charge overflowed".to_owned(), ) } - -fn contract_number(error: impl std::fmt::Display) -> CodeLexicalArtifactErrorV1 { - CodeLexicalArtifactErrorV1::Contract(error.to_string()) -} From 8164a80d31e50f32a19a4a810ac8c53038c6e854 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:00:54 +0000 Subject: [PATCH 092/182] simplify(pass-4/5): share clone fingerprint count SQL The artifact builder and the clone successor derived clone_fingerprint_counts with the same grouped insert. Co-authored-by: Zack Jackson --- .../lexical/projection/artifact/builder.rs | 2 +- .../projection/artifact/clone_successor.rs | 17 ++--------------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/builder.rs b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/builder.rs index f1aed3333f..a9b173f33d 100644 --- a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/builder.rs +++ b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/builder.rs @@ -3997,7 +3997,7 @@ fn derive_clone_fingerprint_postings( .map_err(sqlite_error) } -fn derive_clone_fingerprint_counts( +pub(super) fn derive_clone_fingerprint_counts( transaction: &Transaction<'_>, ) -> Result<(), CodeLexicalArtifactErrorV1> { transaction diff --git a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/clone_successor.rs b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/clone_successor.rs index bb09cccdfa..520ed70b01 100644 --- a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/clone_successor.rs +++ b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/clone_successor.rs @@ -14,8 +14,8 @@ use tracedecay_private_fs::{create_private_file_retained, open_private_file}; use super::super::CodeLexicalProjectionMetadataV1; use super::builder::{ - BuilderMutationGuardV1, compute_clone_section_digests, install_clone_freeze, - register_builder_mutation_gate, sqlite_file_size, verify_clone_rows, + BuilderMutationGuardV1, compute_clone_section_digests, derive_clone_fingerprint_counts, + install_clone_freeze, register_builder_mutation_gate, sqlite_file_size, verify_clone_rows, }; use super::format::{ RECEIPT_RESERVATION_BYTES, VerifiedCodeLexicalArtifactV1, artifact_digest, @@ -810,16 +810,3 @@ fn verify_source_receipt( } Ok(()) } - -fn derive_clone_fingerprint_counts( - transaction: &rusqlite::Transaction<'_>, -) -> Result<(), CodeLexicalArtifactErrorV1> { - transaction - .execute_batch( - "INSERT INTO clone_fingerprint_counts(language, class, normalization_revision, fingerprint, posting_count) - SELECT language, class, normalization_revision, fingerprint, COUNT(*) - FROM clone_fingerprint_postings - GROUP BY language, class, normalization_revision, fingerprint;", - ) - .map_err(sqlite_error) -} From e44ed49bcb0c4a2e70cd34f170b2955d3c8728bb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:02:34 +0000 Subject: [PATCH 093/182] simplify(pass-1/5): drop uncalled code-index entry points Co-authored-by: Zack Jackson --- .../src/incremental.rs | 9 ----- crates/tracedecay-code-extraction/src/lib.rs | 12 ------- .../src/source_mask.rs | 13 ++------ .../src/code_index_scheduler/serving.rs | 10 ------ .../tracedecay-code-index/src/parallelism.rs | 33 ------------------- 5 files changed, 3 insertions(+), 74 deletions(-) diff --git a/crates/tracedecay-code-extraction/src/incremental.rs b/crates/tracedecay-code-extraction/src/incremental.rs index b78ee635a4..461a442d55 100644 --- a/crates/tracedecay-code-extraction/src/incremental.rs +++ b/crates/tracedecay-code-extraction/src/incremental.rs @@ -599,15 +599,6 @@ impl RetainedParseDocument { self.reparse_normalized(next_identity, new_source.into(), None, None) } - pub fn reparse_prepared( - &mut self, - next_identity: ParseDocumentIdentity, - new_source: impl Into, - new_parsed_source: impl Into, - ) -> Result { - self.reparse_prepared_with_control(next_identity, new_source, new_parsed_source, None) - } - pub fn reparse_prepared_with_control( &mut self, next_identity: ParseDocumentIdentity, diff --git a/crates/tracedecay-code-extraction/src/lib.rs b/crates/tracedecay-code-extraction/src/lib.rs index 72aa410221..0dab251a9c 100644 --- a/crates/tracedecay-code-extraction/src/lib.rs +++ b/crates/tracedecay-code-extraction/src/lib.rs @@ -482,12 +482,6 @@ impl LanguageRegistry { } } - #[cfg(any(test, feature = "test-helpers"))] - #[doc(hidden)] - pub fn from_extractors_for_test(extractors: Vec>) -> Self { - Self::from_extractors(extractors) - } - /// Returns the extractor for a file path based on its extension. pub fn extractor_for_file(&self, path: &str) -> Option<&dyn LanguageExtractor> { let extractor = path.rsplit('.').next().and_then(|ext| { @@ -509,9 +503,3 @@ impl LanguageRegistry { .collect() } } - -impl Default for LanguageRegistry { - fn default() -> Self { - Self::new() - } -} diff --git a/crates/tracedecay-code-extraction/src/source_mask.rs b/crates/tracedecay-code-extraction/src/source_mask.rs index b383572d2f..e210a14420 100644 --- a/crates/tracedecay-code-extraction/src/source_mask.rs +++ b/crates/tracedecay-code-extraction/src/source_mask.rs @@ -44,13 +44,6 @@ impl MaskOptions { }; } -/// Returns a copy of `source` with comment and string/char literal contents -/// blanked, preserving implicit format captures. Equivalent to -/// [`masked_rust_source_with`] using [`MaskOptions::UNUSED_IMPORTS`]. -pub fn masked_rust_source(source: &str) -> String { - masked_rust_source_with(source, MaskOptions::UNUSED_IMPORTS) -} - /// Returns a copy of `source` with comments and string/char literals blanked. /// `opts` controls whether implicit format captures survive. /// Blanked bytes become ASCII spaces; newlines and total byte length are @@ -408,7 +401,7 @@ fn format_capture_identifier_end(bytes: &[u8], start: usize) -> Option { #[cfg(test)] mod tests { - use super::{MaskOptions, masked_rust_source, masked_rust_source_with}; + use super::{MaskOptions, masked_rust_source_with}; /// Whole-token match used by the scanners: does `identifier` appear as a /// real token (non-identifier boundaries) anywhere on `line`? Mirrors the @@ -436,7 +429,7 @@ mod tests { /// Does `identifier` survive default import-discovery masking of `source`? fn referenced(source: &str, identifier: &str) -> bool { - masked_rust_source(source) + masked_rust_source_with(source, MaskOptions::UNUSED_IMPORTS) .lines() .any(|line| contains_token(line, identifier)) } @@ -584,7 +577,7 @@ mod tests { #[test] fn masking_preserves_line_count_and_length() { let src = "fn f() {\n // comment HashMap\n let s = \"str\";\n}\n"; - let masked = masked_rust_source(src); + let masked = masked_rust_source_with(src, MaskOptions::UNUSED_IMPORTS); assert_eq!(masked.len(), src.len()); assert_eq!(masked.lines().count(), src.lines().count()); } diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/serving.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/serving.rs index 78c802ef5a..627a324706 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/serving.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/serving.rs @@ -2034,16 +2034,6 @@ impl LatestCompleteCodeIndexV1 { pub fn lexical(&self) -> &[Arc] { self.generation.chunks().chunks() } - - #[cfg(test)] - pub fn graph_edges(&self) -> &[tracedecay_domain::CanonicalRelationEdgeV1] { - self.generation.edges() - } - - #[cfg(test)] - pub fn graph_abstentions(&self) -> &[crate::code_index::chunks::CodeIndexEdgeAbstentionV1] { - self.generation.edge_abstentions() - } } impl LatestCodeTextGenerationV1 { diff --git a/crates/tracedecay-code-index/src/parallelism.rs b/crates/tracedecay-code-index/src/parallelism.rs index 507a445125..d0c8e7191a 100644 --- a/crates/tracedecay-code-index/src/parallelism.rs +++ b/crates/tracedecay-code-index/src/parallelism.rs @@ -363,39 +363,6 @@ fn environment_override_value() -> Result, CodeIndexWorkerPlanErr } } -/// Preview the worker status without constructing a pool or installing any -/// process authority. `available_memory_bytes` must come from the caller's -/// canonical resident-memory authority (`limit - used`), never a second -/// estimator. Environment precedence and every typed refusal are identical to -/// [`install_worker_plan`]. -pub fn preview_worker_plan( - configured: CodeIndexWorkerSelectionV1, - available_memory_bytes: u64, -) -> Result { - let environment_override = environment_override_value()?; - preview_worker_plan_from( - configured, - detected_cores(), - available_memory_bytes, - environment_override.as_deref(), - ) -} - -fn preview_worker_plan_from( - configured: CodeIndexWorkerSelectionV1, - available_logical_cpus: usize, - available_memory_bytes: u64, - environment_override: Option<&str>, -) -> Result { - worker_plan_from( - configured, - available_logical_cpus, - available_memory_bytes, - environment_override, - ) - .map(CodeIndexWorkerPlanV1::status) -} - fn compare_installed_plan( existing: &CodeIndexWorkerPlanV1, requested: &CodeIndexWorkerPlanV1, From 848f81080cdce388614d685c199b358662f38094 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:03:47 +0000 Subject: [PATCH 094/182] refactor(pass-1/5): share retained hydration wire maps Co-authored-by: Zack Jackson --- .../src/retained.rs | 1 + .../src/retained/lcm/output.rs | 142 ++--------------- .../src/retained/session.rs | 144 ++---------------- .../src/retained/wire.rs | 143 +++++++++++++++++ 4 files changed, 165 insertions(+), 265 deletions(-) create mode 100644 crates/tracedecay-session-runtime/src/retained/wire.rs diff --git a/crates/tracedecay-session-runtime/src/retained.rs b/crates/tracedecay-session-runtime/src/retained.rs index a87d42d3b3..9abf1c4d51 100644 --- a/crates/tracedecay-session-runtime/src/retained.rs +++ b/crates/tracedecay-session-runtime/src/retained.rs @@ -17,6 +17,7 @@ pub mod lcm; pub mod profile; pub mod session; pub mod session_refresh; +mod wire; pub use lcm::DirectRetainedLcmPortV1; pub use profile::{ diff --git a/crates/tracedecay-session-runtime/src/retained/lcm/output.rs b/crates/tracedecay-session-runtime/src/retained/lcm/output.rs index 975bfeb0ee..94932180c6 100644 --- a/crates/tracedecay-session-runtime/src/retained/lcm/output.rs +++ b/crates/tracedecay-session-runtime/src/retained/lcm/output.rs @@ -2,23 +2,17 @@ use tracedecay_contracts::RetainedSurfaceExecutionErrorV1; use tracedecay_contracts::retained_surfaces::{ - ClosedUtcIntervalV1, CompactLineageEdgeV1, HydrationStateResultV1, LcmContentRangeV1, - LcmDescribeExternalPayloadV1, LcmDescribeSourceOverviewV1, LcmDescribeSummaryNodeV1, - LcmDescriptionV1, LcmExpandQueryBudgetV1, LcmExpandQueryContextBlockV1, LcmExpandQueryMatchV1, + CompactLineageEdgeV1, LcmContentRangeV1, LcmDescribeExternalPayloadV1, + LcmDescribeSourceOverviewV1, LcmDescribeSummaryNodeV1, LcmDescriptionV1, + LcmExpandQueryBudgetV1, LcmExpandQueryContextBlockV1, LcmExpandQueryMatchV1, LcmExpandQueryPaginationV1, LcmExpandQueryResultV1, LcmExpandQuerySynthesisPromptV1, LcmExpandedSourceV1, LcmExpansionV1, LcmGrepHitV1, LcmMessageV1, LcmRawMessageMetadataV1, LcmRawMessageOverviewV1, LcmRawMessageV1, LcmRetrievalOutcomeV1, LcmSourcePaginationV1, LcmSourceRefV1, LcmStorageKindV1, LcmSummaryNodeOverviewV1, LcmSummaryNodeV1, - LcmTemporalFieldsV1, RetainedOutcomeStatusV1, SessionCoverageIntervalV1, SessionCoverageModeV1, - SessionCoverageReasonV1, SessionCoverageRequestV1, SessionCoverageStateV1, - SessionSourceCoverageV1 as RetainedSourceCoverageV1, TemporalExplanationV1, - TemporalFreshnessV1, TemporalOmissionV1, TemporalWatermarksV1, ValidCoverageIntervalV1, -}; -use tracedecay_domain::{ - CompactContextLineageEdgeV1, HydrationStateV1, SessionSourceCoverageIntervalV1, - SessionSourceCoverageReasonV1, SessionSourceCoverageStateV1, SessionSourceCoverageV1, - TemporalModeV1, ValidCoverageIntervalV1 as DomainValidCoverageIntervalV1, + LcmTemporalFieldsV1, RetainedOutcomeStatusV1, TemporalExplanationV1, TemporalFreshnessV1, + TemporalOmissionV1, }; +use tracedecay_domain::CompactContextLineageEdgeV1; use tracedecay_lcm::contracts::{ LcmContentRange, LcmDataFreshness, LcmDescribeResponse, LcmExpandResponse, LcmRawMessage, LcmRawMessageMetadata, LcmRetrievalOutcome, LcmSourceRef, LcmStorageKind, LcmSummaryNode, @@ -33,6 +27,9 @@ use tracedecay_temporal_query::context::OrderedTextContextAssembler; use crate::session_retrieval::SessionTemporalMetadataView; +pub(super) use super::super::wire::hydration; +use super::super::wire::{coverage, source_coverage, temporal_watermarks}; + #[hotpath::measure(label = "daemon.retained.lcm.hydrate_temporal")] pub(super) fn temporal_fields(value: SessionTemporalMetadataView) -> LcmTemporalFieldsV1 { LcmTemporalFieldsV1 { @@ -41,20 +38,9 @@ pub(super) fn temporal_fields(value: SessionTemporalMetadataView) -> LcmTemporal .into_iter() .map(|anchor| anchor.as_str().to_owned()) .collect(), - watermarks: TemporalWatermarksV1 { - generation: value.watermarks.generation, - source: value.watermarks.source, - projection: value.watermarks.projection, - index: value.watermarks.index, - summary: value.watermarks.summary, - }, + watermarks: temporal_watermarks(value.watermarks), authorized_root: value.authorized_root, - coverage: tracedecay_contracts::retained_surfaces::TemporalCoverageV1 { - visible: value.coverage.visible, - hidden: value.coverage.hidden, - unknown: value.coverage.unknown, - redacted: value.coverage.redacted, - }, + coverage: coverage(value.coverage), source_coverage: value .source_coverage .into_iter() @@ -81,99 +67,6 @@ pub(super) fn temporal_fields(value: SessionTemporalMetadataView) -> LcmTemporal } } -fn source_coverage(value: SessionSourceCoverageV1) -> RetainedSourceCoverageV1 { - RetainedSourceCoverageV1 { - source_id: value.source_id().as_str().to_owned(), - observed_frontier: value.observed_frontier().value(), - committed_frontier: value.committed_frontier().value(), - target_watermark: value.target_watermark().value(), - request: SessionCoverageRequestV1 { - mode: coverage_mode(value.request().mode()), - }, - covered_intervals: value - .covered_intervals() - .iter() - .cloned() - .map(coverage_interval) - .collect(), - missing_intervals: value - .missing_intervals() - .iter() - .cloned() - .map(coverage_interval) - .collect(), - state: coverage_state(value.state()), - reason: coverage_reason(value.reason()), - } -} - -fn coverage_interval(value: SessionSourceCoverageIntervalV1) -> SessionCoverageIntervalV1 { - SessionCoverageIntervalV1 { - knowledge: closed_interval(value.knowledge), - valid: match value.valid { - DomainValidCoverageIntervalV1::Known(interval) => { - ValidCoverageIntervalV1::Known(closed_interval(interval)) - } - DomainValidCoverageIntervalV1::Unknown => ValidCoverageIntervalV1::Unknown, - }, - } -} - -fn closed_interval(value: tracedecay_domain::ClosedUtcIntervalV1) -> ClosedUtcIntervalV1 { - ClosedUtcIntervalV1 { - from_inclusive: value.from_inclusive().map(|value| value.0), - through_inclusive: value.through_inclusive().map(|value| value.0), - } -} - -const fn coverage_mode(value: TemporalModeV1) -> SessionCoverageModeV1 { - match value { - TemporalModeV1::Current => SessionCoverageModeV1::Current, - TemporalModeV1::AsOf { cutoff } => SessionCoverageModeV1::AsOf { cutoff: cutoff.0 }, - TemporalModeV1::Evolution => SessionCoverageModeV1::Evolution, - TemporalModeV1::Forensic => SessionCoverageModeV1::Forensic, - } -} - -const fn coverage_state(value: SessionSourceCoverageStateV1) -> SessionCoverageStateV1 { - match value { - SessionSourceCoverageStateV1::Fresh => SessionCoverageStateV1::Fresh, - SessionSourceCoverageStateV1::Stale => SessionCoverageStateV1::Stale, - SessionSourceCoverageStateV1::Partial => SessionCoverageStateV1::Partial, - SessionSourceCoverageStateV1::Locked => SessionCoverageStateV1::Locked, - SessionSourceCoverageStateV1::Redacted => SessionCoverageStateV1::Redacted, - SessionSourceCoverageStateV1::RetentionWithheld => { - SessionCoverageStateV1::RetentionWithheld - } - SessionSourceCoverageStateV1::Unavailable => SessionCoverageStateV1::Unavailable, - } -} - -fn coverage_reason(value: &SessionSourceCoverageReasonV1) -> SessionCoverageReasonV1 { - match value { - SessionSourceCoverageReasonV1::CaughtUp => SessionCoverageReasonV1::CaughtUp, - SessionSourceCoverageReasonV1::ProjectionBehindSource { lag } => { - SessionCoverageReasonV1::ProjectionBehindSource { lag: *lag } - } - SessionSourceCoverageReasonV1::SourceBehindTarget { lag } => { - SessionCoverageReasonV1::SourceBehindTarget { lag: *lag } - } - SessionSourceCoverageReasonV1::ProjectionAndSourceBehind { - projection_lag, - source_lag, - } => SessionCoverageReasonV1::ProjectionAndSourceBehind { - projection_lag: *projection_lag, - source_lag: *source_lag, - }, - SessionSourceCoverageReasonV1::Locked => SessionCoverageReasonV1::Locked, - SessionSourceCoverageReasonV1::Redacted => SessionCoverageReasonV1::Redacted, - SessionSourceCoverageReasonV1::RetentionWithheld => { - SessionCoverageReasonV1::RetentionWithheld - } - SessionSourceCoverageReasonV1::Unavailable => SessionCoverageReasonV1::Unavailable, - } -} - pub(super) fn sliced_message( result: SessionMessageSearchResult, slice: LcmContentSlice, @@ -595,19 +488,6 @@ fn rebuild_synthesis_user_prompt(result: &mut LcmExpandQueryResultV1) { synthesis.user = format!("QUESTION:\n{prompt}\n\nEXPANDED CONTEXT:\n{context}"); } -pub(super) const fn hydration(value: HydrationStateV1) -> HydrationStateResultV1 { - match value { - HydrationStateV1::Available => HydrationStateResultV1::Available, - HydrationStateV1::RetainedButUnavailable => HydrationStateResultV1::RetainedButUnavailable, - HydrationStateV1::Redacted => HydrationStateResultV1::Redacted, - HydrationStateV1::Deleted => HydrationStateResultV1::Deleted, - HydrationStateV1::RetentionExpired => HydrationStateResultV1::RetentionExpired, - HydrationStateV1::Unauthorized => HydrationStateResultV1::Unauthorized, - HydrationStateV1::Locked => HydrationStateResultV1::Locked, - HydrationStateV1::UnverifiableLegacy => HydrationStateResultV1::UnverifiableLegacy, - } -} - const fn storage_kind(value: LcmStorageKind) -> LcmStorageKindV1 { match value { LcmStorageKind::Inline => LcmStorageKindV1::Inline, diff --git a/crates/tracedecay-session-runtime/src/retained/session.rs b/crates/tracedecay-session-runtime/src/retained/session.rs index 5ce05e0b45..a2af350716 100644 --- a/crates/tracedecay-session-runtime/src/retained/session.rs +++ b/crates/tracedecay-session-runtime/src/retained/session.rs @@ -3,16 +3,12 @@ use std::sync::Arc; use std::time::Duration; use tracedecay_contracts::retained_surfaces::{ - ClosedUtcIntervalV1, GitScopeV1, HydrationStateResultV1, MessageRelationshipScopeV1, - MessageSearchHitV1, MessageSearchRequestV1, MessageSearchResultV1, MessageTypeFilterV1, - RetainedOutcomeStatusV1, RetainedSurfaceOperation, RetainedSurfaceResultV1, - SessionCoverageIntervalV1, SessionCoverageModeV1, SessionCoverageReasonV1, - SessionCoverageRequestV1, SessionCoverageStateV1, SessionMessageV1, SessionRecordV1, - SessionRefreshRequestV1, SessionRefreshScopeV1, - SessionSourceCoverageV1 as WireSourceCoverageV1, SessionsForRequestV1, - TemporalCoverageOmissionV1, TemporalCoverageV1, TemporalExplanationV1, TemporalFreshnessV1, - TemporalMetadataV1, TemporalOmissionV1, TemporalPopulationCountV1, TemporalWatermarksV1, - ValidCoverageIntervalV1, WorkflowsRequestV1, + GitScopeV1, MessageRelationshipScopeV1, MessageSearchHitV1, MessageSearchRequestV1, + MessageSearchResultV1, MessageTypeFilterV1, RetainedOutcomeStatusV1, RetainedSurfaceOperation, + RetainedSurfaceResultV1, SessionMessageV1, SessionRecordV1, SessionRefreshRequestV1, + SessionRefreshScopeV1, SessionsForRequestV1, TemporalCoverageOmissionV1, TemporalExplanationV1, + TemporalFreshnessV1, TemporalMetadataV1, TemporalOmissionV1, TemporalPopulationCountV1, + WorkflowsRequestV1, }; use tracedecay_contracts::{ ApplicationOutcome, RequestAdmission, RetainedSessionExecutionPortV1, RetainedSessionRequestV1, @@ -20,10 +16,8 @@ use tracedecay_contracts::{ RetainedSurfaceExecutionFutureV1, now_micros, }; use tracedecay_domain::{ - HydrationStateV1, ManifestDigest, ProjectId, RetrievalGrainV1, SessionId, - SessionSourceCoverageIntervalV1, SessionSourceCoverageReasonV1, SessionSourceCoverageStateV1, - SessionSourceCoverageV1, TemporalCoverageCountsV1, TemporalModeV1, UserProfileId, - ValidCoverageIntervalV1 as DomainValidCoverageIntervalV1, canonical_sha256, + ManifestDigest, ProjectId, RetrievalGrainV1, SessionId, TemporalModeV1, UserProfileId, + canonical_sha256, }; use tracedecay_session_memory::context::{ResolvedSessionIdentity, SessionRootId, SessionStoreId}; use tracedecay_session_memory::session::{ @@ -1036,13 +1030,7 @@ fn temporal( .into_iter() .map(|anchor| anchor.as_str().to_owned()) .collect(), - watermarks: TemporalWatermarksV1 { - generation: value.watermarks.generation, - source: value.watermarks.source, - projection: value.watermarks.projection, - index: value.watermarks.index, - summary: value.watermarks.summary, - }, + watermarks: temporal_watermarks(value.watermarks), coverage: coverage(value.coverage), source_coverage: value .source_coverage @@ -1104,119 +1092,7 @@ fn coverage_omission(omission: SessionRetrievalCoverageOmissionView) -> Temporal } } -const fn coverage(value: TemporalCoverageCountsV1) -> TemporalCoverageV1 { - TemporalCoverageV1 { - visible: value.visible, - hidden: value.hidden, - unknown: value.unknown, - redacted: value.redacted, - } -} - -pub(super) fn source_coverage(value: SessionSourceCoverageV1) -> WireSourceCoverageV1 { - WireSourceCoverageV1 { - source_id: value.source_id().as_str().to_owned(), - observed_frontier: value.observed_frontier().value(), - committed_frontier: value.committed_frontier().value(), - target_watermark: value.target_watermark().value(), - request: SessionCoverageRequestV1 { - mode: coverage_mode(value.request().mode()), - }, - covered_intervals: value - .covered_intervals() - .iter() - .cloned() - .map(coverage_interval) - .collect(), - missing_intervals: value - .missing_intervals() - .iter() - .cloned() - .map(coverage_interval) - .collect(), - state: coverage_state(value.state()), - reason: coverage_reason(value.reason()), - } -} - -fn coverage_interval(value: SessionSourceCoverageIntervalV1) -> SessionCoverageIntervalV1 { - SessionCoverageIntervalV1 { - knowledge: ClosedUtcIntervalV1 { - from_inclusive: value.knowledge.from_inclusive().map(|value| value.0), - through_inclusive: value.knowledge.through_inclusive().map(|value| value.0), - }, - valid: match value.valid { - DomainValidCoverageIntervalV1::Known(interval) => { - ValidCoverageIntervalV1::Known(ClosedUtcIntervalV1 { - from_inclusive: interval.from_inclusive().map(|value| value.0), - through_inclusive: interval.through_inclusive().map(|value| value.0), - }) - } - DomainValidCoverageIntervalV1::Unknown => ValidCoverageIntervalV1::Unknown, - }, - } -} - -const fn coverage_mode(value: TemporalModeV1) -> SessionCoverageModeV1 { - match value { - TemporalModeV1::Current => SessionCoverageModeV1::Current, - TemporalModeV1::AsOf { cutoff } => SessionCoverageModeV1::AsOf { cutoff: cutoff.0 }, - TemporalModeV1::Evolution => SessionCoverageModeV1::Evolution, - TemporalModeV1::Forensic => SessionCoverageModeV1::Forensic, - } -} - -const fn coverage_state(value: SessionSourceCoverageStateV1) -> SessionCoverageStateV1 { - match value { - SessionSourceCoverageStateV1::Fresh => SessionCoverageStateV1::Fresh, - SessionSourceCoverageStateV1::Stale => SessionCoverageStateV1::Stale, - SessionSourceCoverageStateV1::Partial => SessionCoverageStateV1::Partial, - SessionSourceCoverageStateV1::Locked => SessionCoverageStateV1::Locked, - SessionSourceCoverageStateV1::Redacted => SessionCoverageStateV1::Redacted, - SessionSourceCoverageStateV1::RetentionWithheld => { - SessionCoverageStateV1::RetentionWithheld - } - SessionSourceCoverageStateV1::Unavailable => SessionCoverageStateV1::Unavailable, - } -} - -fn coverage_reason(value: &SessionSourceCoverageReasonV1) -> SessionCoverageReasonV1 { - match value { - SessionSourceCoverageReasonV1::CaughtUp => SessionCoverageReasonV1::CaughtUp, - SessionSourceCoverageReasonV1::ProjectionBehindSource { lag } => { - SessionCoverageReasonV1::ProjectionBehindSource { lag: *lag } - } - SessionSourceCoverageReasonV1::SourceBehindTarget { lag } => { - SessionCoverageReasonV1::SourceBehindTarget { lag: *lag } - } - SessionSourceCoverageReasonV1::ProjectionAndSourceBehind { - projection_lag, - source_lag, - } => SessionCoverageReasonV1::ProjectionAndSourceBehind { - projection_lag: *projection_lag, - source_lag: *source_lag, - }, - SessionSourceCoverageReasonV1::Locked => SessionCoverageReasonV1::Locked, - SessionSourceCoverageReasonV1::Redacted => SessionCoverageReasonV1::Redacted, - SessionSourceCoverageReasonV1::RetentionWithheld => { - SessionCoverageReasonV1::RetentionWithheld - } - SessionSourceCoverageReasonV1::Unavailable => SessionCoverageReasonV1::Unavailable, - } -} - -const fn hydration(value: HydrationStateV1) -> HydrationStateResultV1 { - match value { - HydrationStateV1::Available => HydrationStateResultV1::Available, - HydrationStateV1::RetainedButUnavailable => HydrationStateResultV1::RetainedButUnavailable, - HydrationStateV1::Redacted => HydrationStateResultV1::Redacted, - HydrationStateV1::Deleted => HydrationStateResultV1::Deleted, - HydrationStateV1::RetentionExpired => HydrationStateResultV1::RetentionExpired, - HydrationStateV1::Unauthorized => HydrationStateResultV1::Unauthorized, - HydrationStateV1::Locked => HydrationStateResultV1::Locked, - HydrationStateV1::UnverifiableLegacy => HydrationStateResultV1::UnverifiableLegacy, - } -} +pub(super) use super::wire::{coverage, hydration, source_coverage, temporal_watermarks}; #[cfg(test)] mod refusal_tests { diff --git a/crates/tracedecay-session-runtime/src/retained/wire.rs b/crates/tracedecay-session-runtime/src/retained/wire.rs new file mode 100644 index 0000000000..376d2986e1 --- /dev/null +++ b/crates/tracedecay-session-runtime/src/retained/wire.rs @@ -0,0 +1,143 @@ +//! One retained projection of domain coverage and hydration onto wire results. + +use tracedecay_contracts::retained_surfaces::{ + ClosedUtcIntervalV1, HydrationStateResultV1, SessionCoverageIntervalV1, SessionCoverageModeV1, + SessionCoverageReasonV1, SessionCoverageRequestV1, SessionCoverageStateV1, + SessionSourceCoverageV1 as WireSourceCoverageV1, TemporalCoverageV1, TemporalWatermarksV1, + ValidCoverageIntervalV1, +}; +use tracedecay_domain::{ + ClosedUtcIntervalV1 as DomainClosedUtcIntervalV1, HydrationStateV1, + SessionSourceCoverageIntervalV1, SessionSourceCoverageReasonV1, SessionSourceCoverageStateV1, + SessionSourceCoverageV1, TemporalCoverageCountsV1, TemporalModeV1, + ValidCoverageIntervalV1 as DomainValidCoverageIntervalV1, +}; + +use crate::session_retrieval::SessionTemporalWatermarksView; + +pub(super) const fn hydration(value: HydrationStateV1) -> HydrationStateResultV1 { + match value { + HydrationStateV1::Available => HydrationStateResultV1::Available, + HydrationStateV1::RetainedButUnavailable => HydrationStateResultV1::RetainedButUnavailable, + HydrationStateV1::Redacted => HydrationStateResultV1::Redacted, + HydrationStateV1::Deleted => HydrationStateResultV1::Deleted, + HydrationStateV1::RetentionExpired => HydrationStateResultV1::RetentionExpired, + HydrationStateV1::Unauthorized => HydrationStateResultV1::Unauthorized, + HydrationStateV1::Locked => HydrationStateResultV1::Locked, + HydrationStateV1::UnverifiableLegacy => HydrationStateResultV1::UnverifiableLegacy, + } +} + +pub(super) const fn coverage(value: TemporalCoverageCountsV1) -> TemporalCoverageV1 { + TemporalCoverageV1 { + visible: value.visible, + hidden: value.hidden, + unknown: value.unknown, + redacted: value.redacted, + } +} + +pub(super) const fn temporal_watermarks( + value: SessionTemporalWatermarksView, +) -> TemporalWatermarksV1 { + TemporalWatermarksV1 { + generation: value.generation, + source: value.source, + projection: value.projection, + index: value.index, + summary: value.summary, + } +} + +pub(super) fn source_coverage(value: SessionSourceCoverageV1) -> WireSourceCoverageV1 { + WireSourceCoverageV1 { + source_id: value.source_id().as_str().to_owned(), + observed_frontier: value.observed_frontier().value(), + committed_frontier: value.committed_frontier().value(), + target_watermark: value.target_watermark().value(), + request: SessionCoverageRequestV1 { + mode: coverage_mode(value.request().mode()), + }, + covered_intervals: value + .covered_intervals() + .iter() + .cloned() + .map(coverage_interval) + .collect(), + missing_intervals: value + .missing_intervals() + .iter() + .cloned() + .map(coverage_interval) + .collect(), + state: coverage_state(value.state()), + reason: coverage_reason(value.reason()), + } +} + +fn coverage_interval(value: SessionSourceCoverageIntervalV1) -> SessionCoverageIntervalV1 { + SessionCoverageIntervalV1 { + knowledge: closed_interval(value.knowledge), + valid: match value.valid { + DomainValidCoverageIntervalV1::Known(interval) => { + ValidCoverageIntervalV1::Known(closed_interval(interval)) + } + DomainValidCoverageIntervalV1::Unknown => ValidCoverageIntervalV1::Unknown, + }, + } +} + +fn closed_interval(value: DomainClosedUtcIntervalV1) -> ClosedUtcIntervalV1 { + ClosedUtcIntervalV1 { + from_inclusive: value.from_inclusive().map(|value| value.0), + through_inclusive: value.through_inclusive().map(|value| value.0), + } +} + +const fn coverage_mode(value: TemporalModeV1) -> SessionCoverageModeV1 { + match value { + TemporalModeV1::Current => SessionCoverageModeV1::Current, + TemporalModeV1::AsOf { cutoff } => SessionCoverageModeV1::AsOf { cutoff: cutoff.0 }, + TemporalModeV1::Evolution => SessionCoverageModeV1::Evolution, + TemporalModeV1::Forensic => SessionCoverageModeV1::Forensic, + } +} + +const fn coverage_state(value: SessionSourceCoverageStateV1) -> SessionCoverageStateV1 { + match value { + SessionSourceCoverageStateV1::Fresh => SessionCoverageStateV1::Fresh, + SessionSourceCoverageStateV1::Stale => SessionCoverageStateV1::Stale, + SessionSourceCoverageStateV1::Partial => SessionCoverageStateV1::Partial, + SessionSourceCoverageStateV1::Locked => SessionCoverageStateV1::Locked, + SessionSourceCoverageStateV1::Redacted => SessionCoverageStateV1::Redacted, + SessionSourceCoverageStateV1::RetentionWithheld => { + SessionCoverageStateV1::RetentionWithheld + } + SessionSourceCoverageStateV1::Unavailable => SessionCoverageStateV1::Unavailable, + } +} + +fn coverage_reason(value: &SessionSourceCoverageReasonV1) -> SessionCoverageReasonV1 { + match value { + SessionSourceCoverageReasonV1::CaughtUp => SessionCoverageReasonV1::CaughtUp, + SessionSourceCoverageReasonV1::ProjectionBehindSource { lag } => { + SessionCoverageReasonV1::ProjectionBehindSource { lag: *lag } + } + SessionSourceCoverageReasonV1::SourceBehindTarget { lag } => { + SessionCoverageReasonV1::SourceBehindTarget { lag: *lag } + } + SessionSourceCoverageReasonV1::ProjectionAndSourceBehind { + projection_lag, + source_lag, + } => SessionCoverageReasonV1::ProjectionAndSourceBehind { + projection_lag: *projection_lag, + source_lag: *source_lag, + }, + SessionSourceCoverageReasonV1::Locked => SessionCoverageReasonV1::Locked, + SessionSourceCoverageReasonV1::Redacted => SessionCoverageReasonV1::Redacted, + SessionSourceCoverageReasonV1::RetentionWithheld => { + SessionCoverageReasonV1::RetentionWithheld + } + SessionSourceCoverageReasonV1::Unavailable => SessionCoverageReasonV1::Unavailable, + } +} From bc715fa2071d702b7b828c2ef7776a8952a5f8cf Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:03:53 +0000 Subject: [PATCH 095/182] refactor(pass-2/5): share projection generation decode Co-authored-by: Zack Jackson --- .../tracedecay-session-temporal-store/src/expand.rs | 10 +--------- .../src/hydration.rs | 6 +----- .../tracedecay-session-temporal-store/src/query.rs | 13 +++++++++---- .../src/refresh.rs | 12 ++---------- 4 files changed, 13 insertions(+), 28 deletions(-) diff --git a/crates/tracedecay-session-temporal-store/src/expand.rs b/crates/tracedecay-session-temporal-store/src/expand.rs index 9b0c501719..340f1f626b 100644 --- a/crates/tracedecay-session-temporal-store/src/expand.rs +++ b/crates/tracedecay-session-temporal-store/src/expand.rs @@ -22,7 +22,7 @@ use tracedecay_store::{ }; use tracedecay_temporal_query::ports::{ExecutionControl, TemporalPortError}; -use super::query::{now_micros, storage, storage_message}; +use super::query::{decode_generation_i64, now_micros, storage, storage_message}; use super::relations::{SessionRelationError, SummarySourceRef}; use super::retrieval::partial_summary_invalidation_exists; use super::store::execution_control_graph_cancellation; @@ -837,14 +837,6 @@ fn decode_summary_publication(encoded: &str) -> SessionStoreResult SessionStoreResult { - let generation = u64::try_from(value).map_err(|error| storage(operation, error))?; - SessionProjectionGenerationV1::new(generation).map_err(SessionStoreError::from) -} - fn decode_frozen_watermarks( encoded: &str, active_generation: SessionProjectionGenerationV1, diff --git a/crates/tracedecay-session-temporal-store/src/hydration.rs b/crates/tracedecay-session-temporal-store/src/hydration.rs index 36447df97d..f99530ef20 100644 --- a/crates/tracedecay-session-temporal-store/src/hydration.rs +++ b/crates/tracedecay-session-temporal-store/src/hydration.rs @@ -842,7 +842,7 @@ async fn open_occurrence_content( fn content_matches_descriptor(content: &[u8], descriptor: &PayloadDescriptor) -> bool { content.len() == descriptor.byte_count - && content_hash_matches(&descriptor.content_hash, content) + && content_hash_equals(&descriptor.content_hash, &sha256_hex(content)) } pub(super) fn hydration_failure(error: impl std::fmt::Display) -> HydrationError { @@ -1417,10 +1417,6 @@ fn nonnegative_usize(value: Option) -> Result { .ok_or_else(|| hydration_failure("payload size is not a nonnegative usize")) } -fn content_hash_matches(expected: &str, bytes: &[u8]) -> bool { - content_hash_equals(expected, &sha256_hex(bytes)) -} - fn content_hash_equals(expected: &str, actual_hex: &str) -> bool { sha256_hex_suffix(expected).unwrap_or(expected) == actual_hex } diff --git a/crates/tracedecay-session-temporal-store/src/query.rs b/crates/tracedecay-session-temporal-store/src/query.rs index ae0ab6b4de..fbaa55c50c 100644 --- a/crates/tracedecay-session-temporal-store/src/query.rs +++ b/crates/tracedecay-session-temporal-store/src/query.rs @@ -57,6 +57,14 @@ pub(super) fn frontier_i64(frontier: u64, operation: &'static str) -> SessionSto i64::try_from(frontier).map_err(|error| storage(operation, error)) } +pub(super) fn decode_generation_i64( + value: i64, + operation: &'static str, +) -> SessionStoreResult { + let value = u64::try_from(value).map_err(|error| storage(operation, error))?; + SessionProjectionGenerationV1::new(value).map_err(SessionStoreError::from) +} + pub(super) fn encode_watermarks( watermarks: &SessionFrozenWatermarksV1, operation: &'static str, @@ -128,10 +136,7 @@ pub(super) async fn read_active_generation( return Ok(None); }; let value: i64 = row.get(0).map_err(|error| storage(operation, error))?; - let value = u64::try_from(value).map_err(|error| storage(operation, error))?; - SessionProjectionGenerationV1::new(value) - .map(Some) - .map_err(SessionStoreError::from) + decode_generation_i64(value, operation).map(Some) } pub(super) async fn require_active_generation( diff --git a/crates/tracedecay-session-temporal-store/src/refresh.rs b/crates/tracedecay-session-temporal-store/src/refresh.rs index 027e8932f6..ae80471ed9 100644 --- a/crates/tracedecay-session-temporal-store/src/refresh.rs +++ b/crates/tracedecay-session-temporal-store/src/refresh.rs @@ -26,8 +26,8 @@ use super::projection::{ validate_final_projection_receipt, }; use super::query::{ - encode_watermarks, frontier_i64, generation_i64, now_micros, read_generation, storage, - storage_message, + decode_generation_i64, encode_watermarks, frontier_i64, generation_i64, now_micros, + read_generation, storage, storage_message, }; use super::rebuild::{ checkpoint_relation_rebuild_control, rebuild_candidate_session_relations, @@ -1259,14 +1259,6 @@ async fn next_generation( decode_generation_i64(value, BEGIN_REFRESH) } -fn decode_generation_i64( - value: i64, - operation: &'static str, -) -> SessionStoreResult { - let value = u64::try_from(value).map_err(|error| storage(operation, error))?; - SessionProjectionGenerationV1::new(value).map_err(SessionStoreError::from) -} - const SQLITE_CONSTRAINT: i32 = 19; const SQLITE_CONSTRAINT_PRIMARYKEY: i32 = 1555; const SQLITE_CONSTRAINT_UNIQUE: i32 = 2067; From 06048a265ed37dbe85b7a6e51a98e60ca259f0d2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:03:53 +0000 Subject: [PATCH 096/182] refactor(pass-3/5): share OpenCode scan error helper Co-authored-by: Zack Jackson --- .../src/runtime/hosts/opencode.rs | 4 ++-- .../src/runtime/hosts/opencode_part_scan.rs | 24 +++---------------- .../src/runtime/hosts/opencode_snapshot.rs | 13 +--------- 3 files changed, 6 insertions(+), 35 deletions(-) diff --git a/crates/tracedecay-sessions/src/runtime/hosts/opencode.rs b/crates/tracedecay-sessions/src/runtime/hosts/opencode.rs index 356ab24ef3..dfd422ba9a 100644 --- a/crates/tracedecay-sessions/src/runtime/hosts/opencode.rs +++ b/crates/tracedecay-sessions/src/runtime/hosts/opencode.rs @@ -978,7 +978,7 @@ fn opencode_data_dir(home: &Path) -> PathBuf { } } -fn scan_error( +pub(super) fn scan_error( operation: &'static str, path: &Path, error: impl std::error::Error + Send + Sync + 'static, @@ -990,7 +990,7 @@ fn scan_error( } } -const fn invalid_frame() -> TranscriptIngestError { +pub(super) const fn invalid_frame() -> TranscriptIngestError { TranscriptIngestError::InvalidFrameState { provider: PROVIDER } } diff --git a/crates/tracedecay-sessions/src/runtime/hosts/opencode_part_scan.rs b/crates/tracedecay-sessions/src/runtime/hosts/opencode_part_scan.rs index 70cbc65be4..867903f1ac 100644 --- a/crates/tracedecay-sessions/src/runtime/hosts/opencode_part_scan.rs +++ b/crates/tracedecay-sessions/src/runtime/hosts/opencode_part_scan.rs @@ -4,16 +4,14 @@ use std::path::Path; use rusqlite::params; use crate::runtime::host_scan::HostScanBudget; -use crate::runtime::source::{TranscriptIngestError, TranscriptIngestResult}; +use crate::runtime::source::TranscriptIngestResult; use super::opencode::{ MAX_ID_BYTES, MAX_MESSAGES_PER_PAGE, OpenCodeMessageRef, OpenCodePageCursor, - OpenCodeReferencePage, OpenCodeScanSource, install_progress_handler, open_scan_connection, - sql_text, + OpenCodeReferencePage, OpenCodeScanSource, install_progress_handler, invalid_frame, + open_scan_connection, scan_error, sql_text, }; -const PROVIDER: &str = "opencode"; - pub(super) fn scan_part_reference_page( source: &OpenCodeScanSource, cursor: OpenCodePageCursor, @@ -177,19 +175,3 @@ pub(super) fn scan_part_reference_page( budget, )) } - -fn scan_error( - operation: &'static str, - path: &Path, - error: impl std::error::Error + Send + Sync + 'static, -) -> TranscriptIngestError { - TranscriptIngestError::ScanIo { - operation, - path: path.to_path_buf(), - source: std::io::Error::other(error), - } -} - -const fn invalid_frame() -> TranscriptIngestError { - TranscriptIngestError::InvalidFrameState { provider: PROVIDER } -} diff --git a/crates/tracedecay-sessions/src/runtime/hosts/opencode_snapshot.rs b/crates/tracedecay-sessions/src/runtime/hosts/opencode_snapshot.rs index 817973c624..bc63b8548f 100644 --- a/crates/tracedecay-sessions/src/runtime/hosts/opencode_snapshot.rs +++ b/crates/tracedecay-sessions/src/runtime/hosts/opencode_snapshot.rs @@ -5,6 +5,7 @@ use sha2::{Digest, Sha256}; use tracedecay_domain::ObservationSourceGenerationV1; use tracedecay_runtime_core::sqlite_read_snapshot::SnapshotDatabase; +use super::opencode::scan_error; use crate::runtime::host_scan::HostScanBudget; use crate::runtime::source::{TranscriptIngestError, TranscriptIngestResult}; @@ -188,15 +189,3 @@ pub(super) fn snapshot_scratch_root() -> Option { .ok() .map(|root| root.join("scratch/sqlite-read/opencode")) } - -fn scan_error( - operation: &'static str, - path: &Path, - error: impl std::error::Error + Send + Sync + 'static, -) -> TranscriptIngestError { - TranscriptIngestError::ScanIo { - operation, - path: path.to_path_buf(), - source: std::io::Error::other(error), - } -} From 83204b67d0e178c53593a24ab311444e96a1a508 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:04:13 +0000 Subject: [PATCH 097/182] refactor(pass-4/5): flatten search parse and projection bounds Co-authored-by: Zack Jackson --- .../src/retained/session.rs | 73 ++++++++----------- .../src/relation_projection.rs | 43 ++++++----- 2 files changed, 53 insertions(+), 63 deletions(-) diff --git a/crates/tracedecay-session-runtime/src/retained/session.rs b/crates/tracedecay-session-runtime/src/retained/session.rs index a2af350716..3ca8efd852 100644 --- a/crates/tracedecay-session-runtime/src/retained/session.rs +++ b/crates/tracedecay-session-runtime/src/retained/session.rs @@ -3,12 +3,11 @@ use std::sync::Arc; use std::time::Duration; use tracedecay_contracts::retained_surfaces::{ - GitScopeV1, MessageRelationshipScopeV1, MessageSearchHitV1, MessageSearchRequestV1, - MessageSearchResultV1, MessageTypeFilterV1, RetainedOutcomeStatusV1, RetainedSurfaceOperation, - RetainedSurfaceResultV1, SessionMessageV1, SessionRecordV1, SessionRefreshRequestV1, - SessionRefreshScopeV1, SessionsForRequestV1, TemporalCoverageOmissionV1, TemporalExplanationV1, - TemporalFreshnessV1, TemporalMetadataV1, TemporalOmissionV1, TemporalPopulationCountV1, - WorkflowsRequestV1, + GitScopeV1, MessageSearchHitV1, MessageSearchRequestV1, MessageSearchResultV1, + RetainedOutcomeStatusV1, RetainedSurfaceOperation, RetainedSurfaceResultV1, SessionMessageV1, + SessionRecordV1, SessionRefreshRequestV1, SessionRefreshScopeV1, SessionsForRequestV1, + TemporalCoverageOmissionV1, TemporalExplanationV1, TemporalFreshnessV1, TemporalMetadataV1, + TemporalOmissionV1, TemporalPopulationCountV1, WorkflowsRequestV1, }; use tracedecay_contracts::{ ApplicationOutcome, RequestAdmission, RetainedSessionExecutionPortV1, RetainedSessionRequestV1, @@ -439,6 +438,30 @@ impl RetainedSessionExecutionPortV1 for DirectRetainedSessionPortV1<'_> { } } +/// The provider parser names the offending value and the accepted set; keep +/// that corrective diagnostic in the refusal instead of collapsing it to the +/// generic invalid-request problem. A value the sanitized diagnostic cannot +/// carry (oversized or control characters) still refuses with the generic +/// problem. +fn invalid_provider(error: String) -> RetainedSurfaceExecutionErrorV1 { + tracedecay_contracts::SafeDiagnostic::new( + "application.retained.message-search-provider-invalid", + error, + ) + .map_or( + RetainedSurfaceExecutionErrorV1::InvalidRequest, + |diagnostic| { + RetainedSurfaceExecutionErrorV1::ApplicationProblem( + tracedecay_contracts::ApplicationProblem::InvalidRequest { + diagnostic, + retry: tracedecay_contracts::RetryDirective::Never, + legal_actions: vec![tracedecay_contracts::LegalAction::CorrectRequest], + }, + ) + }, + ) +} + struct MessageSearchInput { query: String, goals: bool, @@ -467,49 +490,17 @@ impl MessageSearchInput { None if goals => String::new(), None => return Err(RetainedSurfaceExecutionErrorV1::InvalidRequest), }; - // The provider parser names the offending value and the accepted set; - // keep that corrective diagnostic in the refusal instead of collapsing - // it to the generic invalid-request problem. A value the sanitized - // diagnostic cannot carry (oversized or control characters) still - // refuses with the generic problem. let provider = - ProviderScope::parse_optional(request.provider.as_deref()).map_err(|error| { - tracedecay_contracts::SafeDiagnostic::new( - "application.retained.message-search-provider-invalid", - error.clone(), - ) - .map_or( - RetainedSurfaceExecutionErrorV1::InvalidRequest, - |diagnostic| { - RetainedSurfaceExecutionErrorV1::ApplicationProblem( - tracedecay_contracts::ApplicationProblem::InvalidRequest { - diagnostic, - retry: tracedecay_contracts::RetryDirective::Never, - legal_actions: vec![ - tracedecay_contracts::LegalAction::CorrectRequest, - ], - }, - ) - }, - ) - })?; + ProviderScope::parse_optional(request.provider.as_deref()).map_err(invalid_provider)?; let include_subagents = request.include_subagents.unwrap_or(true); - let mut scope = match request.scope.unwrap_or(MessageRelationshipScopeV1::All) { - MessageRelationshipScopeV1::All => SessionSearchScope::All, - MessageRelationshipScopeV1::ParentsOnly => SessionSearchScope::ParentsOnly, - MessageRelationshipScopeV1::SubagentsOnly => SessionSearchScope::SubagentsOnly, - }; + let mut scope = super::lcm::relationship_scope(request.scope); if !include_subagents && scope == SessionSearchScope::SubagentsOnly { return Err(RetainedSurfaceExecutionErrorV1::InvalidRequest); } if !include_subagents && scope == SessionSearchScope::All { scope = SessionSearchScope::ParentsOnly; } - let message_type = match request.message_type.unwrap_or(MessageTypeFilterV1::All) { - MessageTypeFilterV1::All => SessionMessageType::All, - MessageTypeFilterV1::DirectUser => SessionMessageType::DirectUser, - MessageTypeFilterV1::ToolResult => SessionMessageType::ToolResult, - }; + let message_type = super::lcm::message_type(request.message_type); let workflow_run = optional_string(request.workflow_run.as_deref())?; let workflow_agent = optional_string(request.workflow_agent.as_deref())?; if workflow_agent.is_some() && workflow_run.is_none() { diff --git a/crates/tracedecay-session-temporal-store/src/relation_projection.rs b/crates/tracedecay-session-temporal-store/src/relation_projection.rs index 520304afb8..ccedf01482 100644 --- a/crates/tracedecay-session-temporal-store/src/relation_projection.rs +++ b/crates/tracedecay-session-temporal-store/src/relation_projection.rs @@ -1198,38 +1198,37 @@ async fn reconstruct_session_metadata( Ok((parent, memberships)) } +fn projection_budget(count: usize, add: usize) -> SessionStoreResult { + count + .checked_add(add) + .ok_or_else(|| storage(RECONSTRUCT_OPERATION, SessionRelationError::BudgetExhausted)) +} + fn enforce_projection_bounds( projection: &SessionRelationProjection, max_entities: usize, max_relations: usize, ) -> SessionStoreResult<()> { - let summary_relations = projection - .summaries - .iter() - .try_fold(0usize, |count, summary| { - count.checked_add(summary.sources.len()).and_then(|count| { - count.checked_add(usize::from(summary.predecessor_summary_id.is_some())) - }) - }) - .ok_or_else(|| storage(RECONSTRUCT_OPERATION, SessionRelationError::BudgetExhausted))?; - let relation_count = [ + let mut summary_relations = 0usize; + for summary in &projection.summaries { + summary_relations = projection_budget(summary_relations, summary.sources.len())?; + summary_relations = projection_budget( + summary_relations, + usize::from(summary.predecessor_summary_id.is_some()), + )?; + } + let mut relation_count = summary_relations; + for count in [ projection.logical_copies.len(), projection.thread_hierarchy.len(), projection.agent_hierarchy.len(), usize::from(projection.parent_session_id.is_some()), projection.workflow_agents.len(), - ] - .into_iter() - .try_fold(summary_relations, usize::checked_add) - .ok_or_else(|| storage(RECONSTRUCT_OPERATION, SessionRelationError::BudgetExhausted))?; - let entity_count = - projection - .summaries - .len() - .checked_add(relation_count.checked_mul(2).ok_or_else(|| { - storage(RECONSTRUCT_OPERATION, SessionRelationError::BudgetExhausted) - })?) - .ok_or_else(|| storage(RECONSTRUCT_OPERATION, SessionRelationError::BudgetExhausted))?; + ] { + relation_count = projection_budget(relation_count, count)?; + } + let entity_count = projection_budget(relation_count, relation_count) + .and_then(|doubled| projection_budget(projection.summaries.len(), doubled))?; if relation_count > max_relations || entity_count > max_entities { return Err(storage( RECONSTRUCT_OPERATION, From 53cd7c1b388a13e2c75303bb634a689e9312a422 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:04:22 +0000 Subject: [PATCH 098/182] simplify(pass-2/5): fold duplicate code-index helpers Co-authored-by: Zack Jackson --- crates/tracedecay-code-index/src/chunks.rs | 33 ++++-------- .../tracedecay-code-index/src/generations.rs | 8 +-- .../src/graph_projection.rs | 25 +++------ .../tracedecay-code-index/src/incremental.rs | 7 +-- .../src/production/mod.rs | 52 +++++++------------ crates/tracedecay-code-index/src/receipts.rs | 9 +--- 6 files changed, 41 insertions(+), 93 deletions(-) diff --git a/crates/tracedecay-code-index/src/chunks.rs b/crates/tracedecay-code-index/src/chunks.rs index 15fd1f44b0..3074b39798 100644 --- a/crates/tracedecay-code-index/src/chunks.rs +++ b/crates/tracedecay-code-index/src/chunks.rs @@ -606,7 +606,16 @@ impl DeterministicCodeChunker { descriptor: &LanguageDescriptorV1, cancellation: &dyn ExtractionCancellation, ) -> Result { - self.build_file_artifacts(file, batch, descriptor, cancellation) + let mut clone_build = ClonePayloadBuildContextV1::new(None); + self.build_file_artifacts_with_parse( + file, + batch, + descriptor, + None, + self.sensitivity_level, + cancellation, + &mut clone_build, + ) } /// Index one receipt-bound file and return the opaque capability required @@ -1133,28 +1142,6 @@ impl CodeChunker for DeterministicCodeChunker { } impl DeterministicCodeChunker { - /// Build all parser-backed file artifacts. The legacy chunk-only port - /// delegates here so chunk, lineage, and graph evidence are always - /// derived from the same bounded parser result. - fn build_file_artifacts( - &self, - file: &ReceiptBoundCodeFileV1, - batch: &ExtractionBatchV1, - descriptor: &LanguageDescriptorV1, - cancellation: &dyn ExtractionCancellation, - ) -> Result { - let mut clone_build = ClonePayloadBuildContextV1::new(None); - self.build_file_artifacts_with_parse( - file, - batch, - descriptor, - None, - self.sensitivity_level, - cancellation, - &mut clone_build, - ) - } - #[allow(clippy::too_many_arguments)] fn build_file_artifacts_with_parse( &self, diff --git a/crates/tracedecay-code-index/src/generations.rs b/crates/tracedecay-code-index/src/generations.rs index cc5a2a79c5..9758a43310 100644 --- a/crates/tracedecay-code-index/src/generations.rs +++ b/crates/tracedecay-code-index/src/generations.rs @@ -663,10 +663,10 @@ fn parse_minted_generation_id(generation_id: &CodeGenerationId) -> Option<(Strin Some((discriminator.to_owned(), sequence.parse().ok()?)) } -/// A well-formed placeholder digest, replaced by the computed seal before the -/// manifest is returned. The seal payload excludes the seal itself, so the -/// placeholder never influences the computed digest. -fn placeholder_digest() -> ManifestDigest { +/// Canonical zero digest substituted until the caller computes the real digest. +/// Callers exclude this placeholder from that computation, so it never +/// influences the result. +pub(crate) fn placeholder_digest() -> ManifestDigest { ManifestDigest::new(format!("sha256:{}", "0".repeat(64))) .expect("a zeroed sha256 digest is canonical") } diff --git a/crates/tracedecay-code-index/src/graph_projection.rs b/crates/tracedecay-code-index/src/graph_projection.rs index 223b7386c1..c639740882 100644 --- a/crates/tracedecay-code-index/src/graph_projection.rs +++ b/crates/tracedecay-code-index/src/graph_projection.rs @@ -478,25 +478,12 @@ impl InMemoryCodeGraphProjectionBuilder { freshness: SourceFreshness, cancellation: &CancellationSignal, ) -> Result { - let snapshot = self - .snapshot - .read() - .map_err(|_| { - CodeGraphProjectionError::Unavailable( - "code graph verified snapshot lock is poisoned".to_owned(), - ) - })? - .clone() - .ok_or_else(|| { - CodeGraphProjectionError::Unavailable( - "code graph generation is not published".to_owned(), - ) - })?; - CodeGraphProjectionStore::from_verified_snapshot( - snapshot.as_ref().clone(), - generation.clone(), - )? - .evidence_reader(generation, repository_id, freshness, cancellation) + self.verified_store(generation)?.evidence_reader( + generation, + repository_id, + freshness, + cancellation, + ) } } diff --git a/crates/tracedecay-code-index/src/incremental.rs b/crates/tracedecay-code-index/src/incremental.rs index a153e075b8..75d6bf500d 100644 --- a/crates/tracedecay-code-index/src/incremental.rs +++ b/crates/tracedecay-code-index/src/incremental.rs @@ -19,7 +19,7 @@ use tracedecay_domain::{ }; use super::chunks::{ChunkingFailureV1, CodeFileChunksV1, symbol_occurrence_id}; -use super::generations::{FileExtractionActionV1, GenerationIncrementPlanV1}; +use super::generations::{FileExtractionActionV1, GenerationIncrementPlanV1, placeholder_digest}; use super::lineage::{ GenerationSymbolIndexV1, LineageResolutionErrorV1, LineageSymbolRecordV1, SymbolLineageCandidateV1, SymbolLineageResolver, @@ -659,11 +659,6 @@ fn map_lineage_error(error: LineageResolutionErrorV1) -> ChunkIncrementErrorV1 { ) } -fn placeholder_digest() -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", "0".repeat(64))) - .expect("a zeroed sha256 digest is canonical") -} - #[cfg(test)] mod tests { use std::collections::BTreeSet; diff --git a/crates/tracedecay-code-index/src/production/mod.rs b/crates/tracedecay-code-index/src/production/mod.rs index 2660154924..bc6af8723f 100644 --- a/crates/tracedecay-code-index/src/production/mod.rs +++ b/crates/tracedecay-code-index/src/production/mod.rs @@ -1947,7 +1947,7 @@ where let started = crate::hotpath_observe::start_build_to_queryable(); crate::hotpath_observe::record_generation_state("building"); crate::hotpath_observe::record_rebuild_state("unknown"); - Self::checkpoint(control)?; + lexical_page_source::checkpoint(control)?; let ignored_source_roster = IgnoredSourceRosterV1::admit( &request.snapshot, &request.repository_parse_identity, @@ -1956,7 +1956,7 @@ where let scope = CodeIndexGenerationScopeV1::for_snapshot(&request.snapshot); let lookup = self.lookup_active_generation(&scope)?; let active = lookup.reusable; - Self::checkpoint(control)?; + lexical_page_source::checkpoint(control)?; let intake = self.intake_at(request.sealed_at, registry_for_snapshot(&request.snapshot)?); let capability = intake @@ -1974,7 +1974,7 @@ where .fold(0_u64, u64::saturating_add), ); } - Self::checkpoint(control)?; + lexical_page_source::checkpoint(control)?; let planner = GenerationPlanner::new( self.config.project_id.clone(), @@ -2027,7 +2027,7 @@ where None, ), }; - Self::checkpoint(control)?; + lexical_page_source::checkpoint(control)?; let parser_registry = Arc::new(tracedecay_code_extraction::LanguageRegistry::new()); let extractor = TreeSitterExtractor::from_shared_registry(Arc::clone(&parser_registry)); @@ -2079,7 +2079,7 @@ where )); } }; - Self::checkpoint(control)?; + lexical_page_source::checkpoint(control)?; let candidate = hotpath::measure_block!("code_index.build.assemble", { let coverage = coverage_summary(&validated.snapshot, &staged.files); let changes = match (active.as_ref(), staged.parent_shared_occurrences.as_ref()) { @@ -2157,10 +2157,10 @@ where changes, &staged.chunks, )?; - Self::checkpoint(control)?; + lexical_page_source::checkpoint(control)?; let projection = project_for_publication(&mut self.projection, projection_request) .map_err(CodeIndexProductionErrorV1::Projection)?; - Self::checkpoint(control)?; + lexical_page_source::checkpoint(control)?; let imports = hotpath::measure_block!( "code_index.build.assemble.import_evidence", derive_import_evidence(&staged.files) @@ -2242,22 +2242,6 @@ where } } - fn checkpoint( - control: &dyn CodeIndexExecutionControlV1, - ) -> Result<(), CodeIndexProductionErrorV1> { - if control.is_cancelled() { - Err(CodeIndexProductionErrorV1::Interrupted( - CodeIndexInterruptionV1::Cancelled, - )) - } else if control.is_deadline_exceeded() { - Err(CodeIndexProductionErrorV1::Interrupted( - CodeIndexInterruptionV1::DeadlineExceeded, - )) - } else { - Ok(()) - } - } - fn interruption_error(control: &dyn CodeIndexExecutionControlV1) -> CodeIndexProductionErrorV1 { if control.is_deadline_exceeded() { CodeIndexProductionErrorV1::Interrupted(CodeIndexInterruptionV1::DeadlineExceeded) @@ -2295,7 +2279,7 @@ where CodeIndexProductionErrorV1, > { crate::hotpath_observe::measure_hot_loop!("code_index.materialize.file", { - Self::checkpoint(control)?; + lexical_page_source::checkpoint(control)?; let captured = captured_files .get(&file.file_occurrence_id) .ok_or(CodeIndexInputErrorV1::MissingCapturedFile)?; @@ -2334,7 +2318,7 @@ where u64::try_from(reused.artifacts.clone_bodies.len()).unwrap_or(u64::MAX), 0, ); - Self::checkpoint(control)?; + lexical_page_source::checkpoint(control)?; let clone_stats = ClonePayloadBuildStatsV1 { reused: u64::try_from(reused.artifacts.clone_bodies.len()).unwrap_or(u64::MAX), computed: 0, @@ -2384,7 +2368,7 @@ where control, ) { Ok((parse_artifacts, parsed_len)) => { - Self::checkpoint(control)?; + lexical_page_source::checkpoint(control)?; extractor .extract_preparsed( &receipt_bound, @@ -2408,12 +2392,12 @@ where .. }), ) => { - Self::checkpoint(control)?; + lexical_page_source::checkpoint(control)?; return Err(error); } Err(error) => return Err(error), }; - Self::checkpoint(control)?; + lexical_page_source::checkpoint(control)?; let (artifacts, exact_authority, clone_stats) = chunker .index_file_with_authority_from_extraction_reusing( &receipt_bound, @@ -2428,7 +2412,7 @@ where error => CodeIndexProductionErrorV1::Chunk(error), })?; physical_artifacts.record_clone_payloads(clone_stats.reused, clone_stats.computed); - Self::checkpoint(control)?; + lexical_page_source::checkpoint(control)?; let (authority, extraction, _) = extraction.into_parts(); let artifact = Arc::new(FileGenerationArtifactsV1 { authority, @@ -2518,11 +2502,11 @@ where // subsequent physical reuse remain deterministic. let mut files = Vec::with_capacity(extracted.len()); for (reuse_key, artifact, _) in extracted { - Self::checkpoint(control)?; + lexical_page_source::checkpoint(control)?; physical_artifacts.insert(reuse_key, &artifact); files.push(artifact); } - Self::checkpoint(control)?; + lexical_page_source::checkpoint(control)?; staged_generation(manifest.generation_id.clone(), files, Vec::new(), None) } @@ -2630,7 +2614,7 @@ where worker| -> Result<(usize, IncrementFileMaterializationV1), CodeIndexProductionErrorV1> { let (index, file_plan) = *item; - Self::checkpoint(control)?; + lexical_page_source::checkpoint(control)?; let materialization = match &file_plan.action { FileExtractionActionV1::CarryForward { file_occurrence_id, @@ -2816,7 +2800,7 @@ where let mut clone_stale_invalidations = 0_u64; for materialization in file_materializations { - Self::checkpoint(control)?; + lexical_page_source::checkpoint(control)?; match materialization { IncrementFileMaterializationV1::CarryForward { artifact, @@ -2851,7 +2835,7 @@ where } } } - Self::checkpoint(control)?; + lexical_page_source::checkpoint(control)?; let mut staged = staged_generation( manifest.generation_id.clone(), diff --git a/crates/tracedecay-code-index/src/receipts.rs b/crates/tracedecay-code-index/src/receipts.rs index a9047e881e..5c99953a22 100644 --- a/crates/tracedecay-code-index/src/receipts.rs +++ b/crates/tracedecay-code-index/src/receipts.rs @@ -30,6 +30,8 @@ use tracedecay_domain::{ ProjectionOperationV1, ProjectionOutcomeV1, ProjectionReplayReasonV1, canonical_sha256, }; +use super::generations::placeholder_digest; + /// Domain separator for the canonical projection-batch-request digest. pub const PROJECTION_REQUEST_SEPARATOR: &str = "tracedecay.projection-batch-request.v1"; @@ -558,13 +560,6 @@ fn check_decision( Ok(()) } -/// A well-formed placeholder digest, replaced by the computed publication -/// digest before the batch is returned. -fn placeholder_digest() -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", "0".repeat(64))) - .expect("a zeroed sha256 digest is canonical") -} - #[cfg(test)] mod tests { use super::*; From 3a4a85d040f2ed8fd6daddb98769e716bd893d79 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:04:30 +0000 Subject: [PATCH 099/182] refactor(pass-5/5): collapse duplicate filter and hydration maps Co-authored-by: Zack Jackson --- .../src/fact_store/crud/add.rs | 20 ++++-------- .../src/retained/session.rs | 18 +++-------- .../src/session_retrieval.rs | 1 + .../src/session_retrieval/contract.rs | 32 +++++++++++++------ .../src/relations/projection_read.rs | 5 +-- .../src/relations/read.rs | 2 +- 6 files changed, 36 insertions(+), 42 deletions(-) diff --git a/crates/tracedecay-session-memory/src/fact_store/crud/add.rs b/crates/tracedecay-session-memory/src/fact_store/crud/add.rs index 4db35a9d31..c9a395be58 100644 --- a/crates/tracedecay-session-memory/src/fact_store/crud/add.rs +++ b/crates/tracedecay-session-memory/src/fact_store/crud/add.rs @@ -5,14 +5,14 @@ use super::super::primitives::{ }; use super::super::projection::load_project_memory_projection_tx; use super::super::scoring::{ - project_memory_fact_vector, project_memory_jaccard, project_memory_millionths, - project_memory_tokens, + project_memory_fact_vector, project_memory_holographic_error, project_memory_jaccard, + project_memory_millionths, project_memory_tokens, }; use crate::memory::diff::{ ADD_COMPARISON_REPORT_FLOOR_MILLIONTHS, NEAR_DUPLICATE_SCORE_MILLIONTHS, POSSIBLE_CONFLICT_SCORE_MILLIONTHS, contains_negation_cue, normalized_equivalent, }; -use crate::memory::encoding::{HolographicEncoder, HolographicEncodingError}; +use crate::memory::encoding::HolographicEncoder; use tracedecay_domain::{FactId, FactOwnerV1}; use tracedecay_runtime_core::db::DatabaseMemoryTransaction as Transaction; use tracedecay_runtime_core::db::engine::params; @@ -34,14 +34,6 @@ pub(super) enum ProjectMemoryAddClassification { }, } -fn holographic_store_error(error: HolographicEncodingError) -> FactStoreError { - match error { - HolographicEncodingError::DimensionMismatch { expected, actual } => { - FactStoreError::HolographicDimensionMismatch { expected, actual } - } - } -} - fn classification_similarity( encoder: &HolographicEncoder, proposed_tokens: &[String], @@ -51,10 +43,10 @@ fn classification_similarity( let candidate_tokens = project_memory_tokens(candidate.content()); let mut similarity = project_memory_jaccard(proposed_tokens, &candidate_tokens); let candidate_vector = - project_memory_fact_vector(encoder, candidate).map_err(holographic_store_error)?; + project_memory_fact_vector(encoder, candidate).map_err(project_memory_holographic_error)?; let holographic = encoder .similarity(proposed_vector, &candidate_vector) - .map_err(holographic_store_error)?; + .map_err(project_memory_holographic_error)?; if holographic >= 0.85 && holographic > similarity { similarity = holographic; } @@ -156,7 +148,7 @@ pub(super) async fn classify_project_memory_add_tx( let proposed_tokens = project_memory_tokens(content); let proposed_vector = encoder .encode_fact(content, entities) - .map_err(holographic_store_error)?; + .map_err(project_memory_holographic_error)?; let mut closest: Option<(&ProjectMemoryFactV1, u32)> = None; for candidate in &candidates { let similarity = diff --git a/crates/tracedecay-session-runtime/src/retained/session.rs b/crates/tracedecay-session-runtime/src/retained/session.rs index 3ca8efd852..11e3792edf 100644 --- a/crates/tracedecay-session-runtime/src/retained/session.rs +++ b/crates/tracedecay-session-runtime/src/retained/session.rs @@ -30,8 +30,7 @@ use tracedecay_sessions::runtime::{ }; use tracedecay_temporal_query::context::ContextBudget; use tracedecay_temporal_query::ports::{ - TemporalCandidateFilterV1, TemporalCandidatePopulationCount, TemporalMessageTypeFilterV1, - TemporalSessionScopeFilterV1, + TemporalCandidateFilterV1, TemporalCandidatePopulationCount, }; use tracedecay_temporal_query::ranking::DiversityLimits; @@ -42,7 +41,8 @@ use super::session_refresh::{ use crate::session_retrieval::{ DaemonSessionRetrievalService, SessionApplicationRetrievalPortV1, SessionRetrievalCoverageOmissionView, SessionRetrievalPageView, SessionRetrievalServiceOutcome, - SessionRetrievalStoreScope, SessionTemporalMetadataView, + SessionRetrievalStoreScope, SessionTemporalMetadataView, temporal_message_type, + temporal_session_scope, }; use tracedecay_contracts::retained_receipts::{evidence_outcome, session_refresh_effect_outcome}; use tracedecay_domain::errors::TraceDecayError; @@ -549,16 +549,8 @@ impl MessageSearchInput { parent_session_id: self.parent_session_id.clone(), source: None, include_summaries: false, - session_scope: match self.scope { - SessionSearchScope::All => TemporalSessionScopeFilterV1::All, - SessionSearchScope::ParentsOnly => TemporalSessionScopeFilterV1::ParentsOnly, - SessionSearchScope::SubagentsOnly => TemporalSessionScopeFilterV1::SubagentsOnly, - }, - message_type: match self.message_type { - SessionMessageType::All => TemporalMessageTypeFilterV1::All, - SessionMessageType::DirectUser => TemporalMessageTypeFilterV1::DirectUser, - SessionMessageType::ToolResult => TemporalMessageTypeFilterV1::ToolResult, - }, + session_scope: temporal_session_scope(self.scope), + message_type: temporal_message_type(self.message_type), roles: Vec::new(), start_time: self.since, end_time: self.until, diff --git a/crates/tracedecay-session-runtime/src/session_retrieval.rs b/crates/tracedecay-session-runtime/src/session_retrieval.rs index 9ff356d24d..2de0cd6590 100644 --- a/crates/tracedecay-session-runtime/src/session_retrieval.rs +++ b/crates/tracedecay-session-runtime/src/session_retrieval.rs @@ -106,6 +106,7 @@ pub use contract::{ SessionRetrievalServiceOutcome, SessionRetrievalStoreScope, SessionRetrievalUnavailable, SessionRetrievalUnavailableReason, SessionTemporalMetadataView, SessionTemporalWatermarksView, }; +pub(crate) use contract::{temporal_message_type, temporal_session_scope}; pub use primitive::DaemonSessionLookupPrimitiveV1; /// Serving identity of the store the daemon currently serves, extracted diff --git a/crates/tracedecay-session-runtime/src/session_retrieval/contract.rs b/crates/tracedecay-session-runtime/src/session_retrieval/contract.rs index 89b70a57a4..776be06888 100644 --- a/crates/tracedecay-session-runtime/src/session_retrieval/contract.rs +++ b/crates/tracedecay-session-runtime/src/session_retrieval/contract.rs @@ -69,6 +69,26 @@ impl SessionRetrievalCommand { } } +pub(crate) const fn temporal_session_scope( + scope: SessionSearchScope, +) -> TemporalSessionScopeFilterV1 { + match scope { + SessionSearchScope::All => TemporalSessionScopeFilterV1::All, + SessionSearchScope::ParentsOnly => TemporalSessionScopeFilterV1::ParentsOnly, + SessionSearchScope::SubagentsOnly => TemporalSessionScopeFilterV1::SubagentsOnly, + } +} + +pub(crate) const fn temporal_message_type( + message_type: SessionMessageType, +) -> TemporalMessageTypeFilterV1 { + match message_type { + SessionMessageType::All => TemporalMessageTypeFilterV1::All, + SessionMessageType::DirectUser => TemporalMessageTypeFilterV1::DirectUser, + SessionMessageType::ToolResult => TemporalMessageTypeFilterV1::ToolResult, + } +} + fn temporal_candidate_filter( filters: &SessionRetrievalFilters, goals: bool, @@ -81,16 +101,8 @@ fn temporal_candidate_filter( parent_session_id: filters.parent_session_id.clone(), source: filters.source.clone(), include_summaries: filters.include_summaries, - session_scope: match filters.scope { - SessionSearchScope::All => TemporalSessionScopeFilterV1::All, - SessionSearchScope::ParentsOnly => TemporalSessionScopeFilterV1::ParentsOnly, - SessionSearchScope::SubagentsOnly => TemporalSessionScopeFilterV1::SubagentsOnly, - }, - message_type: match filters.message_type { - SessionMessageType::All => TemporalMessageTypeFilterV1::All, - SessionMessageType::DirectUser => TemporalMessageTypeFilterV1::DirectUser, - SessionMessageType::ToolResult => TemporalMessageTypeFilterV1::ToolResult, - }, + session_scope: temporal_session_scope(filters.scope), + message_type: temporal_message_type(filters.message_type), roles, start_time: filters.time_range.start_time, end_time: filters.time_range.end_time, diff --git a/crates/tracedecay-session-temporal-store/src/relations/projection_read.rs b/crates/tracedecay-session-temporal-store/src/relations/projection_read.rs index d9f435fa6a..b29a06855e 100644 --- a/crates/tracedecay-session-temporal-store/src/relations/projection_read.rs +++ b/crates/tracedecay-session-temporal-store/src/relations/projection_read.rs @@ -459,10 +459,7 @@ fn string_property<'a>( relation: &'a tracedecay_graph_db::GraphRelation, property: &GraphPropertyName, ) -> Result<&'a str, SessionRelationError> { - match relation.properties.get(property) { - Some(GraphProperty::String(value)) => Ok(value), - _ => Err(SessionRelationError::Corrupt), - } + super::read::string_property(relation, property).ok_or(SessionRelationError::Corrupt) } #[cfg(test)] diff --git a/crates/tracedecay-session-temporal-store/src/relations/read.rs b/crates/tracedecay-session-temporal-store/src/relations/read.rs index c5447f1be8..f40d3906ab 100644 --- a/crates/tracedecay-session-temporal-store/src/relations/read.rs +++ b/crates/tracedecay-session-temporal-store/src/relations/read.rs @@ -296,7 +296,7 @@ fn require_budget(max_relations: usize) -> Result<(), SessionRelationError> { } } -fn string_property<'a>( +pub(super) fn string_property<'a>( relation: &'a GraphRelation, property: &GraphPropertyName, ) -> Option<&'a str> { From 53fd79e28aa99ebcd04151c6bf268f87790bcb8e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:36:22 +0000 Subject: [PATCH 100/182] simplify(pass-3/5): collapse project config open duplicates Co-authored-by: Zack Jackson --- crates/tracedecay-project/src/config.rs | 85 ++++++++++--------- .../tracedecay-project/src/project/facts.rs | 15 ++-- 2 files changed, 51 insertions(+), 49 deletions(-) diff --git a/crates/tracedecay-project/src/config.rs b/crates/tracedecay-project/src/config.rs index d66157c108..365a929675 100644 --- a/crates/tracedecay-project/src/config.rs +++ b/crates/tracedecay-project/src/config.rs @@ -6,8 +6,8 @@ use tracedecay_contracts::clock::now_micros; use tracedecay_domain::ProjectId; use tracedecay_domain::configuration::{ CodeIndexWorkerSelectionV1, ConfigurationLayerIdV1, ConfigurationRevisionId, - ConfigurationSnapshotV1, ConfigurationValueV1, SOURCE_BINDINGS_SETTING_KEY, SettingKey, - UserProfileId, + ConfigurationSnapshotV1, ConfigurationValueV1, SOURCE_BINDINGS_SETTING_KEY, ScopeSourceBinding, + SettingKey, UserProfileId, }; use tracedecay_configuration::{ @@ -15,7 +15,9 @@ use tracedecay_configuration::{ load_config_from_path, }; use tracedecay_domain::errors::{Result, TraceDecayError}; -use tracedecay_global_db::configuration::contracts::ports::ConfigurationControlStore; +use tracedecay_global_db::configuration::contracts::ports::{ + ConfigurationControlStore, ConfigurationCurrentStateV1, +}; use tracedecay_global_db::configuration::contracts::types::ConfigurationError; use tracedecay_global_db::configuration::{ GlobalDbConfigurationControlStore, ProfileCodeIndexWorkerConfigurationStore, @@ -455,18 +457,8 @@ async fn initialize_canonical_project_configuration( .map_err(|error| { config_error(format!("invalid initial configuration revision: {error}")) })?; - let daemon_binding = - tracedecay_configuration::config::scope_control::daemon_owned_project_source_binding( - &target.project_id, - &target.project_root, - ) - .map_err(|error| { - config_error(format!( - "daemon project source binding could not be derived: {error}" - )) - })?; - let source_bindings_key = SettingKey::new(SOURCE_BINDINGS_SETTING_KEY) - .map_err(|error| config_error(format!("invalid source bindings setting key: {error}")))?; + let daemon_binding = daemon_project_source_binding(target)?; + let source_bindings_key = source_bindings_setting_key()?; let resolution = resolver::resolve_configuration( ®istry, &[resolver::ConfigurationLayerV1 { @@ -489,10 +481,6 @@ async fn initialize_canonical_project_configuration( .map_err(map_configuration_error) } -#[expect( - clippy::too_many_lines, - reason = "Open converges the durable current revision and verifies the daemon-owned source binding before any caller sees the pin." -)] async fn open_runtime_configuration_from_store( target: RuntimeConfigurationTarget, store: &GlobalDbConfigurationControlStore<'_>, @@ -507,29 +495,16 @@ async fn open_runtime_configuration_from_store( } initialize_canonical_project_configuration(store, &target).await?; } - let daemon_binding = - tracedecay_configuration::config::scope_control::daemon_owned_project_source_binding( - &target.project_id, - &target.project_root, - ) - .map_err(|error| { - config_error(format!( - "daemon project source binding could not be derived: {error}" - )) - })?; + let daemon_binding = daemon_project_source_binding(&target)?; let current = store.current().await.map_err(map_configuration_error)?; let mut current = match store .converge_registered_registry_shape(¤t.revision_id, now_micros()) .await { Ok(state) => state, - Err(ConfigurationError::RevisionConflict) => { - store.current().await.map_err(map_configuration_error)? - } - Err(error) => return Err(map_configuration_error(error)), + Err(error) => current_after_conflict(store, error).await?, }; - let source_bindings_key = SettingKey::new(SOURCE_BINDINGS_SETTING_KEY) - .map_err(|error| config_error(format!("invalid source bindings setting key: {error}")))?; + let source_bindings_key = source_bindings_setting_key()?; enum SourceBindingCheck { Verified, LocatorDigestDrift, @@ -576,6 +551,8 @@ async fn open_runtime_configuration_from_store( // instead of demanding a reset. SourceBindingCheck::LocatorDigestDrift if !rebind_attempted => { rebind_attempted = true; + // A concurrent open may have won the swap; adopt what it + // published and re-verify it exactly. current = match store .rebind_daemon_project_source_binding( ¤t.revision_id, @@ -585,12 +562,7 @@ async fn open_runtime_configuration_from_store( .await { Ok(state) => state, - // A concurrent open won the swap; adopt what it - // published and re-verify it exactly. - Err(ConfigurationError::RevisionConflict) => { - store.current().await.map_err(map_configuration_error)? - } - Err(error) => return Err(map_configuration_error(error)), + Err(error) => current_after_conflict(store, error).await?, }; } SourceBindingCheck::LocatorDigestDrift | SourceBindingCheck::Mismatch => { @@ -679,6 +651,37 @@ fn validate_registered_configuration_database( } } +fn daemon_project_source_binding( + target: &RuntimeConfigurationTarget, +) -> Result { + tracedecay_configuration::config::scope_control::daemon_owned_project_source_binding( + &target.project_id, + &target.project_root, + ) + .map_err(|error| { + config_error(format!( + "daemon project source binding could not be derived: {error}" + )) + }) +} + +fn source_bindings_setting_key() -> Result { + SettingKey::new(SOURCE_BINDINGS_SETTING_KEY) + .map_err(|error| config_error(format!("invalid source bindings setting key: {error}"))) +} + +async fn current_after_conflict( + store: &GlobalDbConfigurationControlStore<'_>, + error: ConfigurationError, +) -> Result { + match error { + ConfigurationError::RevisionConflict => { + store.current().await.map_err(map_configuration_error) + } + error => Err(map_configuration_error(error)), + } +} + fn map_configuration_error(error: ConfigurationError) -> TraceDecayError { match error { ConfigurationError::ResetRequired { reason } => { diff --git a/crates/tracedecay-project/src/project/facts.rs b/crates/tracedecay-project/src/project/facts.rs index eea371eabb..da5da6476e 100644 --- a/crates/tracedecay-project/src/project/facts.rs +++ b/crates/tracedecay-project/src/project/facts.rs @@ -23,15 +23,14 @@ impl TraceDecay { &self.db_path(), &self.store_layout.graph_db_path, ) { - Ok(ProjectMemoryDbHandle::Active(&self.db)) - } else { - let database = if self.read_only { - self.open_project_store_db_read_only()? - } else { - self.open_project_store_db()? - }; - Ok(ProjectMemoryDbHandle::Owned(Box::new(database))) + return Ok(ProjectMemoryDbHandle::Active(&self.db)); } + let database = if self.read_only { + self.open_project_store_db_read_only()? + } else { + self.open_project_store_db()? + }; + Ok(ProjectMemoryDbHandle::Owned(Box::new(database))) } /// Resolves the project-memory owner and database into one owner-bound From 863ba61a629f4a54ea4f0782418b2bb5e70df05d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:37:55 +0000 Subject: [PATCH 101/182] simplify(pass-4/5): share LSP admission refusal replies Co-authored-by: Zack Jackson --- crates/tracedecay-lsp/src/protocol.rs | 18 +- .../src/protocol/context_controller.rs | 206 ++++++------------ .../src/protocol/lifecycle_controller.rs | 8 +- .../src/protocol/semantic_controller.rs | 156 +++++-------- 4 files changed, 135 insertions(+), 253 deletions(-) diff --git a/crates/tracedecay-lsp/src/protocol.rs b/crates/tracedecay-lsp/src/protocol.rs index ee0d88379b..35142b599f 100644 --- a/crates/tracedecay-lsp/src/protocol.rs +++ b/crates/tracedecay-lsp/src/protocol.rs @@ -431,9 +431,7 @@ where .overlays .change(&uri, version, &changes) .map_err(|error| self.close_for_overlay_error(error))?; - self.diagnostics.workspace_snapshots.clear(); - self.diagnostics.workspace_failures.clear(); - self.discard_document_context(&uri); + self.invalidate_document(&uri); self.diagnostics.native_upstream.remove(&uri); self.lifecycle .control @@ -461,9 +459,7 @@ where .overlays .close(&uri) .map_err(overlay_failure)?; - self.diagnostics.workspace_snapshots.clear(); - self.diagnostics.workspace_failures.clear(); - self.discard_document_context(&uri); + self.invalidate_document(&uri); self.diagnostics.native_upstream.remove(&uri); self.lifecycle .control @@ -486,9 +482,7 @@ where self.require_ready()?; let uri = required_nonempty_string(text_document(params)?, "uri")?; self.require_document_root(&uri)?; - self.diagnostics.workspace_snapshots.clear(); - self.diagnostics.workspace_failures.clear(); - self.discard_document_context(&uri); + self.invalidate_document(&uri); if matches!( self.lifecycle.gateway.document_saved(uri.clone()), FeedbackCycleResponse::Accepted @@ -504,6 +498,12 @@ where } Ok(()) } + + fn invalidate_document(&mut self, uri: &str) { + self.diagnostics.workspace_snapshots.clear(); + self.diagnostics.workspace_failures.clear(); + self.discard_document_context(uri); + } } #[cfg(test)] diff --git a/crates/tracedecay-lsp/src/protocol/context_controller.rs b/crates/tracedecay-lsp/src/protocol/context_controller.rs index 14a2afa8ae..74fa10c4b3 100644 --- a/crates/tracedecay-lsp/src/protocol/context_controller.rs +++ b/crates/tracedecay-lsp/src/protocol/context_controller.rs @@ -1,10 +1,11 @@ use serde::Serialize; use super::diagnostics_controller::refresh_pending_failure; +use super::semantic_controller::admission_refusal; use super::{ - Arc, BTreeMap, BTreeSet, CodeGenerationId, CommitId, CompletionDisposition, ContentDigest, - ContextCoverage, ContextExpansionEnvelope, ContextExpansionOutcome, ContextExpansionRequest, - ContextFreshness, ContextProducerState, ContextProjectionChange, ContextProjectionEnvelope, + Arc, BTreeMap, BTreeSet, CodeGenerationId, CommitId, ContentDigest, ContextCoverage, + ContextExpansionEnvelope, ContextExpansionOutcome, ContextExpansionRequest, ContextFreshness, + ContextProducerState, ContextProjectionChange, ContextProjectionEnvelope, ContextProjectionIdentity, ContextProjectionKind, ContextProjectionOutcome, ContextProjectionPort, ContextProjectionRegistration, ContextProjectionRequest, ContextSubscribeRequest, DaemonLspProtocolSession, DiagnosticSnapshotPort, FeedbackCyclePort, @@ -14,7 +15,7 @@ use super::{ ProcessLocalRequestSequence, RpcFailure, SemanticProviderPort, TRACEDECAY_CONTEXT_CHANGED_METHOD, TRACEDECAY_CONTEXT_EXPAND_METHOD, TRACEDECAY_CONTEXT_METHOD, TRACEDECAY_SUBSCRIBE_METHOD, Value, error_response, is_supported_context_projection, json, - request_id, success_response, + request_id, }; struct CountingSink { @@ -216,67 +217,41 @@ where now_ms: u64, ) { let deadline = now_ms.saturating_add(self.lifecycle.request_deadline_ms); - match self.lifecycle.control.admit_request_with_deadline( + let admission = self.lifecycle.control.admit_request_with_deadline( request_id.clone(), document, Some(deadline), - ) { - crate::session::RequestAdmission::Accepted => { - let Ok(operation_id) = - NEXT_CONTEXT_OPERATION_ID.next_string("lsp-context-operation-") - else { - self.complete_context_request( - request_id, + ); + if let Some(failure) = admission_refusal(admission) { + let _ = self.enqueue_value(error_response(response_id, failure)); + return; + } + let Ok(operation_id) = NEXT_CONTEXT_OPERATION_ID.next_string("lsp-context-operation-") + else { + self.finish_admitted_request( + request_id, + response_id, + Err(RpcFailure::request_failure( + LspRequestFailure::ServerCancelled { + retrigger_request: true, + }, + )), + ); + return; + }; + let operation_id = LspRequestId::String(operation_id); + match self.context_snapshot_value(&operation_id, &request) { + Ok(None) => { + self.context.pending_requests.insert( + request_id, + PendingContextRequest { response_id, - Err(RpcFailure::request_failure( - LspRequestFailure::ServerCancelled { - retrigger_request: true, - }, - )), - ); - return; - }; - let operation_id = LspRequestId::String(operation_id); - match self.context_snapshot_value(&operation_id, &request) { - Ok(None) => { - self.context.pending_requests.insert( - request_id, - PendingContextRequest { - response_id, - operation_id, - request, - }, - ); - } - result => self.complete_context_request(request_id, response_id, result), - } - } - crate::session::RequestAdmission::DuplicateId => { - let _ = self.enqueue_value(error_response( - response_id, - RpcFailure { - code: -32600, - message: "Invalid Request", - data: json!({ "detail": "duplicate request id" }), + operation_id, + request, }, - )); - } - crate::session::RequestAdmission::SessionUnavailable => { - let _ = self.enqueue_value(error_response( - response_id, - RpcFailure::request_failure(LspRequestFailure::ServerCancelled { - retrigger_request: true, - }), - )); - } - crate::session::RequestAdmission::Saturated { retrigger_request } => { - let _ = self.enqueue_value(error_response( - response_id, - RpcFailure::request_failure(LspRequestFailure::ServerCancelled { - retrigger_request, - }), - )); + ); } + result => self.finish_admitted_request(request_id, response_id, result), } } @@ -288,91 +263,40 @@ where now_ms: u64, ) { let deadline = now_ms.saturating_add(self.lifecycle.request_deadline_ms); - match self.lifecycle.control.admit_request_with_deadline( + let admission = self.lifecycle.control.admit_request_with_deadline( request_id.clone(), None, Some(deadline), - ) { - crate::session::RequestAdmission::Accepted => { - let Ok(operation_id) = - NEXT_CONTEXT_OPERATION_ID.next_string("lsp-context-expansion-") - else { - self.complete_context_request( - request_id, - response_id, - Err(RpcFailure::request_failure( - LspRequestFailure::ServerCancelled { - retrigger_request: true, - }, - )), - ); - return; - }; - let operation_id = LspRequestId::String(operation_id); - match self.context_expansion_value(&operation_id, &request) { - Ok(None) => { - self.context.pending_expansions.insert( - request_id, - PendingContextExpansion { - response_id, - operation_id, - }, - ); - } - result => self.complete_context_request(request_id, response_id, result), - } - } - crate::session::RequestAdmission::DuplicateId => { - let _ = self.enqueue_value(error_response( - response_id, - RpcFailure { - code: -32600, - message: "Invalid Request", - data: json!({ "detail": "duplicate request id" }), - }, - )); - } - crate::session::RequestAdmission::SessionUnavailable => { - let _ = self.enqueue_value(error_response( - response_id, - RpcFailure::request_failure(LspRequestFailure::ServerCancelled { - retrigger_request: true, - }), - )); - } - crate::session::RequestAdmission::Saturated { retrigger_request } => { - let _ = self.enqueue_value(error_response( - response_id, - RpcFailure::request_failure(LspRequestFailure::ServerCancelled { - retrigger_request, - }), - )); - } + ); + if let Some(failure) = admission_refusal(admission) { + let _ = self.enqueue_value(error_response(response_id, failure)); + return; } - } - - pub(super) fn complete_context_request( - &mut self, - request_id: LspRequestId, - response_id: Value, - result: Result, RpcFailure>, - ) { - let completion = self.lifecycle.control.complete_request(&request_id); - if let Some(failure) = completion.failure() { - let _ = self.enqueue_value(error_response( + let Ok(operation_id) = NEXT_CONTEXT_OPERATION_ID.next_string("lsp-context-expansion-") + else { + self.finish_admitted_request( + request_id, response_id, - RpcFailure::request_failure(failure), - )); - } else if completion == CompletionDisposition::Publish { - match result { - Ok(Some(value)) => { - let _ = self.enqueue_value(success_response(response_id, value)); - } - Ok(None) => {} - Err(error) => { - let _ = self.enqueue_value(error_response(response_id, error)); - } + Err(RpcFailure::request_failure( + LspRequestFailure::ServerCancelled { + retrigger_request: true, + }, + )), + ); + return; + }; + let operation_id = LspRequestId::String(operation_id); + match self.context_expansion_value(&operation_id, &request) { + Ok(None) => { + self.context.pending_expansions.insert( + request_id, + PendingContextExpansion { + response_id, + operation_id, + }, + ); } + result => self.finish_admitted_request(request_id, response_id, result), } } @@ -520,7 +444,7 @@ where .get(&pending.request.kind) .copied() else { - self.complete_context_request( + self.finish_admitted_request( request_id, pending.response_id, Err(RpcFailure::unavailable( @@ -531,7 +455,7 @@ where continue; }; let result = self.context_projection_value(&pending.request, revision, outcome); - self.complete_context_request(request_id, pending.response_id, result); + self.finish_admitted_request(request_id, pending.response_id, result); } } @@ -619,7 +543,7 @@ where continue; }; let result = self.context_expansion_outcome_value(outcome); - self.complete_context_request(request_id, pending.response_id, result); + self.finish_admitted_request(request_id, pending.response_id, result); } } diff --git a/crates/tracedecay-lsp/src/protocol/lifecycle_controller.rs b/crates/tracedecay-lsp/src/protocol/lifecycle_controller.rs index a4b880e2a6..a108482e5d 100644 --- a/crates/tracedecay-lsp/src/protocol/lifecycle_controller.rs +++ b/crates/tracedecay-lsp/src/protocol/lifecycle_controller.rs @@ -393,7 +393,7 @@ where { let _ = cancellation.cancel_upstream(&root, &request_id); } - self.complete_context_request( + self.finish_admitted_request( request_id, pending.response_id, Err(RpcFailure::request_failure( @@ -413,7 +413,7 @@ where { let _ = context.cancel_request(&root, &pending.operation_id); } - self.complete_context_request( + self.finish_admitted_request( request_id, pending.response_id, Err(RpcFailure::request_failure( @@ -430,7 +430,7 @@ where { let _ = context.cancel_request(&root, &pending.operation_id); } - self.complete_context_request( + self.finish_admitted_request( request_id, pending.response_id, Err(RpcFailure::request_failure( @@ -756,7 +756,7 @@ where .or_else(|| context_pending.map(|pending| pending.response_id)) .or_else(|| expansion_pending.map(|pending| pending.response_id)); if let Some(response_id) = response_id { - self.complete_context_request( + self.finish_admitted_request( id.clone(), response_id, Err(RpcFailure::request_failure( diff --git a/crates/tracedecay-lsp/src/protocol/semantic_controller.rs b/crates/tracedecay-lsp/src/protocol/semantic_controller.rs index 053ea12d46..8aa2c0d8c2 100644 --- a/crates/tracedecay-lsp/src/protocol/semantic_controller.rs +++ b/crates/tracedecay-lsp/src/protocol/semantic_controller.rs @@ -1,3 +1,5 @@ +use crate::session::RequestAdmission; + use super::{ AnalyzerCancellationPort, Arc, BTreeMap, CompletionDisposition, DaemonLspProtocolSession, DiagnosticSnapshotPort, FeedbackCyclePort, GatewayResponse, LspRequestFailure, LspRequestId, @@ -46,55 +48,17 @@ where return; }; let deadline = now_ms.saturating_add(self.lifecycle.request_deadline_ms); - match self.lifecycle.control.admit_request_with_deadline( + let admission = self.lifecycle.control.admit_request_with_deadline( request_id.clone(), document, Some(deadline), - ) { - crate::session::RequestAdmission::Accepted => { - let result = route(self); - let completion = self.lifecycle.control.complete_request(&request_id); - if let Some(failure) = completion.failure() { - let _ = self - .enqueue_value(error_response(id, RpcFailure::request_failure(failure))); - } else if completion == CompletionDisposition::Publish { - match result { - Ok(value) => { - let _ = self.enqueue_value(success_response(id, value)); - } - Err(error) => { - let _ = self.enqueue_value(error_response(id, error)); - } - } - } - } - crate::session::RequestAdmission::DuplicateId => { - let _ = self.enqueue_value(error_response( - id, - RpcFailure { - code: -32600, - message: "Invalid Request", - data: json!({ "detail": "duplicate request id" }), - }, - )); - } - crate::session::RequestAdmission::SessionUnavailable => { - let _ = self.enqueue_value(error_response( - id, - RpcFailure::request_failure(LspRequestFailure::ServerCancelled { - retrigger_request: true, - }), - )); - } - crate::session::RequestAdmission::Saturated { retrigger_request } => { - let _ = self.enqueue_value(error_response( - id, - RpcFailure::request_failure(LspRequestFailure::ServerCancelled { - retrigger_request, - }), - )); - } + ); + if let Some(failure) = admission_refusal(admission) { + let _ = self.enqueue_value(error_response(id, failure)); + return; } + let result = route(self); + self.finish_admitted_request(request_id, id, result.map(Some)); } #[hotpath::measure( @@ -116,51 +80,26 @@ where return; }; let deadline = now_ms.saturating_add(self.lifecycle.request_deadline_ms); - match self.lifecycle.control.admit_request_with_deadline( + let admission = self.lifecycle.control.admit_request_with_deadline( request_id.clone(), document, Some(deadline), - ) { - crate::session::RequestAdmission::Accepted => { - match self.semantic_request_value(&request_id, &request) { - Ok(None) => { - self.semantic.pending.insert( - request_id, - PendingSemanticRequest { - response_id, - request, - }, - ); - } - result => self.complete_semantic_request(request_id, response_id, result), - } - } - crate::session::RequestAdmission::DuplicateId => { - let _ = self.enqueue_value(error_response( - response_id, - RpcFailure { - code: -32600, - message: "Invalid Request", - data: json!({ "detail": "duplicate request id" }), + ); + if let Some(failure) = admission_refusal(admission) { + let _ = self.enqueue_value(error_response(response_id, failure)); + return; + } + match self.semantic_request_value(&request_id, &request) { + Ok(None) => { + self.semantic.pending.insert( + request_id, + PendingSemanticRequest { + response_id, + request, }, - )); - } - crate::session::RequestAdmission::SessionUnavailable => { - let _ = self.enqueue_value(error_response( - response_id, - RpcFailure::request_failure(LspRequestFailure::ServerCancelled { - retrigger_request: true, - }), - )); - } - crate::session::RequestAdmission::Saturated { retrigger_request } => { - let _ = self.enqueue_value(error_response( - response_id, - RpcFailure::request_failure(LspRequestFailure::ServerCancelled { - retrigger_request, - }), - )); + ); } + result => self.finish_admitted_request(request_id, response_id, result), } } @@ -187,7 +126,22 @@ where } } - pub(super) fn complete_semantic_request( + pub(super) fn poll_semantic_requests(&mut self) { + let request_ids = self.semantic.pending.keys().cloned().collect::>(); + for request_id in request_ids { + let Some(pending) = self.semantic.pending.get(&request_id).cloned() else { + continue; + }; + let result = self.semantic_request_value(&request_id, &pending.request); + if matches!(result, Ok(None)) { + continue; + } + self.semantic.pending.remove(&request_id); + self.finish_admitted_request(request_id, pending.response_id, result); + } + } + + pub(super) fn finish_admitted_request( &mut self, request_id: LspRequestId, response_id: Value, @@ -211,19 +165,23 @@ where } } } +} - pub(super) fn poll_semantic_requests(&mut self) { - let request_ids = self.semantic.pending.keys().cloned().collect::>(); - for request_id in request_ids { - let Some(pending) = self.semantic.pending.get(&request_id).cloned() else { - continue; - }; - let result = self.semantic_request_value(&request_id, &pending.request); - if matches!(result, Ok(None)) { - continue; - } - self.semantic.pending.remove(&request_id); - self.complete_semantic_request(request_id, pending.response_id, result); - } +pub(super) fn admission_refusal(admission: RequestAdmission) -> Option { + match admission { + RequestAdmission::Accepted => None, + RequestAdmission::DuplicateId => Some(RpcFailure { + code: -32600, + message: "Invalid Request", + data: json!({ "detail": "duplicate request id" }), + }), + RequestAdmission::SessionUnavailable => Some(RpcFailure::request_failure( + LspRequestFailure::ServerCancelled { + retrigger_request: true, + }, + )), + RequestAdmission::Saturated { retrigger_request } => Some(RpcFailure::request_failure( + LspRequestFailure::ServerCancelled { retrigger_request }, + )), } } From 6e0505becfbbe0f8e20a877088b61012cb8efa2d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:39:36 +0000 Subject: [PATCH 102/182] simplify(pass-5/5): collapse source-edit refusal shapes Co-authored-by: Zack Jackson --- .../src/edits/ast_grep.rs | 104 +++++++++--------- .../src/edits/primitives.rs | 72 ++++++------ crates/tracedecay-source-edit/src/execute.rs | 65 ++++------- 3 files changed, 109 insertions(+), 132 deletions(-) diff --git a/crates/tracedecay-source-edit/src/edits/ast_grep.rs b/crates/tracedecay-source-edit/src/edits/ast_grep.rs index 13d933e447..b199b5cf21 100644 --- a/crates/tracedecay-source-edit/src/edits/ast_grep.rs +++ b/crates/tracedecay-source-edit/src/edits/ast_grep.rs @@ -24,11 +24,7 @@ pub(crate) async fn ast_grep_rewrite( rewrite: &str, dry_run: bool, ) -> Result { - let rel_path = super::primitives::resolve_path(project_root, path).ok_or_else(|| { - TraceDecayError::Config { - message: "path is not within the project".to_string(), - } - })?; + let rel_path = super::primitives::require_project_path(project_root, path)?; let file = SourceEditFileAuthority::open(project_root, Path::new(&rel_path))?; let (source, source_identity) = file.read_to_string(path)?; @@ -38,49 +34,45 @@ pub(crate) async fn ast_grep_rewrite( ); if check_output.is_err() { - if can_use_literal_rewrite_fallback(pattern) { - if !source.contains(pattern) { - return Ok(AstGrepResult { - success: false, - file_path: rel_path.clone(), - pattern: pattern.to_string(), - rewrite: rewrite.to_string(), - dry_run, - diff: None, - message: "pattern not found (built-in literal fallback)".to_string(), - }); - } - let modified = source.replace(pattern, rewrite); - let diff = super::primitives::commit_or_preview_edit( - &rel_path, - &file, - &source_identity, - &source, - &modified, + if !can_use_literal_rewrite_fallback(pattern) { + return Ok(refused_ast_grep( + rel_path, + pattern, + rewrite, dry_run, - ) - .await?; - return Ok(AstGrepResult { - success: true, - file_path: rel_path, - pattern: pattern.to_string(), - rewrite: rewrite.to_string(), + "ast-grep is not installed and this pattern needs SGPattern matching. Simple literal rewrites are handled by the built-in fallback.", + )); + } + if !source.contains(pattern) { + return Ok(refused_ast_grep( + rel_path, + pattern, + rewrite, dry_run, - diff, - message: edit_success_message( - dry_run, - "literal rewrite completed using built-in fallback", - ), - }); + "pattern not found (built-in literal fallback)", + )); } + let modified = source.replace(pattern, rewrite); + let diff = super::primitives::commit_or_preview_edit( + &rel_path, + &file, + &source_identity, + &source, + &modified, + dry_run, + ) + .await?; return Ok(AstGrepResult { - success: false, - file_path: rel_path.clone(), + success: true, + file_path: rel_path, pattern: pattern.to_string(), rewrite: rewrite.to_string(), dry_run, - diff: None, - message: "ast-grep is not installed and this pattern needs SGPattern matching. Simple literal rewrites are handled by the built-in fallback.".to_string(), + diff, + message: edit_success_message( + dry_run, + "literal rewrite completed using built-in fallback", + ), }); } @@ -136,15 +128,9 @@ pub(crate) async fn ast_grep_rewrite( File: {rel_path}, pattern: {pattern:?}" ) }; - return Ok(AstGrepResult { - success: false, - file_path: rel_path.clone(), - pattern: pattern.to_string(), - rewrite: rewrite.to_string(), - dry_run, - diff: None, - message, - }); + return Ok(refused_ast_grep( + rel_path, pattern, rewrite, dry_run, message, + )); } let modified = reconstruct_ast_grep_rewrite(&source, &output.stdout)?; @@ -238,6 +224,24 @@ fn can_use_literal_rewrite_fallback(pattern: &str) -> bool { && !pattern.contains('\r') } +fn refused_ast_grep( + file_path: String, + pattern: &str, + rewrite: &str, + dry_run: bool, + message: impl Into, +) -> AstGrepResult { + AstGrepResult { + success: false, + file_path, + pattern: pattern.to_string(), + rewrite: rewrite.to_string(), + dry_run, + diff: None, + message: message.into(), + } +} + #[cfg(test)] mod tests { use super::reconstruct_ast_grep_rewrite; diff --git a/crates/tracedecay-source-edit/src/edits/primitives.rs b/crates/tracedecay-source-edit/src/edits/primitives.rs index cddccf322f..ccd27c1444 100644 --- a/crates/tracedecay-source-edit/src/edits/primitives.rs +++ b/crates/tracedecay-source-edit/src/edits/primitives.rs @@ -44,19 +44,33 @@ pub(crate) fn splice_lines>( joined } +fn refused_multi_edit(file_path: String, dry_run: bool, message: String) -> MultiEditResult { + MultiEditResult { + success: false, + file_path, + applied_count: 0, + dry_run, + diff: None, + message, + } +} + /// Resolves a path to a relative path string. /// If the path is already relative, validates that it stays in the project. /// If absolute, strips the `project_root` prefix. -pub(super) fn resolve_path(project_root: &Path, path: &str) -> Option { +pub(super) fn require_project_path(project_root: &Path, path: &str) -> Result { let path = Path::new(path); let relative = if path.is_absolute() { - path.strip_prefix(project_root).ok()? + path.strip_prefix(project_root).ok() } else { - path + Some(path) }; - normalize_source_edit_relative_path(relative) - .ok() + relative + .and_then(|relative| normalize_source_edit_relative_path(relative).ok()) .map(|path| path.to_string_lossy().replace('\\', "/")) + .ok_or_else(|| TraceDecayError::Config { + message: "path is not within the project".to_string(), + }) } /// Write-or-preview gate shared by every edit primitive. On a real run this @@ -105,9 +119,7 @@ pub(crate) async fn str_replace( new_str: &str, dry_run: bool, ) -> Result { - let rel_path = resolve_path(project_root, path).ok_or_else(|| TraceDecayError::Config { - message: "path is not within the project".to_string(), - })?; + let rel_path = require_project_path(project_root, path)?; let file = SourceEditFileAuthority::open(project_root, Path::new(&rel_path))?; let (source, source_identity) = file.read_to_string(path)?; @@ -177,9 +189,7 @@ pub(crate) async fn multi_str_replace( replacements: &[(&str, &str)], dry_run: bool, ) -> Result { - let rel_path = resolve_path(project_root, path).ok_or_else(|| TraceDecayError::Config { - message: "path is not within the project".to_string(), - })?; + let rel_path = require_project_path(project_root, path)?; let file = SourceEditFileAuthority::open(project_root, Path::new(&rel_path))?; let (source, source_identity) = file.read_to_string(path)?; @@ -194,17 +204,14 @@ pub(crate) async fn multi_str_replace( for (old, new) in replacements { let mut hits = source.match_indices(old); let Some((start, matched)) = hits.next() else { - return Ok(MultiEditResult { - success: false, - file_path: rel_path.clone(), - applied_count: 0, + return Ok(refused_multi_edit( + rel_path, dry_run, - diff: None, - message: format!( + format!( "replacement '{}' matches 0 times, must match exactly once", tracedecay_runtime_core::text::utf8_prefix_at_or_before(old, 20) ), - }); + )); }; if hits.next().is_some() { // Two matches already consumed from `hits`; the remainder it @@ -212,18 +219,14 @@ pub(crate) async fn multi_str_replace( // that count plus the two already seen, no need for a // redundant full-string `source.matches(old).count()` pass. let count = 2 + hits.count(); - return Ok(MultiEditResult { - success: false, - file_path: rel_path.clone(), - applied_count: 0, + return Ok(refused_multi_edit( + rel_path, dry_run, - diff: None, - message: format!( - "replacement '{}' matches {} times, must match exactly once", + format!( + "replacement '{}' matches {count} times, must match exactly once", tracedecay_runtime_core::text::utf8_prefix_at_or_before(old, 20), - count ), - }); + )); } spans.push((start, start + matched.len(), old, new)); } @@ -236,18 +239,15 @@ pub(crate) async fn multi_str_replace( let (_, prev_end, prev_old, _) = window[0]; let (next_start, _, next_old, _) = window[1]; if next_start < prev_end { - return Ok(MultiEditResult { - success: false, - file_path: rel_path.clone(), - applied_count: 0, + return Ok(refused_multi_edit( + rel_path, dry_run, - diff: None, - message: format!( + format!( "replacements '{}' and '{}' target overlapping ranges; apply them separately", tracedecay_runtime_core::text::utf8_prefix_at_or_before(prev_old, 20), tracedecay_runtime_core::text::utf8_prefix_at_or_before(next_old, 20) ), - }); + )); } } @@ -294,9 +294,7 @@ pub(crate) async fn insert_at( before: bool, dry_run: bool, ) -> Result { - let rel_path = resolve_path(project_root, path).ok_or_else(|| TraceDecayError::Config { - message: "path is not within the project".to_string(), - })?; + let rel_path = require_project_path(project_root, path)?; let file = SourceEditFileAuthority::open(project_root, Path::new(&rel_path))?; let (source, source_identity) = file.read_to_string(path)?; diff --git a/crates/tracedecay-source-edit/src/execute.rs b/crates/tracedecay-source-edit/src/execute.rs index bdf7efe4f8..c6511f86f3 100644 --- a/crates/tracedecay-source-edit/src/execute.rs +++ b/crates/tracedecay-source-edit/src/execute.rs @@ -244,6 +244,17 @@ where let durability = SourceEditDurability::for_graph(graph); let _lock = durability.lock()?; let input_digest = request.input_digest().map_err(application_contract_error)?; + let refuse = |authority: &tracedecay_contracts::SourceEditAuthorizationAdmissionV1, + state: PreEffectState| { + fail_pre_effect( + &durability, + operation, + &request, + authority, + &input_digest, + state, + ) + }; let requested_authority = tracedecay_contracts::SourceEditAuthorizationAdmissionV1::new( request.authority.clone(), request.proof.clone(), @@ -276,23 +287,15 @@ where { Ok(admission) => admission, Err(_) => { - return fail_pre_effect( - &durability, - operation, - &request, + return refuse( &requested_authority, - &input_digest, PreEffectState::unpreviewed(request.expected_state.clone()), ); } }; if admission.receipt != request.authority || admission.proof != request.proof { - return fail_pre_effect( - &durability, - operation, - &request, + return refuse( &requested_authority, - &input_digest, PreEffectState::unpreviewed(request.expected_state.clone()), ); } @@ -302,12 +305,8 @@ where { Ok(authority) if authority_still_matches(&authority, &request) => authority, Ok(_) => { - return fail_pre_effect( - &durability, - operation, - &request, + return refuse( &requested_authority, - &input_digest, PreEffectState::unpreviewed(request.expected_state.clone()), ); } @@ -317,12 +316,8 @@ where { return Err(application_problem(error)); } - return fail_pre_effect( - &durability, - operation, - &request, + return refuse( &admission, - &input_digest, PreEffectState::unpreviewed(request.expected_state.clone()), ); } @@ -386,12 +381,8 @@ where .expected_state .ok_or_else(|| config_error("successful source edit preview omitted expected state"))?; if !request.edit.dry_run() && current_state != request.expected_state { - return fail_pre_effect( - &durability, - operation, - &request, + return refuse( ¤t_authority, - &input_digest, PreEffectState { expected: request.expected_state.clone(), predicted: Some(predicted_state), @@ -430,12 +421,8 @@ where { Ok(authority) if authority_still_matches(&authority, &request) => authority, _ => { - return fail_pre_effect( - &durability, - operation, - &request, + return refuse( ¤t_authority, - &input_digest, PreEffectState { expected: current_state, predicted: Some(predicted_state), @@ -469,12 +456,8 @@ where { Ok(authority) if authority_still_matches(&authority, &request) => authority, _ => { - return fail_pre_effect( - &durability, - operation, - &request, + return refuse( ¤t_authority, - &input_digest, PreEffectState { expected: request.expected_state.clone(), predicted: Some(predicted_state), @@ -486,12 +469,8 @@ where let recaptured_state = match source_edit_state_digest(graph.project_root(), &candidate_files) { Ok(state) => state, Err(_) => { - return fail_pre_effect( - &durability, - operation, - &request, + return refuse( ¤t_authority, - &input_digest, PreEffectState { expected: request.expected_state.clone(), predicted: Some(predicted_state), @@ -501,12 +480,8 @@ where } }; if recaptured_state != request.expected_state { - return fail_pre_effect( - &durability, - operation, - &request, + return refuse( ¤t_authority, - &input_digest, PreEffectState { expected: request.expected_state.clone(), predicted: Some(predicted_state), From 7d36ad3eaa10ef0ea1c73db91517e927e7b26cf0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 18:46:25 +0000 Subject: [PATCH 103/182] simplify(pass-5/5): reuse protocol and coverage helpers Client protocol errors and the unknown Observatory coverage stop repeating the same constructor. Co-authored-by: Zack Jackson --- .../read_model/rejected_arguments.rs | 14 +- crates/tracedecay-cli/src/workflow_cli.rs | 1 + crates/tracedecay-sdk/src/client.rs | 243 +++++++++--------- 3 files changed, 128 insertions(+), 130 deletions(-) diff --git a/crates/tracedecay-application/src/observability/read_model/rejected_arguments.rs b/crates/tracedecay-application/src/observability/read_model/rejected_arguments.rs index 1d248c17db..ec9ec1c513 100644 --- a/crates/tracedecay-application/src/observability/read_model/rejected_arguments.rs +++ b/crates/tracedecay-application/src/observability/read_model/rejected_arguments.rs @@ -29,7 +29,7 @@ pub(crate) fn unavailable_rejected_arguments( reason: &str, ) -> RejectedArgumentAnalyticsV1 { RejectedArgumentAnalyticsV1 { - coverage: unknown_coverage(), + coverage: super::unknown_coverage(), projector_revision: PROJECTOR_REVISION.to_owned(), watermark: watermark.to_owned(), eligible_attempts: None, @@ -227,15 +227,3 @@ fn finish( unavailable_reason, } } - -const fn unknown_coverage() -> MetricCoverageV1 { - MetricCoverageV1 { - eligible: None, - observed: 0, - completed: 0, - censored: 0, - unknown: 1, - excluded: 0, - state: CoverageStateV1::Unknown, - } -} diff --git a/crates/tracedecay-cli/src/workflow_cli.rs b/crates/tracedecay-cli/src/workflow_cli.rs index 2541f72cfc..897c203436 100644 --- a/crates/tracedecay-cli/src/workflow_cli.rs +++ b/crates/tracedecay-cli/src/workflow_cli.rs @@ -51,6 +51,7 @@ fn workflow_catalog(operation: WorkflowOperation) -> Result<(ResultContractRef, )) } +#[cfg(test)] fn workflow_cli_deadline(operation: WorkflowOperation, observed_at: UtcMicros) -> Result { let (_, maximum_millis) = workflow_catalog(operation)?; deadline_from_maximum_millis(maximum_millis, observed_at) diff --git a/crates/tracedecay-sdk/src/client.rs b/crates/tracedecay-sdk/src/client.rs index 4703be105d..314e77fe42 100644 --- a/crates/tracedecay-sdk/src/client.rs +++ b/crates/tracedecay-sdk/src/client.rs @@ -300,31 +300,31 @@ impl Client { || schema_id != Some(Operation::RESULT_SCHEMA_ID) || schema_revision != Some(u64::from(Operation::RESULT_SCHEMA_REVISION)) { - return Err(ClientError::Protocol { - status: Some(response.status()), - message: format!( + return Err(protocol( + response.status(), + format!( "daemon returned mismatched contracts for {}", Operation::OPERATION_ID ), - }); + )); } - let outcome = - response - .envelope() - .get("outcome") - .ok_or_else(|| ClientError::Protocol { - status: Some(response.status()), - message: format!("daemon omitted the {} outcome", Operation::OPERATION_ID), - })?; + let outcome = response.envelope().get("outcome").ok_or_else(|| { + protocol( + response.status(), + format!("daemon omitted the {} outcome", Operation::OPERATION_ID), + ) + })?; let outcome_kind = outcome .get("outcome") .and_then(Value::as_str) - .ok_or_else(|| ClientError::Protocol { - status: Some(response.status()), - message: format!( - "daemon omitted the {} outcome kind", - Operation::OPERATION_ID - ), + .ok_or_else(|| { + protocol( + response.status(), + format!( + "daemon omitted the {} outcome kind", + Operation::OPERATION_ID + ), + ) })?; let lifecycle_shape_is_legal = matches!( (Operation::RECEIPT, Operation::RECONCILIATION, outcome_kind), @@ -339,100 +339,107 @@ impl Client { ) ); if !lifecycle_shape_is_legal { - return Err(ClientError::Protocol { - status: Some(response.status()), - message: format!( + return Err(protocol( + response.status(), + format!( "daemon returned outcome {outcome_kind} outside the {} receipt contract", Operation::OPERATION_ID ), - }); + )); } let execution = outcome .get("value") .and_then(|value| value.get("execution")) - .ok_or_else(|| ClientError::Protocol { - status: Some(response.status()), - message: format!( - "daemon omitted the {} execution receipt", - Operation::OPERATION_ID - ), + .ok_or_else(|| { + protocol( + response.status(), + format!( + "daemon omitted the {} execution receipt", + Operation::OPERATION_ID + ), + ) })?; let termination = execution .get("termination") .and_then(Value::as_str) - .ok_or_else(|| ClientError::Protocol { - status: Some(response.status()), - message: format!( - "daemon omitted the {} terminal state", - Operation::OPERATION_ID - ), + .ok_or_else(|| { + protocol( + response.status(), + format!( + "daemon omitted the {} terminal state", + Operation::OPERATION_ID + ), + ) })?; if !Operation::TERMINAL_STATES .iter() .copied() .any(|state| terminal_state_name(state) == termination) { - return Err(ClientError::Protocol { - status: Some(response.status()), - message: format!( + return Err(protocol( + response.status(), + format!( "daemon returned terminal state {termination} outside the {} contract", Operation::OPERATION_ID ), - }); + )); } if let Some(cancellation) = execution.get("cancellation") && !cancellation.is_null() { if !Operation::CANCELLABLE { - return Err(ClientError::Protocol { - status: Some(response.status()), - message: format!( + return Err(protocol( + response.status(), + format!( "daemon returned cancellation evidence for non-cancellable {}", Operation::OPERATION_ID ), - }); + )); } let stage = cancellation .get("stage") .and_then(Value::as_str) - .ok_or_else(|| ClientError::Protocol { - status: Some(response.status()), - message: format!( - "daemon returned malformed cancellation evidence for {}", - Operation::OPERATION_ID - ), + .ok_or_else(|| { + protocol( + response.status(), + format!( + "daemon returned malformed cancellation evidence for {}", + Operation::OPERATION_ID + ), + ) })?; if !Operation::CANCELLATION_POINTS .iter() .copied() .any(|point| cancellation_point_name(point) == stage) { - return Err(ClientError::Protocol { - status: Some(response.status()), - message: format!( + return Err(protocol( + response.status(), + format!( "daemon returned cancellation stage {stage} outside the {} contract", Operation::OPERATION_ID ), - }); + )); } } - let payload = response - .payload() - .cloned() - .ok_or_else(|| ClientError::Protocol { - status: Some(response.status()), - message: format!( + let payload = response.payload().cloned().ok_or_else(|| { + protocol( + response.status(), + format!( "daemon omitted the {} result payload", Operation::OPERATION_ID ), - })?; + ) + })?; let request_id = response .envelope() .get("request_id") .and_then(Value::as_str) - .ok_or_else(|| ClientError::Protocol { - status: Some(response.status()), - message: "daemon omitted the application request ID".into(), + .ok_or_else(|| { + protocol( + response.status(), + "daemon omitted the application request ID", + ) })? .to_owned(); if !crate::semantic::response_matches( @@ -442,22 +449,23 @@ impl Client { &request, &payload, ) { - return Err(ClientError::Protocol { - status: Some(response.status()), - message: format!( + return Err(protocol( + response.status(), + format!( "daemon returned a semantically invalid {} result", Operation::OPERATION_ID ), - }); + )); } - let result = - serde_json::from_value(payload).map_err(|error| ClientError::Protocol { - status: Some(response.status()), - message: format!( + let result = serde_json::from_value(payload).map_err(|error| { + protocol( + response.status(), + format!( "daemon returned a malformed {} result: {error}", Operation::OPERATION_ID ), - })?; + ) + })?; Ok(TypedResponse { request_id, result, @@ -521,9 +529,11 @@ impl Client { return Err(ClientError::Authentication(status.as_u16())); } let body: Value = crate::observe::body_decode(|| { - response.json().map_err(|error| ClientError::Protocol { - status: Some(status.as_u16()), - message: format!("daemon returned malformed cancellation JSON: {error}"), + response.json().map_err(|error| { + protocol( + status.as_u16(), + format!("daemon returned malformed cancellation JSON: {error}"), + ) }) })?; if body.get("kind").and_then(Value::as_str) == Some("problem") { @@ -533,11 +543,12 @@ impl Client { "cancellation problem envelope has no value", )); } - let value: OperationCancellation = - serde_json::from_value(body).map_err(|error| ClientError::Protocol { - status: Some(status.as_u16()), - message: format!("daemon returned malformed cancellation JSON: {error}"), - })?; + let value: OperationCancellation = serde_json::from_value(body).map_err(|error| { + protocol( + status.as_u16(), + format!("daemon returned malformed cancellation JSON: {error}"), + ) + })?; let valid_status = matches!( (status, value.status), (StatusCode::ACCEPTED, CancellationStatus::Requested) @@ -545,10 +556,10 @@ impl Client { | (StatusCode::OK, CancellationStatus::AlreadyTerminal) ); if !valid_status { - return Err(ClientError::Protocol { - status: Some(status.as_u16()), - message: "daemon returned a non-canonical cancellation response".into(), - }); + return Err(protocol( + status.as_u16(), + "daemon returned a non-canonical cancellation response", + )); } Ok(value) })()) @@ -612,24 +623,23 @@ impl Client { return Err(ClientError::Authentication(status.as_u16())); } if media_type(response.headers()) != Some("application/json") { - return Err(ClientError::Protocol { - status: Some(status.as_u16()), - message: "daemon response is not application/json".into(), - }); + return Err(protocol( + status.as_u16(), + "daemon response is not application/json", + )); } - let body: Value = response.json().map_err(|error| ClientError::Protocol { - status: Some(status.as_u16()), - message: format!("daemon returned malformed application JSON: {error}"), + let body: Value = response.json().map_err(|error| { + protocol( + status.as_u16(), + format!("daemon returned malformed application JSON: {error}"), + ) })?; match body.get("kind").and_then(Value::as_str) { Some("success") if status.is_success() => { let value = body .get("value") .cloned() - .ok_or_else(|| ClientError::Protocol { - status: Some(status.as_u16()), - message: "success envelope has no value".into(), - })?; + .ok_or_else(|| protocol(status.as_u16(), "success envelope has no value"))?; ApplicationResponse::new(value, status.as_u16()) } Some("problem") if !status.is_success() => { @@ -640,10 +650,10 @@ impl Client { if expected_request_id .is_some_and(|expected| response_request_id != Some(expected.as_str())) { - return Err(ClientError::Protocol { - status: Some(status.as_u16()), - message: "daemon returned a different application request ID".into(), - }); + return Err(protocol( + status.as_u16(), + "daemon returned a different application request ID", + )); } Err(problem_error( &body, @@ -651,10 +661,10 @@ impl Client { "problem envelope has no value", )) } - _ => Err(ClientError::Protocol { - status: Some(status.as_u16()), - message: "daemon returned an inconsistent HTTP envelope".into(), - }), + _ => Err(protocol( + status.as_u16(), + "daemon returned an inconsistent HTTP envelope", + )), } } } @@ -784,10 +794,7 @@ fn media_type(headers: &HeaderMap) -> Option<&str> { /// carries no value, because only the caller knows which surface it read. fn problem_error(body: &Value, status: u16, missing_value: &'static str) -> ClientError { let Some(envelope) = body.get("value").cloned() else { - return ClientError::Protocol { - status: Some(status), - message: missing_value.into(), - }; + return protocol(status, missing_value); }; match ProblemError::new(status, envelope) { Ok(problem) => ClientError::Problem(Box::new(problem)), @@ -1084,9 +1091,11 @@ impl OperationStream { let media_type = media_type(response.headers()); if !status.is_success() && media_type == Some("application/json") { let body: Value = crate::observe::body_decode(|| { - response.json().map_err(|error| ClientError::Protocol { - status: Some(status.as_u16()), - message: format!("daemon returned malformed stream problem JSON: {error}"), + response.json().map_err(|error| { + protocol( + status.as_u16(), + format!("daemon returned malformed stream problem JSON: {error}"), + ) }) })?; if body.get("kind").and_then(Value::as_str) == Some("problem") { @@ -1096,16 +1105,16 @@ impl OperationStream { "stream problem envelope has no value", )); } - return Err(ClientError::Protocol { - status: Some(status.as_u16()), - message: "daemon returned an unknown stream problem envelope".into(), - }); + return Err(protocol( + status.as_u16(), + "daemon returned an unknown stream problem envelope", + )); } if !status.is_success() || media_type != Some("text/event-stream") { - return Err(ClientError::Protocol { - status: Some(status.as_u16()), - message: "daemon did not open a canonical event stream".into(), - }); + return Err(protocol( + status.as_u16(), + "daemon did not open a canonical event stream", + )); } self.reader = Some(BufReader::new(response)); Ok(()) From 4fee9463cea67fb80e02614f096ed7910e247213 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:09:22 +0000 Subject: [PATCH 104/182] simplify(pass-3/5): reuse shared extractor state Co-authored-by: Zack Jackson --- .../src/c_extractor.rs | 78 +++---------------- .../src/cobol_extractor.rs | 53 +------------ .../src/dart_extractor.rs | 10 +-- .../src/java_extractor.rs | 6 +- .../src/julia_extractor.rs | 49 +----------- .../src/kotlin_extractor.rs | 6 +- .../src/objc_extractor.rs | 8 +- .../src/quint_extractor.rs | 12 +-- 8 files changed, 24 insertions(+), 198 deletions(-) diff --git a/crates/tracedecay-code-extraction/src/c_extractor.rs b/crates/tracedecay-code-extraction/src/c_extractor.rs index 408b18b932..5d100cbb66 100644 --- a/crates/tracedecay-code-extraction/src/c_extractor.rs +++ b/crates/tracedecay-code-extraction/src/c_extractor.rs @@ -6,10 +6,10 @@ use std::time::Instant; use tree_sitter::{Node as TsNode, Tree}; -use crate::common::local_node_id; +use crate::common::{ExtractionState, local_node_id}; use crate::types::{ - ComplexityAnalysisV1, Edge, EdgeKind, ExtractionResult, Node, NodeKind, UnresolvedRef, - Visibility, generate_node_id, + ComplexityAnalysisV1, Edge, EdgeKind, ExtractionResult, Node, NodeKind, Visibility, + generate_node_id, }; use crate::{ common::{clean_c_comment, docstring_from_preceding_comments, extract_call_expression_sites}, @@ -20,63 +20,7 @@ use crate::{ /// Extracts code graph nodes and edges from C source files using tree-sitter. pub struct CExtractor; -/// Internal state used during AST traversal. -struct ExtractionState<'s> { - nodes: Vec, - edges: Vec, - unresolved_refs: Vec, - errors: Vec, - /// Stack of (name, `node_id`) for building qualified names and parent edges. - node_stack: Vec<(String, String)>, - file_path: String, - source: &'s [u8], - timestamp: u64, -} - impl<'s> ExtractionState<'s> { - fn new(file_path: &str, source: &'s str) -> Self { - let timestamp = crate::common::unix_timestamp_secs(); - Self { - nodes: Vec::new(), - edges: Vec::new(), - unresolved_refs: Vec::new(), - errors: Vec::new(), - node_stack: Vec::new(), - file_path: file_path.to_string(), - source: source.as_bytes(), - timestamp, - } - } - - /// Returns the current qualified name prefix from the node stack. - /// - /// The file root is pushed onto `node_stack` as the first frame when - /// extraction begins, so iterating the stack already yields the file - /// path as the leading segment. Prepending `self.file_path` here was - /// a leftover that duplicated the prefix (`::::Type::method`). - fn qualified_prefix(&self) -> String { - self.node_stack - .iter() - .map(|(name, _)| name.as_str()) - .collect::>() - .join("::") - } - - /// Returns the current parent node ID, or None if at file root level. - fn parent_node_id(&self) -> Option<&str> { - self.node_stack.last().map(|(_, id)| id.as_str()) - } - - /// Gets the text of a tree-sitter node from the source. - fn node_text(&self, node: TsNode<'_>) -> &'s str { - node.utf8_text(self.source).unwrap_or("") - } - - /// Borrowed text of a tree-sitter node, sliced straight from the source. - fn node_str(&self, node: TsNode<'_>) -> &'s str { - node.utf8_text(self.source).unwrap_or("") - } - /// Source slice from `node.start_byte()` up to `end_byte`. fn text_before(&self, node: TsNode<'_>, end_byte: usize) -> &str { let start = node.start_byte(); @@ -256,13 +200,13 @@ impl CExtractor { if let Some(declarator) = find_descendant_by_kind(node, "function_declarator") { // The function name is the identifier child of the function_declarator if let Some(ident) = find_direct_child_by_kind(declarator, "identifier") { - return Some(state.node_str(ident)); + return Some(state.node_text(ident)); } // Could also be inside a pointer_declarator -> function_declarator if let Some(ident) = find_direct_child_by_kind(declarator, "parenthesized_declarator") { // For function pointer patterns, try finding identifier deeper if let Some(inner_ident) = find_descendant_by_kind(ident, "identifier") { - return Some(state.node_str(inner_ident)); + return Some(state.node_text(inner_ident)); } } } @@ -311,7 +255,7 @@ impl CExtractor { let name = Self::extract_function_name(state, node).unwrap_or(""); let signature = Some( state - .node_str(node) + .node_text(node) .trim() .trim_end_matches(';') .trim() @@ -384,7 +328,7 @@ impl CExtractor { let signature = Some( state - .node_str(node) + .node_text(node) .trim() .trim_end_matches(';') .trim() @@ -448,24 +392,24 @@ impl CExtractor { if let Some(init_decl) = find_direct_child_by_kind(node, "init_declarator") { // The identifier is the first child of init_declarator if let Some(ident) = find_direct_child_by_kind(init_decl, "identifier") { - return Some(state.node_str(ident)); + return Some(state.node_text(ident)); } // Could be a pointer declarator: `int *x = NULL;` if let Some(ptr_decl) = find_direct_child_by_kind(init_decl, "pointer_declarator") && let Some(ident) = find_direct_child_by_kind(ptr_decl, "identifier") { - return Some(state.node_str(ident)); + return Some(state.node_text(ident)); } } // Direct identifier child (e.g., `int x;`) if let Some(ident) = find_direct_child_by_kind(node, "identifier") { - return Some(state.node_str(ident)); + return Some(state.node_text(ident)); } // Pointer declarator without init (e.g., `char *name;`) if let Some(ptr_decl) = find_direct_child_by_kind(node, "pointer_declarator") && let Some(ident) = find_direct_child_by_kind(ptr_decl, "identifier") { - return Some(state.node_str(ident)); + return Some(state.node_text(ident)); } None } diff --git a/crates/tracedecay-code-extraction/src/cobol_extractor.rs b/crates/tracedecay-code-extraction/src/cobol_extractor.rs index d820e943a6..3796fe3f82 100644 --- a/crates/tracedecay-code-extraction/src/cobol_extractor.rs +++ b/crates/tracedecay-code-extraction/src/cobol_extractor.rs @@ -8,7 +8,7 @@ use std::time::Instant; use tree_sitter::{Node as TsNode, Tree}; -use crate::common::local_node_id; +use crate::common::{ExtractionState, local_node_id}; use crate::traversal::find_direct_child_by_kind; use crate::types::{ ComplexityAnalysisV1, Edge, EdgeKind, ExtractionResult, Node, NodeKind, UnresolvedRef, @@ -18,58 +18,7 @@ use crate::types::{ /// Extracts code graph nodes and edges from COBOL source files using tree-sitter. pub struct CobolExtractor; -/// Internal state used during AST traversal. -struct ExtractionState<'s> { - nodes: Vec, - edges: Vec, - unresolved_refs: Vec, - errors: Vec, - /// Stack of (name, `node_id`) for building qualified names and parent edges. - node_stack: Vec<(String, String)>, - file_path: String, - source: &'s [u8], - timestamp: u64, -} - impl<'s> ExtractionState<'s> { - fn new(file_path: &str, source: &'s str) -> Self { - let timestamp = crate::common::unix_timestamp_secs(); - Self { - nodes: Vec::new(), - edges: Vec::new(), - unresolved_refs: Vec::new(), - errors: Vec::new(), - node_stack: Vec::new(), - file_path: file_path.to_string(), - source: source.as_bytes(), - timestamp, - } - } - - /// Returns the current qualified name prefix from the node stack. - /// - /// The file root is pushed onto `node_stack` as the first frame when - /// extraction begins, so iterating the stack already yields the file - /// path as the leading segment. Prepending `self.file_path` here was - /// a leftover that duplicated the prefix (`::::Type::method`). - fn qualified_prefix(&self) -> String { - self.node_stack - .iter() - .map(|(name, _)| name.as_str()) - .collect::>() - .join("::") - } - - /// Returns the current parent node ID, or None if at file root level. - fn parent_node_id(&self) -> Option<&str> { - self.node_stack.last().map(|(_, id)| id.as_str()) - } - - /// Gets the text of a tree-sitter node from the source. - fn node_text(&self, node: TsNode<'_>) -> &'s str { - node.utf8_text(self.source).unwrap_or("") - } - /// Extracts the full source line at a given byte offset. /// /// COBOL comment nodes in tree-sitter-cobol have zero-width byte ranges, diff --git a/crates/tracedecay-code-extraction/src/dart_extractor.rs b/crates/tracedecay-code-extraction/src/dart_extractor.rs index e4e32c8186..9828268cf3 100644 --- a/crates/tracedecay-code-extraction/src/dart_extractor.rs +++ b/crates/tracedecay-code-extraction/src/dart_extractor.rs @@ -71,10 +71,6 @@ impl<'s> ExtractionState<'s> { node.utf8_text(self.source).unwrap_or("") } - fn node_str(&self, node: TsNode<'_>) -> &'s str { - node.utf8_text(self.source).unwrap_or("") - } - fn text_before(&self, node: TsNode<'_>, end_byte: usize) -> &str { let start = node.start_byte(); let end = end_byte.min(self.source.len()).max(start); @@ -1273,7 +1269,7 @@ impl DartExtractor { let visibility = Self::dart_visibility(&name); let docstring = Self::extract_docstring(state, decl_node); - let text = state.node_str(decl_node); + let text = state.node_text(decl_node); let signature = if let Some(body) = decl_node.child_by_field_name("body") { Some( state @@ -1346,7 +1342,7 @@ impl DartExtractor { // ---------------------------------- fn visit_operator(state: &mut ExtractionState, decl_node: TsNode<'_>, _sig_node: TsNode<'_>) { - let text = state.node_str(decl_node); + let text = state.node_text(decl_node); let name = text.find("operator").map_or_else( || "operator".to_string(), |pos| { @@ -1701,7 +1697,7 @@ impl DartExtractor { .trim() .to_string(); } - let text = state.node_str(node); + let text = state.node_text(node); if let Some(brace_pos) = text.find('{') { text[..brace_pos].trim().to_string() } else { diff --git a/crates/tracedecay-code-extraction/src/java_extractor.rs b/crates/tracedecay-code-extraction/src/java_extractor.rs index 0e351986d9..c03766640a 100644 --- a/crates/tracedecay-code-extraction/src/java_extractor.rs +++ b/crates/tracedecay-code-extraction/src/java_extractor.rs @@ -77,10 +77,6 @@ impl<'s> ExtractionState<'s> { fn node_text(&self, node: TsNode<'_>) -> &'s str { node.utf8_text(self.source).unwrap_or("") } - - fn node_str(&self, node: TsNode<'_>) -> &'s str { - node.utf8_text(self.source).unwrap_or("") - } } impl<'s> AnnotationEmitterState for ExtractionState<'s> { @@ -101,7 +97,7 @@ impl<'s> AnnotationEmitterState for ExtractionState<'s> { } fn node_str(&self, node: TsNode<'_>) -> &str { - ExtractionState::node_str(self, node) + self.node_text(node) } fn timestamp(&self) -> u64 { diff --git a/crates/tracedecay-code-extraction/src/julia_extractor.rs b/crates/tracedecay-code-extraction/src/julia_extractor.rs index b37440c426..bcda9a1b64 100644 --- a/crates/tracedecay-code-extraction/src/julia_extractor.rs +++ b/crates/tracedecay-code-extraction/src/julia_extractor.rs @@ -2,7 +2,7 @@ use std::time::Instant; use tree_sitter::{Node as TsNode, Tree}; -use crate::common::local_node_id; +use crate::common::{ExtractionState, local_node_id}; use crate::complexity::{ComplexityMetrics, JULIA_COMPLEXITY, count_complexity}; use crate::types::{ ComplexityAnalysisV1, Edge, EdgeKind, ExtractionResult, Node, NodeKind, UnresolvedRef, @@ -25,54 +25,7 @@ impl NodeText { } } -struct ExtractionState<'s> { - nodes: Vec, - edges: Vec, - unresolved_refs: Vec, - errors: Vec, - node_stack: Vec<(String, String)>, - file_path: String, - source: &'s [u8], - timestamp: u64, -} - impl<'s> ExtractionState<'s> { - fn new(file_path: &str, source: &'s str) -> Self { - let timestamp = crate::common::unix_timestamp_secs(); - Self { - nodes: Vec::new(), - edges: Vec::new(), - unresolved_refs: Vec::new(), - errors: Vec::new(), - node_stack: Vec::new(), - file_path: file_path.to_string(), - source: source.as_bytes(), - timestamp, - } - } - - /// Returns the current qualified name prefix from the node stack. - /// - /// The file root is pushed onto `node_stack` as the first frame when - /// extraction begins, so iterating the stack already yields the file - /// path as the leading segment. Prepending `self.file_path` here was - /// a leftover that duplicated the prefix (`::::Type::method`). - fn qualified_prefix(&self) -> String { - self.node_stack - .iter() - .map(|(name, _)| name.as_str()) - .collect::>() - .join("::") - } - - fn parent_node_id(&self) -> Option<&str> { - self.node_stack.last().map(|(_, id)| id.as_str()) - } - - fn node_text(&self, node: TsNode<'_>) -> &'s str { - node.utf8_text(self.source).unwrap_or("") - } - fn push_node( &mut self, kind: NodeKind, diff --git a/crates/tracedecay-code-extraction/src/kotlin_extractor.rs b/crates/tracedecay-code-extraction/src/kotlin_extractor.rs index f2fb6a0cfa..6e9d5be644 100644 --- a/crates/tracedecay-code-extraction/src/kotlin_extractor.rs +++ b/crates/tracedecay-code-extraction/src/kotlin_extractor.rs @@ -80,10 +80,6 @@ impl<'s> ExtractionState<'s> { fn node_text(&self, node: TsNode<'_>) -> &'s str { node.utf8_text(self.source).unwrap_or("") } - - fn node_str(&self, node: TsNode<'_>) -> &'s str { - node.utf8_text(self.source).unwrap_or("") - } } impl<'s> AnnotationEmitterState for ExtractionState<'s> { @@ -104,7 +100,7 @@ impl<'s> AnnotationEmitterState for ExtractionState<'s> { } fn node_str(&self, node: TsNode<'_>) -> &str { - ExtractionState::node_str(self, node) + self.node_text(node) } fn timestamp(&self) -> u64 { diff --git a/crates/tracedecay-code-extraction/src/objc_extractor.rs b/crates/tracedecay-code-extraction/src/objc_extractor.rs index 99c0336a8a..2ad5f39139 100644 --- a/crates/tracedecay-code-extraction/src/objc_extractor.rs +++ b/crates/tracedecay-code-extraction/src/objc_extractor.rs @@ -72,10 +72,6 @@ impl<'s> ExtractionState<'s> { node.utf8_text(self.source).unwrap_or("") } - fn node_str(&self, node: TsNode<'_>) -> &'s str { - node.utf8_text(self.source).unwrap_or("") - } - fn text_before(&self, node: TsNode<'_>, end_byte: usize) -> &str { let start = node.start_byte(); let end = end_byte.min(self.source.len()).max(start); @@ -329,7 +325,7 @@ impl ObjcExtractor { .to_string() }) .or_else(|| { - let text = state.node_str(node); + let text = state.node_text(node); text.find('{').map(|pos| text[..pos].trim().to_string()) }); let qualified_name = format!("{}::{}", state.qualified_prefix(), enum_name); @@ -1340,7 +1336,7 @@ impl ObjcExtractor { .trim() .to_string(); } - let text = state.node_str(node); + let text = state.node_text(node); if let Some(brace_pos) = text.find('{') { text[..brace_pos].trim().to_string() } else { diff --git a/crates/tracedecay-code-extraction/src/quint_extractor.rs b/crates/tracedecay-code-extraction/src/quint_extractor.rs index 7bdd49cacf..0220dc9bb9 100644 --- a/crates/tracedecay-code-extraction/src/quint_extractor.rs +++ b/crates/tracedecay-code-extraction/src/quint_extractor.rs @@ -62,12 +62,8 @@ impl<'s> ExtractionState<'s> { } } - fn node_str(&self, node: TsNode<'_>) -> &'s str { - node.utf8_text(self.source).unwrap_or("") - } - fn node_text(&self, node: TsNode<'_>) -> &'s str { - self.node_str(node) + node.utf8_text(self.source).unwrap_or("") } } @@ -96,7 +92,7 @@ impl TokenWalker { // The `.` operator extends an import path; everything else // is a terminator handled below. let extends_import = - matches!(kind, "identifier") || (kind == "operator" && state.node_str(child) == "."); + matches!(kind, "identifier") || (kind == "operator" && state.node_text(child) == "."); if !extends_import && let Some((parts, line)) = self.import_collect.take() { QuintExtractor::commit_import(state, &parts, line); } @@ -125,7 +121,7 @@ impl TokenWalker { } } "keyword" => { - let text = state.node_str(child); + let text = state.node_text(child); if text == "module" { self.pending = Some(PendingKind::Module); } else if text == "import" { @@ -136,7 +132,7 @@ impl TokenWalker { "storage_modifier" => { // `pure` prefixes `def`/`val`; we just keep updating `pending` // until the meaningful storage modifier arrives. - if let Some(kind) = quint_storage_kind(state.node_str(child)) { + if let Some(kind) = quint_storage_kind(state.node_text(child)) { self.pending = Some(kind); } } From 65f1b2053bc2df342cd3099d669f9345445c3b59 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:09:22 +0000 Subject: [PATCH 105/182] simplify(pass-4/5): drop retention journal pass-throughs Co-authored-by: Zack Jackson --- .../src/code_index_generations.rs | 54 ++++--- .../generation_transactions.rs | 63 ++------ .../src/code_index_generations/scope_roots.rs | 151 +++++++----------- .../src/code_index_generations/tests.rs | 71 ++++++-- .../tests/graph_replay_release_tests.rs | 17 +- .../code_index_generations/text_artifacts.rs | 78 +++------ 6 files changed, 196 insertions(+), 238 deletions(-) diff --git a/crates/tracedecay-code-index-retention/src/code_index_generations.rs b/crates/tracedecay-code-index-retention/src/code_index_generations.rs index e6009275fb..4f14d84089 100644 --- a/crates/tracedecay-code-index-retention/src/code_index_generations.rs +++ b/crates/tracedecay-code-index-retention/src/code_index_generations.rs @@ -67,12 +67,12 @@ pub use text_artifacts::{ }; use generation_transactions::{ - GENERATION_RECEIPT_STORE, acquire_graph_replay_pool_lock_checked, - cleanup_committed_transaction, cleanup_committed_transaction_under_graph_replay_pool_lock, - clear_transaction, expose_staged_generations_under_graph_replay_pool_lock, load_transaction, - open_file_sha256_hex_cancellable, path_still_names_open_file, persist_transaction, - receipt_is_durable, regular_file_exists, remove_empty_stage_root, rollback_staged_transaction, - stage_collectable_generations, transaction_path, write_receipt, + GENERATION_RECEIPT_STORE, GENERATION_TRANSACTION_JOURNAL, + acquire_graph_replay_pool_lock_checked, cleanup_committed_transaction, + cleanup_committed_transaction_under_graph_replay_pool_lock, + expose_staged_generations_under_graph_replay_pool_lock, open_file_sha256_hex_cancellable, + path_still_names_open_file, regular_file_exists, remove_empty_stage_root, + rollback_staged_transaction, stage_collectable_generations, transaction_path, }; #[cfg(test)] use generation_transactions::{ @@ -84,14 +84,14 @@ use receipt_store::receipt_digest_file_component; use scope_roots::is_code_index_scope_hash; #[cfg(test)] use scope_roots::{ - ScopeRootRetentionTransactionV1, build_scope_receipt, persist_scope_transaction, - scope_receipt_digest, scope_receipt_path, scope_stage_root, scope_transaction_path, - validate_scope_transaction, write_scope_receipt, + SCOPE_RECEIPT_STORE, SCOPE_TRANSACTION_JOURNAL, ScopeRootRetentionTransactionV1, + build_scope_receipt, scope_receipt_digest, scope_receipt_path, scope_stage_root, + scope_transaction_path, validate_scope_transaction, }; #[cfg(test)] use text_artifacts::{ - build_text_artifact_receipt, persist_text_artifact_transaction, - stage_collectable_text_artifacts, total_text_artifact_bytes, write_text_artifact_receipt, + TEXT_ARTIFACT_RECEIPT_STORE, TEXT_ARTIFACT_TRANSACTION_JOURNAL, build_text_artifact_receipt, + stage_collectable_text_artifacts, total_text_artifact_bytes, }; use text_artifacts::{ execute_text_artifact_retention_under_store_lock, plan_collectable_text_artifacts_cancellable, @@ -1553,7 +1553,7 @@ pub fn execute_code_generation_retention_cancellable( )?), None => None, }; - persist_transaction(store_root, &transaction)?; + journal::persist_journal(store_root, &GENERATION_TRANSACTION_JOURNAL, &transaction)?; let result = (|| { // Durable before the first unlink: the pointer must never name a @@ -1579,7 +1579,12 @@ pub fn execute_code_generation_retention_cancellable( // the replay reconciler never observes a receipt whose pool // survival events are missing. graph_replay_release::write_events(store_root, &receipt)?; - write_receipt(store_root, &receipt)?; + receipt_store::write_receipt( + store_root, + &GENERATION_RECEIPT_STORE, + &receipt.receipt_digest, + &receipt, + )?; cleanup_committed_transaction_under_graph_replay_pool_lock( store_root, &transaction, @@ -1592,13 +1597,18 @@ pub fn execute_code_generation_retention_cancellable( graph_replay_pool_root, is_cancelled, )?; - clear_transaction(store_root) + journal::clear_journal(store_root, &GENERATION_TRANSACTION_JOURNAL) })(); if let Err(error) = result { drop(graph_replay_pool_lock); - if !receipt_is_durable(store_root, &receipt)? { + if !receipt_store::receipt_is_durable( + store_root, + &GENERATION_RECEIPT_STORE, + &receipt.receipt_digest, + &receipt, + )? { rollback_staged_transaction(store_root, &transaction, graph_replay_pool_root)?; - clear_transaction(store_root)?; + journal::clear_journal(store_root, &GENERATION_TRANSACTION_JOURNAL)?; } return Err(error); } @@ -1769,11 +1779,17 @@ fn recover_pending_transaction_unlocked( graph_replay_pool_root: Option<&Path>, is_cancelled: &dyn Fn() -> bool, ) -> Result<(), CodeGenerationRetentionErrorV1> { - let Some(transaction) = load_transaction(store_root)? else { + let Some(transaction) = journal::load_journal(store_root, &GENERATION_TRANSACTION_JOURNAL)? + else { return Ok(()); }; - if receipt_is_durable(store_root, &transaction.receipt)? { + if receipt_store::receipt_is_durable( + store_root, + &GENERATION_RECEIPT_STORE, + &transaction.receipt.receipt_digest, + &transaction.receipt, + )? { cleanup_committed_transaction( store_root, &transaction, @@ -1784,7 +1800,7 @@ fn recover_pending_transaction_unlocked( } else { rollback_staged_transaction(store_root, &transaction, graph_replay_pool_root)?; } - clear_transaction(store_root) + journal::clear_journal(store_root, &GENERATION_TRANSACTION_JOURNAL) } fn read_active_pointer( diff --git a/crates/tracedecay-code-index-retention/src/code_index_generations/generation_transactions.rs b/crates/tracedecay-code-index-retention/src/code_index_generations/generation_transactions.rs index a87577aeee..20b52ab396 100644 --- a/crates/tracedecay-code-index-retention/src/code_index_generations/generation_transactions.rs +++ b/crates/tracedecay-code-index-retention/src/code_index_generations/generation_transactions.rs @@ -17,11 +17,8 @@ use tracedecay_domain::CodeGenerationId; use tracedecay_domain::canonical_text::{encode_lowercase_hex, is_lowercase_hex}; use super::graph_replay_release; -use super::journal::{ - BoundedJournalSpec, clear_journal, journal_path, load_journal, persist_journal, -}; +use super::journal::{BoundedJournalSpec, journal_path}; use super::locking::{CodeGenerationStoreLockV1, try_acquire_code_generation_store_lock}; -use super::receipt_store; use super::receipt_store::ReceiptStoreSpec; use super::{ CodeGenerationRetentionErrorV1, CodeGenerationRetentionGenerationV1, @@ -32,14 +29,15 @@ use super::{ sync_directory, total_bytes, validate_generation_file, write_active_pointer, }; -const GENERATION_TRANSACTION_JOURNAL: BoundedJournalSpec = - BoundedJournalSpec { - file_name: TRANSACTION_FILE, - max_bytes: MAX_TRANSACTION_BYTES, - label: "retention transaction", - write_context: "code-generation-retention-transaction", - validate: validate_transaction, - }; +pub(super) const GENERATION_TRANSACTION_JOURNAL: BoundedJournalSpec< + CodeGenerationRetentionTransactionV1, +> = BoundedJournalSpec { + file_name: TRANSACTION_FILE, + max_bytes: MAX_TRANSACTION_BYTES, + label: "retention transaction", + write_context: "code-generation-retention-transaction", + validate: validate_transaction, +}; pub(super) const GENERATION_RECEIPT_STORE: ReceiptStoreSpec = ReceiptStoreSpec { directory: RECEIPTS_DIRECTORY, @@ -58,19 +56,6 @@ pub(super) fn transaction_stage_root( .join(&receipt.receipt_digest) } -pub(super) fn persist_transaction( - store_root: &Path, - transaction: &CodeGenerationRetentionTransactionV1, -) -> Result<(), CodeGenerationRetentionErrorV1> { - persist_journal(store_root, &GENERATION_TRANSACTION_JOURNAL, transaction) -} - -pub(super) fn load_transaction( - store_root: &Path, -) -> Result, CodeGenerationRetentionErrorV1> { - load_journal(store_root, &GENERATION_TRANSACTION_JOURNAL) -} - pub(super) fn validate_transaction( transaction: &CodeGenerationRetentionTransactionV1, ) -> Result<(), CodeGenerationRetentionErrorV1> { @@ -140,30 +125,6 @@ pub(super) fn validate_transaction( Ok(()) } -pub(super) fn receipt_is_durable( - store_root: &Path, - receipt: &CodeGenerationRetentionReceiptV1, -) -> Result { - receipt_store::receipt_is_durable( - store_root, - &GENERATION_RECEIPT_STORE, - &receipt.receipt_digest, - receipt, - ) -} - -pub(super) fn write_receipt( - store_root: &Path, - receipt: &CodeGenerationRetentionReceiptV1, -) -> Result<(), CodeGenerationRetentionErrorV1> { - receipt_store::write_receipt( - store_root, - &GENERATION_RECEIPT_STORE, - &receipt.receipt_digest, - receipt, - ) -} - #[hotpath::measure(label = "usecases.retention.stage")] pub(super) fn stage_collectable_generations( store_root: &Path, @@ -955,10 +916,6 @@ pub(super) fn ensure_transaction_liveness( Ok(()) } -pub(super) fn clear_transaction(store_root: &Path) -> Result<(), CodeGenerationRetentionErrorV1> { - clear_journal(store_root, &GENERATION_TRANSACTION_JOURNAL) -} - pub(super) fn remove_empty_stage_root( stage_root: &Path, ) -> Result<(), CodeGenerationRetentionErrorV1> { diff --git a/crates/tracedecay-code-index-retention/src/code_index_generations/scope_roots.rs b/crates/tracedecay-code-index-retention/src/code_index_generations/scope_roots.rs index ec3d0b611b..34dc440402 100644 --- a/crates/tracedecay-code-index-retention/src/code_index_generations/scope_roots.rs +++ b/crates/tracedecay-code-index-retention/src/code_index_generations/scope_roots.rs @@ -39,7 +39,7 @@ use super::{ storage, }; -const SCOPE_TRANSACTION_JOURNAL: BoundedJournalSpec = +pub(super) const SCOPE_TRANSACTION_JOURNAL: BoundedJournalSpec = BoundedJournalSpec { file_name: SCOPE_RETENTION_TRANSACTION_FILE, max_bytes: MAX_SCOPE_TRANSACTION_BYTES, @@ -48,16 +48,17 @@ const SCOPE_TRANSACTION_JOURNAL: BoundedJournalSpec = - BoundedJournalSpec { - file_name: SCOPE_BINDING_CLEANUP_INTENT_FILE, - max_bytes: MAX_SCOPE_BINDING_CLEANUP_INTENT_BYTES, - label: "scope binding cleanup intent", - write_context: "code-index-scope-binding-cleanup-intent", - validate: validate_scope_binding_cleanup_intent, - }; +pub(super) const SCOPE_BINDING_CLEANUP_INTENT_JOURNAL: BoundedJournalSpec< + ScopeRootBindingCleanupIntentV1, +> = BoundedJournalSpec { + file_name: SCOPE_BINDING_CLEANUP_INTENT_FILE, + max_bytes: MAX_SCOPE_BINDING_CLEANUP_INTENT_BYTES, + label: "scope binding cleanup intent", + write_context: "code-index-scope-binding-cleanup-intent", + validate: validate_scope_binding_cleanup_intent, +}; -const SCOPE_RECEIPT_STORE: ReceiptStoreSpec = ReceiptStoreSpec { +pub(super) const SCOPE_RECEIPT_STORE: ReceiptStoreSpec = ReceiptStoreSpec { directory: SCOPE_RETENTION_RECEIPTS_DIRECTORY, label: "scope reconciliation receipt", }; @@ -644,7 +645,7 @@ pub fn execute_scope_root_retention( "scope liveness authority changed at the quarantine boundary".to_owned(), )); } - match load_scope_binding_cleanup_intent(store_root)? { + match load_journal(store_root, &SCOPE_BINDING_CLEANUP_INTENT_JOURNAL)? { Some(intent) if intent == expected_binding_cleanup_intent => {} Some(_) => { return Err(CodeGenerationRetentionErrorV1::UnsafeState( @@ -716,18 +717,28 @@ pub fn execute_scope_root_retention( receipt: receipt.clone(), scope_identities: quarantine.scope_identities().clone(), }; - persist_scope_transaction(store_root, &transaction)?; + persist_journal(store_root, &SCOPE_TRANSACTION_JOURNAL, &transaction)?; let result = (|| { quarantine.stage(&transaction.receipt.collected_scopes)?; - write_scope_receipt(store_root, &receipt)?; + receipt_store::write_receipt( + store_root, + &SCOPE_RECEIPT_STORE, + &receipt.receipt_digest, + &receipt, + )?; quarantine.cleanup_committed(&transaction.receipt.collected_scopes)?; - clear_scope_transaction(store_root) + clear_journal(store_root, &SCOPE_TRANSACTION_JOURNAL) })(); if let Err(error) = result { - if !scope_receipt_is_durable(store_root, &receipt)? { + if !receipt_store::receipt_is_durable( + store_root, + &SCOPE_RECEIPT_STORE, + &receipt.receipt_digest, + &receipt, + )? { quarantine.rollback(&transaction.receipt.collected_scopes)?; - clear_scope_transaction(store_root)?; + clear_journal(store_root, &SCOPE_TRANSACTION_JOURNAL)?; } return Err(error); } @@ -792,8 +803,8 @@ pub fn prepare_scope_root_binding_cleanup( "scope binding cleanup cannot begin while filesystem recovery is pending".to_owned(), )); } - match load_scope_binding_cleanup_intent(store_root)? { - None => persist_scope_binding_cleanup_intent(store_root, &intent), + match load_journal(store_root, &SCOPE_BINDING_CLEANUP_INTENT_JOURNAL)? { + None => persist_journal(store_root, &SCOPE_BINDING_CLEANUP_INTENT_JOURNAL, &intent), Some(existing) if existing == intent => Ok(()), Some(_) => Err(CodeGenerationRetentionErrorV1::UnsafeState( "a different scope binding cleanup intent is already pending".to_owned(), @@ -816,11 +827,16 @@ pub fn recover_scope_root_binding_cleanup( "scope binding cleanup requires filesystem transaction recovery first".to_owned(), )); } - let Some(intent) = load_scope_binding_cleanup_intent(store_root)? else { + let Some(intent) = load_journal(store_root, &SCOPE_BINDING_CLEANUP_INTENT_JOURNAL)? else { return Ok(None); }; let source_exists = scope_directory_exists(&scope_root_path(store_root, &intent.scope_hash)?)?; - if scope_receipt_is_durable(store_root, &intent.receipt)? { + if receipt_store::receipt_is_durable( + store_root, + &SCOPE_RECEIPT_STORE, + &intent.receipt.receipt_digest, + &intent.receipt, + )? { if source_exists { return Err(CodeGenerationRetentionErrorV1::UnsafeState( "scope binding cleanup receipt is durable but its source scope remains".to_owned(), @@ -833,7 +849,7 @@ pub fn recover_scope_root_binding_cleanup( })); } if source_exists { - clear_scope_binding_cleanup_intent(store_root)?; + clear_journal(store_root, &SCOPE_BINDING_CLEANUP_INTENT_JOURNAL)?; return Ok(None); } Err(CodeGenerationRetentionErrorV1::UnsafeState( @@ -853,11 +869,12 @@ pub fn complete_scope_root_binding_cleanup( "scope binding cleanup cannot complete while filesystem recovery is pending".to_owned(), )); } - let intent = load_scope_binding_cleanup_intent(store_root)?.ok_or_else(|| { - CodeGenerationRetentionErrorV1::UnsafeState( - "scope binding cleanup completion has no pending intent".to_owned(), - ) - })?; + let intent = + load_journal(store_root, &SCOPE_BINDING_CLEANUP_INTENT_JOURNAL)?.ok_or_else(|| { + CodeGenerationRetentionErrorV1::UnsafeState( + "scope binding cleanup completion has no pending intent".to_owned(), + ) + })?; if intent.scope_hash != replay.scope_hash || intent.source_scope != replay.source_scope || intent.liveness_proof != replay.liveness_proof @@ -866,7 +883,12 @@ pub fn complete_scope_root_binding_cleanup( "scope binding cleanup completion does not match its pending intent".to_owned(), )); } - if !scope_receipt_is_durable(store_root, &intent.receipt)? { + if !receipt_store::receipt_is_durable( + store_root, + &SCOPE_RECEIPT_STORE, + &intent.receipt.receipt_digest, + &intent.receipt, + )? { return Err(CodeGenerationRetentionErrorV1::UnsafeState( "scope binding cleanup completion has no durable filesystem receipt".to_owned(), )); @@ -876,13 +898,13 @@ pub fn complete_scope_root_binding_cleanup( "scope binding cleanup completion found its source scope present".to_owned(), )); } - clear_scope_binding_cleanup_intent(store_root) + clear_journal(store_root, &SCOPE_BINDING_CLEANUP_INTENT_JOURNAL) } pub(super) fn recover_pending_scope_transaction_unlocked( store_root: &Path, ) -> Result<(), CodeGenerationRetentionErrorV1> { - let Some(transaction) = load_scope_transaction(store_root)? else { + let Some(transaction) = load_journal(store_root, &SCOPE_TRANSACTION_JOURNAL)? else { return Ok(()); }; let mut quarantine = ScopeQuarantineAuthority::recover( @@ -890,12 +912,17 @@ pub(super) fn recover_pending_scope_transaction_unlocked( &transaction.receipt.receipt_digest, transaction.scope_identities.clone(), )?; - if scope_receipt_is_durable(store_root, &transaction.receipt)? { + if receipt_store::receipt_is_durable( + store_root, + &SCOPE_RECEIPT_STORE, + &transaction.receipt.receipt_digest, + &transaction.receipt, + )? { quarantine.cleanup_committed(&transaction.receipt.collected_scopes)?; } else { quarantine.rollback(&transaction.receipt.collected_scopes)?; } - clear_scope_transaction(store_root) + clear_journal(store_root, &SCOPE_TRANSACTION_JOURNAL) } pub(super) fn scope_transaction_path(store_root: &Path) -> PathBuf { @@ -1279,68 +1306,6 @@ pub(super) fn validate_scope_binding_cleanup_intent( Ok(()) } -pub(super) fn persist_scope_transaction( - store_root: &Path, - transaction: &ScopeRootRetentionTransactionV1, -) -> Result<(), CodeGenerationRetentionErrorV1> { - persist_journal(store_root, &SCOPE_TRANSACTION_JOURNAL, transaction) -} - -pub(super) fn load_scope_transaction( - store_root: &Path, -) -> Result, CodeGenerationRetentionErrorV1> { - load_journal(store_root, &SCOPE_TRANSACTION_JOURNAL) -} - -pub(super) fn clear_scope_transaction( - store_root: &Path, -) -> Result<(), CodeGenerationRetentionErrorV1> { - clear_journal(store_root, &SCOPE_TRANSACTION_JOURNAL) -} - -pub(super) fn persist_scope_binding_cleanup_intent( - store_root: &Path, - intent: &ScopeRootBindingCleanupIntentV1, -) -> Result<(), CodeGenerationRetentionErrorV1> { - persist_journal(store_root, &SCOPE_BINDING_CLEANUP_INTENT_JOURNAL, intent) -} - -pub(super) fn load_scope_binding_cleanup_intent( - store_root: &Path, -) -> Result, CodeGenerationRetentionErrorV1> { - load_journal(store_root, &SCOPE_BINDING_CLEANUP_INTENT_JOURNAL) -} - -pub(super) fn clear_scope_binding_cleanup_intent( - store_root: &Path, -) -> Result<(), CodeGenerationRetentionErrorV1> { - clear_journal(store_root, &SCOPE_BINDING_CLEANUP_INTENT_JOURNAL) -} - -pub(super) fn scope_receipt_is_durable( - store_root: &Path, - receipt: &ScopeRootRetentionReceiptV1, -) -> Result { - receipt_store::receipt_is_durable( - store_root, - &SCOPE_RECEIPT_STORE, - &receipt.receipt_digest, - receipt, - ) -} - -pub(super) fn write_scope_receipt( - store_root: &Path, - receipt: &ScopeRootRetentionReceiptV1, -) -> Result<(), CodeGenerationRetentionErrorV1> { - receipt_store::write_receipt( - store_root, - &SCOPE_RECEIPT_STORE, - &receipt.receipt_digest, - receipt, - ) -} - pub(super) fn scope_directory_exists(path: &Path) -> Result { match std::fs::symlink_metadata(path) { Ok(metadata) if metadata.file_type().is_dir() => Ok(true), diff --git a/crates/tracedecay-code-index-retention/src/code_index_generations/tests.rs b/crates/tracedecay-code-index-retention/src/code_index_generations/tests.rs index e97d9ee5be..0c6f6b8d1e 100644 --- a/crates/tracedecay-code-index-retention/src/code_index_generations/tests.rs +++ b/crates/tracedecay-code-index-retention/src/code_index_generations/tests.rs @@ -1,6 +1,7 @@ #![allow(clippy::expect_used, clippy::unwrap_used)] use super::*; +use super::{journal, receipt_store}; use tracedecay_domain::sha256_hex_suffix; mod graph_replay_pool_lock_tests; @@ -1304,8 +1305,12 @@ fn text_artifact_recovery_rolls_back_before_receipt_and_commits_after_receipt() active_pointer: plan.active_pointer.clone(), receipt: receipt.clone(), }; - persist_text_artifact_transaction(store.path(), &transaction) - .expect("journal artifact retention"); + journal::persist_journal( + store.path(), + &TEXT_ARTIFACT_TRANSACTION_JOURNAL, + &transaction, + ) + .expect("journal artifact retention"); stage_collectable_text_artifacts(store.path(), &transaction).expect("quarantine artifact"); assert!(!orphan_path.exists()); recover_code_generation_retention(store.path(), &BTreeSet::new(), None) @@ -1315,11 +1320,21 @@ fn text_artifact_recovery_rolls_back_before_receipt_and_commits_after_receipt() "uncommitted artifact staging must roll back" ); - persist_text_artifact_transaction(store.path(), &transaction) - .expect("journal second transaction"); + journal::persist_journal( + store.path(), + &TEXT_ARTIFACT_TRANSACTION_JOURNAL, + &transaction, + ) + .expect("journal second transaction"); stage_collectable_text_artifacts(store.path(), &transaction) .expect("quarantine second artifact"); - write_text_artifact_receipt(store.path(), &receipt).expect("durably commit artifact receipt"); + receipt_store::write_receipt( + store.path(), + &TEXT_ARTIFACT_RECEIPT_STORE, + &receipt.receipt_digest, + &receipt, + ) + .expect("durably commit artifact receipt"); recover_code_generation_retention(store.path(), &BTreeSet::new(), None) .expect("finish a committed artifact transaction"); assert!( @@ -1352,8 +1367,12 @@ fn cancellable_recovery_preserves_pending_artifact_journal_for_retry() { active_pointer: plan.active_pointer.clone(), receipt, }; - persist_text_artifact_transaction(store.path(), &transaction) - .expect("journal artifact retention"); + journal::persist_journal( + store.path(), + &TEXT_ARTIFACT_TRANSACTION_JOURNAL, + &transaction, + ) + .expect("journal artifact retention"); stage_collectable_text_artifacts(store.path(), &transaction) .expect("quarantine uncommitted candidate"); assert!(!orphan_path.exists()); @@ -1781,7 +1800,8 @@ fn recovery_restores_quarantined_generations_without_a_durable_receipt() { let generations_root = store.path().join(GENERATIONS_DIRECTORY); let staged_root = transaction_stage_root(store.path(), &receipt); - persist_transaction(store.path(), &transaction).expect("persist transaction journal"); + journal::persist_journal(store.path(), &GENERATION_TRANSACTION_JOURNAL, &transaction) + .expect("persist transaction journal"); stage_collectable_generations(store.path(), &transaction).expect("stage generation"); assert!(!generations_root.join(&collectable.generation_file).exists()); assert!(staged_root.join(&collectable.generation_file).is_file()); @@ -2366,7 +2386,8 @@ fn scope_recovery_restores_quarantined_scopes_without_a_durable_receipt() { let staged_root = scope_stage_root(store.path(), &receipt); // Crash exactly between quarantine and the durable receipt. - persist_scope_transaction(store.path(), &transaction).expect("persist journal"); + journal::persist_journal(store.path(), &SCOPE_TRANSACTION_JOURNAL, &transaction) + .expect("persist journal"); quarantine .stage(&transaction.receipt.collected_scopes) .expect("quarantine stranded scope"); @@ -2412,11 +2433,18 @@ fn scope_recovery_completes_collection_once_the_receipt_is_durable() { // Crash after the receipt is durable but before the quarantine is // unlinked: the decision is committed, so recovery rolls forward. - persist_scope_transaction(store.path(), &transaction).expect("persist journal"); + journal::persist_journal(store.path(), &SCOPE_TRANSACTION_JOURNAL, &transaction) + .expect("persist journal"); quarantine .stage(&transaction.receipt.collected_scopes) .expect("quarantine stranded scope"); - write_scope_receipt(store.path(), &receipt).expect("commit reconciliation receipt"); + receipt_store::write_receipt( + store.path(), + &SCOPE_RECEIPT_STORE, + &receipt.receipt_digest, + &receipt, + ) + .expect("commit reconciliation receipt"); recover_scope_root_retention(store.path()).expect("recover committed reconciliation"); @@ -2999,7 +3027,8 @@ fn pointer_rewrite_fixture() -> PointerRewriteFixture { active_pointer: Some(original.clone()), receipt, }; - persist_transaction(store.path(), &transaction).expect("journal the collection unit"); + journal::persist_journal(store.path(), &GENERATION_TRANSACTION_JOURNAL, &transaction) + .expect("journal the collection unit"); PointerRewriteFixture { store, original, @@ -3077,8 +3106,13 @@ fn recovery_keeps_the_rewritten_index_once_the_receipt_is_durable() { .expect("publish the rewritten index"); stage_collectable_generations(fixture.store.path(), &fixture.transaction) .expect("quarantine the collectable generations"); - write_receipt(fixture.store.path(), &fixture.transaction.receipt) - .expect("commit the deletion receipt"); + receipt_store::write_receipt( + fixture.store.path(), + &GENERATION_RECEIPT_STORE, + &fixture.transaction.receipt.receipt_digest, + &fixture.transaction.receipt, + ) + .expect("commit the deletion receipt"); recover_code_generation_retention(fixture.store.path(), &BTreeSet::new(), None) .expect("finish a committed collection unit"); @@ -3099,8 +3133,13 @@ fn recovery_completes_a_committed_rewrite_that_never_reached_the_pointer() { let fixture = pointer_rewrite_fixture(); stage_collectable_generations(fixture.store.path(), &fixture.transaction) .expect("quarantine the collectable generations"); - write_receipt(fixture.store.path(), &fixture.transaction.receipt) - .expect("commit the deletion receipt"); + receipt_store::write_receipt( + fixture.store.path(), + &GENERATION_RECEIPT_STORE, + &fixture.transaction.receipt.receipt_digest, + &fixture.transaction.receipt, + ) + .expect("commit the deletion receipt"); assert_eq!( read_active_pointer(fixture.store.path()).expect("read pointer"), fixture.original, diff --git a/crates/tracedecay-code-index-retention/src/code_index_generations/tests/graph_replay_release_tests.rs b/crates/tracedecay-code-index-retention/src/code_index_generations/tests/graph_replay_release_tests.rs index d8a0ad9bf8..d1a181eec6 100644 --- a/crates/tracedecay-code-index-retention/src/code_index_generations/tests/graph_replay_release_tests.rs +++ b/crates/tracedecay-code-index-retention/src/code_index_generations/tests/graph_replay_release_tests.rs @@ -193,7 +193,8 @@ fn missing_staged_generation_blocks_pool_exposure_before_receipt() { active_pointer: plan.active_pointer.clone(), receipt: receipt.clone(), }; - persist_transaction(store.path(), &transaction).expect("persist transaction journal"); + journal::persist_journal(store.path(), &GENERATION_TRANSACTION_JOURNAL, &transaction) + .expect("persist transaction journal"); stage_collectable_generations(store.path(), &transaction).expect("stage generation"); let missing = transaction_stage_root(store.path(), &receipt) .join(&receipt.deleted_generations[0].generation_file); @@ -241,7 +242,8 @@ fn stale_reconciler_retirement_interleaves_with_retention_without_orphan_or_miss }; // Retention: journal, quarantine, and expose before the receipt. - persist_transaction(store.path(), &transaction).expect("persist transaction journal"); + journal::persist_journal(store.path(), &GENERATION_TRANSACTION_JOURNAL, &transaction) + .expect("persist transaction journal"); stage_collectable_generations(store.path(), &transaction).expect("stage generation"); { let pool_lock = @@ -261,7 +263,13 @@ fn stale_reconciler_retirement_interleaves_with_retention_without_orphan_or_miss // Retention: the receipt and its release event become durable, in the // same order the production executor uses (events before receipt). graph_replay_release::write_events(store.path(), &receipt).expect("write release events"); - write_receipt(store.path(), &receipt).expect("write durable receipt"); + receipt_store::write_receipt( + store.path(), + &GENERATION_RECEIPT_STORE, + &receipt.receipt_digest, + &receipt, + ) + .expect("write durable receipt"); let page = code_generation_graph_replay_release_page(store.path(), None) .expect("read durable release event"); assert_eq!(page.releases.len(), 1); @@ -333,7 +341,8 @@ fn stale_reconciler_retirement_interleaves_with_retention_without_orphan_or_miss &|| false, ) .expect("cleanup retries after release completion"); - clear_transaction(store.path()).expect("clear transaction journal"); + journal::clear_journal(store.path(), &GENERATION_TRANSACTION_JOURNAL) + .expect("clear transaction journal"); // No orphan: the consumed release's pool copy must not be resurrected, // and no release event survives without its pool copy. diff --git a/crates/tracedecay-code-index-retention/src/code_index_generations/text_artifacts.rs b/crates/tracedecay-code-index-retention/src/code_index_generations/text_artifacts.rs index a6e90c8cd9..b1a938e25d 100644 --- a/crates/tracedecay-code-index-retention/src/code_index_generations/text_artifacts.rs +++ b/crates/tracedecay-code-index-retention/src/code_index_generations/text_artifacts.rs @@ -34,7 +34,7 @@ use super::{ validate_sealed_generation_identity, validate_text_artifact_descriptor, }; -const TEXT_ARTIFACT_TRANSACTION_JOURNAL: BoundedJournalSpec< +pub(super) const TEXT_ARTIFACT_TRANSACTION_JOURNAL: BoundedJournalSpec< CodeTextArtifactRetentionTransactionV1, > = BoundedJournalSpec { file_name: TEXT_ARTIFACT_TRANSACTION_FILE, @@ -44,7 +44,7 @@ const TEXT_ARTIFACT_TRANSACTION_JOURNAL: BoundedJournalSpec< validate: validate_text_artifact_transaction, }; -const TEXT_ARTIFACT_RECEIPT_STORE: ReceiptStoreSpec = ReceiptStoreSpec { +pub(super) const TEXT_ARTIFACT_RECEIPT_STORE: ReceiptStoreSpec = ReceiptStoreSpec { directory: TEXT_ARTIFACT_RECEIPTS_DIRECTORY, label: "text-artifact retention receipt", }; @@ -653,7 +653,7 @@ pub(super) fn execute_text_artifact_retention_under_store_lock( active_pointer: active_pointer.cloned(), receipt: receipt.clone(), }; - persist_text_artifact_transaction(store_root, &transaction)?; + persist_journal(store_root, &TEXT_ARTIFACT_TRANSACTION_JOURNAL, &transaction)?; let result = (|| { if observe_cancel(is_cancelled) { return Err(CodeGenerationRetentionErrorV1::Cancelled); @@ -671,14 +671,24 @@ pub(super) fn execute_text_artifact_retention_under_store_lock( if observe_cancel(is_cancelled) { return Err(CodeGenerationRetentionErrorV1::Cancelled); } - write_text_artifact_receipt(store_root, &receipt)?; + receipt_store::write_receipt( + store_root, + &TEXT_ARTIFACT_RECEIPT_STORE, + &receipt.receipt_digest, + &receipt, + )?; cleanup_committed_text_artifact_transaction(store_root, &transaction)?; - clear_text_artifact_transaction(store_root) + clear_journal(store_root, &TEXT_ARTIFACT_TRANSACTION_JOURNAL) })(); if let Err(error) = result { - if !text_artifact_receipt_is_durable(store_root, &receipt)? { + if !receipt_store::receipt_is_durable( + store_root, + &TEXT_ARTIFACT_RECEIPT_STORE, + &receipt.receipt_digest, + &receipt, + )? { rollback_staged_text_artifact_transaction(store_root, &transaction)?; - clear_text_artifact_transaction(store_root)?; + clear_journal(store_root, &TEXT_ARTIFACT_TRANSACTION_JOURNAL)?; } return Err(error); } @@ -688,15 +698,20 @@ pub(super) fn execute_text_artifact_retention_under_store_lock( pub(super) fn recover_pending_text_artifact_transaction_unlocked( store_root: &Path, ) -> Result<(), CodeGenerationRetentionErrorV1> { - let Some(transaction) = load_text_artifact_transaction(store_root)? else { + let Some(transaction) = load_journal(store_root, &TEXT_ARTIFACT_TRANSACTION_JOURNAL)? else { return Ok(()); }; - if text_artifact_receipt_is_durable(store_root, &transaction.receipt)? { + if receipt_store::receipt_is_durable( + store_root, + &TEXT_ARTIFACT_RECEIPT_STORE, + &transaction.receipt.receipt_digest, + &transaction.receipt, + )? { cleanup_committed_text_artifact_transaction(store_root, &transaction)?; } else { rollback_staged_text_artifact_transaction(store_root, &transaction)?; } - clear_text_artifact_transaction(store_root) + clear_journal(store_root, &TEXT_ARTIFACT_TRANSACTION_JOURNAL) } pub(super) fn text_artifact_transaction_path(store_root: &Path) -> PathBuf { @@ -712,19 +727,6 @@ pub(super) fn text_artifact_transaction_stage_root( .join(&receipt.receipt_digest) } -pub(super) fn persist_text_artifact_transaction( - store_root: &Path, - transaction: &CodeTextArtifactRetentionTransactionV1, -) -> Result<(), CodeGenerationRetentionErrorV1> { - persist_journal(store_root, &TEXT_ARTIFACT_TRANSACTION_JOURNAL, transaction) -} - -pub(super) fn load_text_artifact_transaction( - store_root: &Path, -) -> Result, CodeGenerationRetentionErrorV1> { - load_journal(store_root, &TEXT_ARTIFACT_TRANSACTION_JOURNAL) -} - pub(super) fn validate_text_artifact_transaction( transaction: &CodeTextArtifactRetentionTransactionV1, ) -> Result<(), CodeGenerationRetentionErrorV1> { @@ -815,18 +817,6 @@ pub(super) fn validate_text_artifact_candidate( Ok(()) } -pub(super) fn text_artifact_receipt_is_durable( - store_root: &Path, - receipt: &CodeTextArtifactRetentionReceiptV1, -) -> Result { - receipt_store::receipt_is_durable( - store_root, - &TEXT_ARTIFACT_RECEIPT_STORE, - &receipt.receipt_digest, - receipt, - ) -} - #[cfg(test)] pub(super) fn stage_collectable_text_artifacts( store_root: &Path, @@ -1008,12 +998,6 @@ pub(super) fn ensure_text_artifact_transaction_liveness( Ok(()) } -pub(super) fn clear_text_artifact_transaction( - store_root: &Path, -) -> Result<(), CodeGenerationRetentionErrorV1> { - clear_journal(store_root, &TEXT_ARTIFACT_TRANSACTION_JOURNAL) -} - pub(super) fn build_text_artifact_receipt( plan: &CodeGenerationRetentionPlanV1, active_pointer: Option<&DurablePublicationPointerV1>, @@ -1055,18 +1039,6 @@ pub(super) fn build_text_artifact_receipt( }) } -pub(super) fn write_text_artifact_receipt( - store_root: &Path, - receipt: &CodeTextArtifactRetentionReceiptV1, -) -> Result<(), CodeGenerationRetentionErrorV1> { - receipt_store::write_receipt( - store_root, - &TEXT_ARTIFACT_RECEIPT_STORE, - &receipt.receipt_digest, - receipt, - ) -} - pub(super) fn total_text_artifact_bytes(artifacts: &[CodeTextArtifactRetentionCandidateV1]) -> u64 { artifacts.iter().fold(0_u64, |total, artifact| { total.saturating_add(artifact.size_bytes) From 41ff4ec16eed8f68630c3baf42f1e15db9b34d61 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:09:55 +0000 Subject: [PATCH 106/182] simplify(pass-5/5): fold artifact gauge record stubs Each lexical-artifact recorder is one function with the gauge inside the hotpath cfg, not a feature-off twin. Also drop the unused graph abstention getter and the stale semantic-crate note, and keep the shared bench helpers out of src/bin. Co-authored-by: Zack Jackson --- .../src/code_index_scheduler/serving.rs | 5 - crates/tracedecay-global-db/src/lib.rs | 5 +- .../artifact_bench.rs => bench_support.rs} | 0 .../src/bin/tracedecay_index_bench.rs | 9 +- .../src/bin/tracedecay_search_bench.rs | 10 +- .../lexical/projection/artifact/builder.rs | 252 +++++++++--------- .../lexical/projection/artifact/postings.rs | 2 +- 7 files changed, 135 insertions(+), 148 deletions(-) rename crates/tracedecay-query/src/{bin/artifact_bench.rs => bench_support.rs} (100%) diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/serving.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/serving.rs index 78c802ef5a..8b6a9af0ae 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/serving.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/serving.rs @@ -2039,11 +2039,6 @@ impl LatestCompleteCodeIndexV1 { pub fn graph_edges(&self) -> &[tracedecay_domain::CanonicalRelationEdgeV1] { self.generation.edges() } - - #[cfg(test)] - pub fn graph_abstentions(&self) -> &[crate::code_index::chunks::CodeIndexEdgeAbstentionV1] { - self.generation.edge_abstentions() - } } impl LatestCodeTextGenerationV1 { diff --git a/crates/tracedecay-global-db/src/lib.rs b/crates/tracedecay-global-db/src/lib.rs index 2cfbce86dc..db3c9a3e41 100644 --- a/crates/tracedecay-global-db/src/lib.rs +++ b/crates/tracedecay-global-db/src/lib.rs @@ -10,10 +10,9 @@ //! //! ## Dependency edges //! -//! Depends on `tracedecay-runtime-core` (kernel db/errors/storage/config), +//! Depends on `tracedecay-runtime-core` (kernel db/errors/storage/config) and //! `tracedecay-sessions` (session runtime, `lcm::contracts`, -//! `retrieval_content`), and `tracedecay-semantic` (resource ceilings, default -//! embedding model). All three are proven acyclic, `cargo tree -p -e +//! `retrieval_content`). Both edges are acyclic: `cargo tree -p -e //! normal` never names this crate. `RuntimeExternalSourceStore` and //! `GlobalDbObservationStore` is deliberately a root-owned adapter. It takes //! a guarded database client issued by the registered owner, so the composition diff --git a/crates/tracedecay-query/src/bin/artifact_bench.rs b/crates/tracedecay-query/src/bench_support.rs similarity index 100% rename from crates/tracedecay-query/src/bin/artifact_bench.rs rename to crates/tracedecay-query/src/bench_support.rs diff --git a/crates/tracedecay-query/src/bin/tracedecay_index_bench.rs b/crates/tracedecay-query/src/bin/tracedecay_index_bench.rs index b672984dbe..59a1b4729e 100644 --- a/crates/tracedecay-query/src/bin/tracedecay_index_bench.rs +++ b/crates/tracedecay-query/src/bin/tracedecay_index_bench.rs @@ -49,13 +49,13 @@ #![allow(clippy::print_stdout, clippy::print_stderr)] -#[path = "artifact_bench.rs"] +#[path = "../bench_support.rs"] mod artifact_bench; use artifact_bench::{ ActiveControl, AdmittedFile, ApplyingProjectionSink, MemoryPublicationStore, SealedDrainBounds, - default_corpus_root, drain_pages, identity, millis, peak_rss_bytes, percentile, replicate, - sealed_state_digest, + default_corpus_root, drain_pages, identity, load_corpus, millis, peak_rss_bytes, percentile, + replicate, sealed_state_digest, }; use std::collections::{BTreeMap, BTreeSet}; use std::num::NonZeroUsize; @@ -70,8 +70,7 @@ use tracedecay_code_index::production::{ CodeIndexBuildRequestV1, CodeIndexCapturedFileV1, CodeIndexExecutionControlV1, CodeIndexProductionConfigV1, CodeIndexProductionOwnerV1, CodeIndexPublishedGenerationV1, CodeIndexRepositoryParseIdentityV1, PhysicalCodeArtifactPoolStatsV1, - VerifiedSealedLexicalPageSourceV1, VerifiedSealedLexicalPageV1, - VerifiedSealedLexicalSourceReceiptV1, + VerifiedSealedLexicalPageV1, VerifiedSealedLexicalSourceReceiptV1, }; use tracedecay_domain::{ ChunkerRevision, ComponentRevision, ContentDigest, FileOccurrenceId, FreshnessCompatibilityV1, diff --git a/crates/tracedecay-query/src/bin/tracedecay_search_bench.rs b/crates/tracedecay-query/src/bin/tracedecay_search_bench.rs index 00de73661e..c84de10e31 100644 --- a/crates/tracedecay-query/src/bin/tracedecay_search_bench.rs +++ b/crates/tracedecay-query/src/bin/tracedecay_search_bench.rs @@ -27,15 +27,16 @@ #![allow(clippy::print_stdout, clippy::print_stderr)] -#[path = "artifact_bench.rs"] +#[path = "../bench_support.rs"] mod artifact_bench; use artifact_bench::{ ActiveControl, AdmittedFile, ApplyingProjectionSink, MemoryPublicationStore, SealedDrainBounds, - default_corpus_root, drain_pages, identity, millis, peak_rss_bytes, percentile, replicate, - sealed_state_digest, + default_corpus_root, drain_pages, identity, load_corpus, millis, peak_rss_bytes, percentile, + replicate, sealed_state_digest, }; use std::collections::BTreeSet; +use std::io::Read; use std::path::{Path, PathBuf}; use std::process::ExitCode; use std::sync::Arc; @@ -59,7 +60,8 @@ use tracedecay_domain::{ SourceNamespace, TemporalModeV1, TreeId, UtcMicros, VectorWatermark, }; use tracedecay_query::retrieval::exact::{ - CentralExactAdmissionAuthorityV1, ExactLane, ExactLaneRequest, ExactLaneRetriever, + CentralExactAdmissionAuthorityV1, ExactAdmissionAuthority, ExactLane, ExactLaneRequest, + ExactLaneRetriever, }; use tracedecay_query::retrieval::lexical::{ CODE_LEXICAL_ARTIFACT_QUERY_CACHE_BUDGET_BYTES_V1, CodeLexicalArtifactBuilderV1, diff --git a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/builder.rs b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/builder.rs index a9b173f33d..1edad59958 100644 --- a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/builder.rs +++ b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/builder.rs @@ -6546,177 +6546,169 @@ fn verify_source_receipt( Ok(()) } -#[cfg(feature = "hotpath")] fn record_finalization_step(step: &CodeLexicalArtifactFinalizationStepV1) { - match step { - CodeLexicalArtifactFinalizationStepV1::Pending { completed_rows, .. } => { - hotpath::gauge!("query.artifact.finalization.outcome.pending_total").inc(1u64); - crate::hotpath_metrics::Residency::Rebuilding.record("query.artifact.residency"); - hotpath::gauge!("query.artifact.rows").set(*completed_rows); - } - CodeLexicalArtifactFinalizationStepV1::Ready(receipt) => { - hotpath::gauge!("query.artifact.finalization.outcome.ready_total").inc(1u64); - crate::hotpath_metrics::Residency::Warm.record("query.artifact.residency"); - hotpath::gauge!("query.artifact.pages").set(receipt.page_count()); - hotpath::gauge!("query.artifact.bytes").set(receipt.file_size_bytes()); + #[cfg(feature = "hotpath")] + { + match step { + CodeLexicalArtifactFinalizationStepV1::Pending { completed_rows, .. } => { + hotpath::gauge!("query.artifact.finalization.outcome.pending_total").inc(1u64); + crate::hotpath_metrics::Residency::Rebuilding.record("query.artifact.residency"); + hotpath::gauge!("query.artifact.rows").set(*completed_rows); + } + CodeLexicalArtifactFinalizationStepV1::Ready(receipt) => { + hotpath::gauge!("query.artifact.finalization.outcome.ready_total").inc(1u64); + crate::hotpath_metrics::Residency::Warm.record("query.artifact.residency"); + hotpath::gauge!("query.artifact.pages").set(receipt.page_count()); + hotpath::gauge!("query.artifact.bytes").set(receipt.file_size_bytes()); + } } } -} - -#[cfg(not(feature = "hotpath"))] -fn record_finalization_step(step: &CodeLexicalArtifactFinalizationStepV1) { + #[cfg(not(feature = "hotpath"))] let _ = step; } -#[cfg(feature = "hotpath")] fn record_batch_outcome( result: &Result, ) { - match result { - Ok(_) => { - hotpath::gauge!("query.artifact.batch.outcome.committed_total").inc(1u64); - } - Err(CodeLexicalArtifactErrorV1::Interrupted(_)) => { - hotpath::gauge!("query.artifact.batch.outcome.interrupted_total").inc(1u64); - } - Err(_) => { - hotpath::gauge!("query.artifact.batch.outcome.failed_total").inc(1u64); + #[cfg(feature = "hotpath")] + { + match result { + Ok(_) => { + hotpath::gauge!("query.artifact.batch.outcome.committed_total").inc(1u64); + } + Err(CodeLexicalArtifactErrorV1::Interrupted(_)) => { + hotpath::gauge!("query.artifact.batch.outcome.interrupted_total").inc(1u64); + } + Err(_) => { + hotpath::gauge!("query.artifact.batch.outcome.failed_total").inc(1u64); + } } } -} - -#[cfg(not(feature = "hotpath"))] -fn record_batch_outcome( - result: &Result, -) { + #[cfg(not(feature = "hotpath"))] let _ = result; } -#[cfg(feature = "hotpath")] -fn record_prepared_batch_metrics(pages: &[PreparedCodeLexicalArtifactPageV1]) { - let documents = pages.iter().map(|page| page.documents.len()).sum::(); - let source_bytes = pages - .iter() - .map(PreparedCodeLexicalArtifactPageV1::source_retained_bytes) - .sum::(); - let prepared_bytes = pages - .iter() - .map(PreparedCodeLexicalArtifactPageV1::retained_owned_bytes) - .sum::(); - let effective_workers = tracedecay_code_index::parallelism::indexing_workers().min(pages.len()); - let mut scratch = pages - .iter() - .map(PreparedCodeLexicalArtifactPageV1::preparation_scratch_bytes) - .collect::>(); - scratch.sort_unstable_by(|left, right| right.cmp(left)); - let active_scratch = scratch.into_iter().take(effective_workers).sum::(); - hotpath::gauge!("query.artifact.batch.prepared_pages_total").inc(pages.len() as u64); - hotpath::gauge!("query.artifact.batch.prepared_documents_total").inc(documents as u64); - hotpath::gauge!("query.artifact.batch.source_bytes_total").inc(source_bytes as u64); - hotpath::gauge!("query.artifact.batch.prepared_bytes_total").inc(prepared_bytes as u64); - hotpath::gauge!("query.artifact.batch.active_scratch_bytes_total").inc(active_scratch as u64); - hotpath::gauge!("query.artifact.batch.effective_workers").set(effective_workers as u64); -} - -#[cfg(not(feature = "hotpath"))] fn record_prepared_batch_metrics(pages: &[PreparedCodeLexicalArtifactPageV1]) { + #[cfg(feature = "hotpath")] + { + let documents = pages.iter().map(|page| page.documents.len()).sum::(); + let source_bytes = pages + .iter() + .map(PreparedCodeLexicalArtifactPageV1::source_retained_bytes) + .sum::(); + let prepared_bytes = pages + .iter() + .map(PreparedCodeLexicalArtifactPageV1::retained_owned_bytes) + .sum::(); + let effective_workers = + tracedecay_code_index::parallelism::indexing_workers().min(pages.len()); + let mut scratch = pages + .iter() + .map(PreparedCodeLexicalArtifactPageV1::preparation_scratch_bytes) + .collect::>(); + scratch.sort_unstable_by(|left, right| right.cmp(left)); + let active_scratch = scratch.into_iter().take(effective_workers).sum::(); + hotpath::gauge!("query.artifact.batch.prepared_pages_total").inc(pages.len() as u64); + hotpath::gauge!("query.artifact.batch.prepared_documents_total").inc(documents as u64); + hotpath::gauge!("query.artifact.batch.source_bytes_total").inc(source_bytes as u64); + hotpath::gauge!("query.artifact.batch.prepared_bytes_total").inc(prepared_bytes as u64); + hotpath::gauge!("query.artifact.batch.active_scratch_bytes_total") + .inc(active_scratch as u64); + hotpath::gauge!("query.artifact.batch.effective_workers").set(effective_workers as u64); + } + #[cfg(not(feature = "hotpath"))] let _ = pages; } -#[cfg(feature = "hotpath")] -fn record_batch_import_metrics(pages: &[PreparedCodeLexicalArtifactPageV1]) { - let imports = pages.iter().map(|page| page.imports.len()).sum::(); - hotpath::gauge!("query.artifact.batch.import_rows_total").inc(imports as u64); -} - -#[cfg(not(feature = "hotpath"))] fn record_batch_import_metrics(pages: &[PreparedCodeLexicalArtifactPageV1]) { + #[cfg(feature = "hotpath")] + { + let imports = pages.iter().map(|page| page.imports.len()).sum::(); + hotpath::gauge!("query.artifact.batch.import_rows_total").inc(imports as u64); + } + #[cfg(not(feature = "hotpath"))] let _ = pages; } -#[cfg(feature = "hotpath")] -fn record_batch_posting_metrics(pages: &[PreparedCodeLexicalArtifactPageV1]) { - let relational_postings = pages - .iter() - .flat_map(|page| &page.documents) - .map(|document| document.term_postings.len() + document.exact_postings.len()) - .sum::(); - let ngram_shards = pages - .iter() - .map(|page| page.ngram_shards.len()) - .sum::(); - let ngram_documents = pages - .iter() - .flat_map(|page| &page.ngram_shards) - .map(|shard| shard.cardinality) - .sum::(); - let ngram_bytes = pages - .iter() - .flat_map(|page| &page.ngram_shards) - .map(|shard| shard.documents.len()) - .sum::(); - hotpath::gauge!("query.artifact.batch.posting_rows_total").inc(relational_postings as u64); - hotpath::gauge!("query.artifact.batch.ngram_shard_rows_total").inc(ngram_shards as u64); - hotpath::gauge!("query.artifact.batch.ngram_documents_total").inc(ngram_documents); - hotpath::gauge!("query.artifact.batch.ngram_bytes_total").inc(ngram_bytes as u64); -} - -#[cfg(not(feature = "hotpath"))] fn record_batch_posting_metrics(pages: &[PreparedCodeLexicalArtifactPageV1]) { + #[cfg(feature = "hotpath")] + { + let relational_postings = pages + .iter() + .flat_map(|page| &page.documents) + .map(|document| document.term_postings.len() + document.exact_postings.len()) + .sum::(); + let ngram_shards = pages + .iter() + .map(|page| page.ngram_shards.len()) + .sum::(); + let ngram_documents = pages + .iter() + .flat_map(|page| &page.ngram_shards) + .map(|shard| shard.cardinality) + .sum::(); + let ngram_bytes = pages + .iter() + .flat_map(|page| &page.ngram_shards) + .map(|shard| shard.documents.len()) + .sum::(); + hotpath::gauge!("query.artifact.batch.posting_rows_total").inc(relational_postings as u64); + hotpath::gauge!("query.artifact.batch.ngram_shard_rows_total").inc(ngram_shards as u64); + hotpath::gauge!("query.artifact.batch.ngram_documents_total").inc(ngram_documents); + hotpath::gauge!("query.artifact.batch.ngram_bytes_total").inc(ngram_bytes as u64); + } + #[cfg(not(feature = "hotpath"))] let _ = pages; } -#[cfg(feature = "hotpath")] -fn record_batch_row_metrics(pages: &[PreparedCodeLexicalArtifactPageV1]) { - let rows = pages.iter().map(|page| page.documents.len()).sum::(); - hotpath::gauge!("query.artifact.batch.document_rows_total").inc(rows as u64); -} - -#[cfg(not(feature = "hotpath"))] fn record_batch_row_metrics(pages: &[PreparedCodeLexicalArtifactPageV1]) { + #[cfg(feature = "hotpath")] + { + let rows = pages.iter().map(|page| page.documents.len()).sum::(); + hotpath::gauge!("query.artifact.batch.document_rows_total").inc(rows as u64); + } + #[cfg(not(feature = "hotpath"))] let _ = pages; } -#[cfg(feature = "hotpath")] -fn record_batch_receipt_metrics(pages: &[PreparedCodeLexicalArtifactPageV1]) { - hotpath::gauge!("query.artifact.batch.receipt_rows_total").inc(pages.len() as u64); -} - -#[cfg(not(feature = "hotpath"))] fn record_batch_receipt_metrics(pages: &[PreparedCodeLexicalArtifactPageV1]) { + #[cfg(feature = "hotpath")] + { + hotpath::gauge!("query.artifact.batch.receipt_rows_total").inc(pages.len() as u64); + } + #[cfg(not(feature = "hotpath"))] let _ = pages; } -#[cfg(feature = "hotpath")] fn record_batch_prefix_limit(limit: CodeLexicalArtifactBatchLimitV1) { - match limit { - CodeLexicalArtifactBatchLimitV1::Memory => { - hotpath::gauge!("query.artifact.batch.prefix_limited.memory_total").inc(1u64); - } - CodeLexicalArtifactBatchLimitV1::PreparedRows => { - hotpath::gauge!("query.artifact.batch.prefix_limited.prepared_rows_total").inc(1u64); - } - CodeLexicalArtifactBatchLimitV1::EstimatedWriteBytes => { - hotpath::gauge!("query.artifact.batch.prefix_limited.estimated_write_bytes_total") - .inc(1u64); + #[cfg(feature = "hotpath")] + { + match limit { + CodeLexicalArtifactBatchLimitV1::Memory => { + hotpath::gauge!("query.artifact.batch.prefix_limited.memory_total").inc(1u64); + } + CodeLexicalArtifactBatchLimitV1::PreparedRows => { + hotpath::gauge!("query.artifact.batch.prefix_limited.prepared_rows_total") + .inc(1u64); + } + CodeLexicalArtifactBatchLimitV1::EstimatedWriteBytes => { + hotpath::gauge!("query.artifact.batch.prefix_limited.estimated_write_bytes_total") + .inc(1u64); + } } } -} - -#[cfg(not(feature = "hotpath"))] -fn record_batch_prefix_limit(limit: CodeLexicalArtifactBatchLimitV1) { + #[cfg(not(feature = "hotpath"))] let _ = limit; } -#[cfg(feature = "hotpath")] -fn record_artifact_progress(progress: &CodeLexicalArtifactBuildProgressV1) { - hotpath::gauge!("query.artifact.pages").set(progress.next_page_ordinal); - hotpath::gauge!("query.artifact.rows").set(progress.completed_chunks); - hotpath::gauge!("query.artifact.bytes").set(progress.completed_payload_bytes); -} - -#[cfg(not(feature = "hotpath"))] fn record_artifact_progress(progress: &CodeLexicalArtifactBuildProgressV1) { + #[cfg(feature = "hotpath")] + { + hotpath::gauge!("query.artifact.pages").set(progress.next_page_ordinal); + hotpath::gauge!("query.artifact.rows").set(progress.completed_chunks); + hotpath::gauge!("query.artifact.bytes").set(progress.completed_payload_bytes); + } + #[cfg(not(feature = "hotpath"))] let _ = progress; } diff --git a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/postings.rs b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/postings.rs index 8ff71b510b..dd05ecfb8c 100644 --- a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/postings.rs +++ b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/postings.rs @@ -111,7 +111,7 @@ pub(super) fn query_ngrams(bytes: &[u8]) -> BTreeSet { bytes.windows(width).map(pack_byte_ngram).collect() } -pub(super) fn pack_byte_ngram(bytes: &[u8]) -> u32 { +pub(in crate::retrieval::lexical) fn pack_byte_ngram(bytes: &[u8]) -> u32 { debug_assert!((1..=3).contains(&bytes.len())); bytes .iter() From ac340203fa3e55697b63dc6794c27318561d70cf Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:10:04 +0000 Subject: [PATCH 107/182] simplify(pass-5/5): drop dead helpers and share claude dispatch Unused Scout assembly, model config, tool-prefix, and CLI-fallback wrappers are gone. SessionStart and Stop share one guidance hook, and Codex user ingest calls the shared session ingest directly. Co-authored-by: Zack Jackson --- .../src/agents/context_scout/owner.rs | 30 ------ .../src/agents/context_scout/ports.rs | 45 +------- .../src/agents/prompt_rules.rs | 6 -- .../src/hooks/claude.rs | 101 ++++-------------- .../tracedecay-agent-hosts/src/hooks/codex.rs | 11 +- .../tracedecay-agent-hosts/src/tool_name.rs | 13 +-- 6 files changed, 28 insertions(+), 178 deletions(-) diff --git a/crates/tracedecay-agent-hosts/src/agents/context_scout/owner.rs b/crates/tracedecay-agent-hosts/src/agents/context_scout/owner.rs index 5efdff117c..92a12c933f 100644 --- a/crates/tracedecay-agent-hosts/src/agents/context_scout/owner.rs +++ b/crates/tracedecay-agent-hosts/src/agents/context_scout/owner.rs @@ -136,18 +136,6 @@ pub fn unregister_registered_context_scout_owner( } impl ProjectContextScoutOwnerV1 { - pub async fn startup_configured( - database: Database, - project_id: [u8; 16], - now: UtcMicros, - pin: ContextScoutConfigurationPinV1, - model_config: Option<&AutomationConfig>, - ) -> Option> { - let owner = Self::startup(database, project_id, now, model_config).await?; - owner.install_configuration(pin, model_config).await.ok()?; - Some(owner) - } - #[hotpath::measure( future = true, label = "hosts.agent.context_scout.startup", @@ -853,24 +841,6 @@ impl ProjectContextScoutOwnerV1 { Ok(status_with_recent(status, &recent)) } - pub async fn configure_model(&self, config: &AutomationConfig) { - let configuration = self.configuration.read().await; - let Some(control) = configuration - .as_ref() - .map(ContextScoutConfigurationPinV1::control) - else { - return; - }; - let model = context_scout_model_assistant_from_project_config(Some(config)); - if control - .model_path - .is_some_and(|expected| expected != model.backend()) - { - return; - } - self.runtime.lock().await.replace_model(model); - } - pub async fn claim( &self, address: ContextScoutAddressV1, diff --git a/crates/tracedecay-agent-hosts/src/agents/context_scout/ports.rs b/crates/tracedecay-agent-hosts/src/agents/context_scout/ports.rs index 0bc81ca056..21aef7c6e4 100644 --- a/crates/tracedecay-agent-hosts/src/agents/context_scout/ports.rs +++ b/crates/tracedecay-agent-hosts/src/agents/context_scout/ports.rs @@ -514,6 +514,7 @@ impl ProjectContextScoutAddressRegistryV1 { ContextScoutAddressBindOutcomeV1::Bound(address) } + #[cfg(test)] async fn resolve( &self, hook: &AdmittedContextScoutHookV1, @@ -794,50 +795,6 @@ where } } - #[hotpath::measure( - label = "context_scout_assemble_registered", - impl_type = "ContextScoutCanonicalInputAssemblerV1" - )] - pub async fn assemble_registered( - &self, - hook: &AdmittedContextScoutHookV1, - pin: &ContextScoutAuthorityPinV1, - context: &RequestContext, - observed_at: UtcMicros, - ) -> Option { - if !pin.matches_context(context, observed_at) { - return None; - } - let ContextScoutAddressResolveOutcomeV1::Resolved(address) = - self.registry.resolve(hook, pin).await - else { - return None; - }; - self.assemble(address, pin, context, observed_at).await - } - - #[hotpath::measure( - label = "context_scout_assemble_registered_exact", - impl_type = "ContextScoutCanonicalInputAssemblerV1" - )] - pub async fn assemble_registered_exact( - &self, - hook: &AdmittedContextScoutHookV1, - pin: &ContextScoutAuthorityPinV1, - lifecycle: &ContextScoutLifecycleAddressV1, - context: &RequestContext, - observed_at: UtcMicros, - ) -> Option { - let ContextScoutAddressResolveOutcomeV1::Resolved(address) = self - .registry - .resolve_current_exact(hook, pin, lifecycle, context, observed_at) - .await - else { - return None; - }; - self.assemble(address, pin, context, observed_at).await - } - #[hotpath::measure( label = "context_scout_bind_and_assemble", impl_type = "ContextScoutCanonicalInputAssemblerV1" diff --git a/crates/tracedecay-agent-hosts/src/agents/prompt_rules.rs b/crates/tracedecay-agent-hosts/src/agents/prompt_rules.rs index 1f2561ace6..6a77f192e9 100644 --- a/crates/tracedecay-agent-hosts/src/agents/prompt_rules.rs +++ b/crates/tracedecay-agent-hosts/src/agents/prompt_rules.rs @@ -201,12 +201,6 @@ pub(crate) fn standard_prompt_rules(marker: &str, options: &PromptRulesOptions) block } -/// The CLI-fallback paragraph every host's rules must carry; exposed so -/// integration tests can assert parity across hosts. -pub fn cli_fallback_paragraph() -> &'static str { - super::CLI_FALLBACK_PROMPT_RULES -} - /// End offset of a managed block whose marker heading ends at `search_from`: /// the next `\n## ` heading, the managed-skill index start marker, or EOF, /// whichever comes first. diff --git a/crates/tracedecay-agent-hosts/src/hooks/claude.rs b/crates/tracedecay-agent-hosts/src/hooks/claude.rs index cb47837d63..6b144df4a3 100644 --- a/crates/tracedecay-agent-hosts/src/hooks/claude.rs +++ b/crates/tracedecay-agent-hosts/src/hooks/claude.rs @@ -87,57 +87,7 @@ pub(super) fn is_code_research_prompt(prompt: &str) -> bool { /// Claude Code `SessionStart` hook handler. #[hotpath::measure(future = true, label = "hosts.hooks.claude.session_start")] pub async fn hook_claude_session_start(runtime: &HookRuntimeV1) -> i32 { - let started = Instant::now(); - let event = read_hook_event!(); - let (root, output) = claude_session_start_response(runtime, &event, started).await; - if !super::write_hook_output( - root.as_deref(), - tracedecay_hooks::HookHostV1::ClaudeCode, - &event, - &output, - ) - .await - { - return 1; - } - 0 -} - -/// Returns the identity-resolved root alongside the response so the handler -/// does not repeat the registry-probing resolution for output delivery. -async fn claude_session_start_response( - runtime: &HookRuntimeV1, - event: &str, - started: Instant, -) -> (Option, String) { - let parsed = serde_json::from_str::(event).unwrap_or(Value::Null); - // Resolve the project root the same identity-aware way the printed context - // does, including global-only stores and fresh harness-created worktrees. - let root = event_project_root_with_identity(runtime, &parsed).await; - let hook_telemetry = record_hook_invoked_parsed( - runtime, - root.as_deref(), - HintAgent::Claude, - "SessionStart", - event, - &parsed, - ); - let output = super::dispatch::dispatch_for_scope( - runtime, - tracedecay_hooks::HookHostV1::ClaudeCode, - event, - root.as_deref(), - Some(&hook_telemetry), - started, - ) - .await - .into_recorded_guidance(&hook_telemetry) - .flatten() - .map_or_else( - || serde_json::json!({}).to_string(), - |guidance| additional_context_json("SessionStart", &guidance), - ); - (root, output) + claude_guidance_hook(runtime, "SessionStart").await } /// Claude Code `PostCompact` hook handler. @@ -238,43 +188,28 @@ async fn claude_post_tool_use_response( /// `Stop` hook handler: submits the native turn boundary to the daemon. #[hotpath::measure(future = true, label = "hosts.hooks.claude.stop")] pub async fn hook_stop(runtime: &HookRuntimeV1) -> i32 { - let started = Instant::now(); - let event = read_hook_event!(); - let (root, output) = claude_stop_response_for_event(runtime, &event, started).await; - if !super::write_hook_output( - root.as_deref(), - tracedecay_hooks::HookHostV1::ClaudeCode, - &event, - &output, - ) - .await - { - return 1; - } - 0 + claude_guidance_hook(runtime, "Stop").await } -/// Returns the identity-resolved root alongside the response so the handler -/// does not repeat the registry-probing resolution for output delivery. -async fn claude_stop_response_for_event( - runtime: &HookRuntimeV1, - event: &str, - started: Instant, -) -> (Option, String) { - let parsed = serde_json::from_str::(event).unwrap_or(Value::Null); +/// SessionStart and Stop share one guidance envelope. The project root is +/// resolved once and reused for both dispatch and stdout delivery. +async fn claude_guidance_hook(runtime: &HookRuntimeV1, hook_name: &'static str) -> i32 { + let started = Instant::now(); + let event = read_hook_event!(); + let parsed = serde_json::from_str::(&event).unwrap_or(Value::Null); let root = event_project_root_with_identity(runtime, &parsed).await; let hook_telemetry = record_hook_invoked_parsed( runtime, root.as_deref(), HintAgent::Claude, - "Stop", - event, + hook_name, + &event, &parsed, ); let output = super::dispatch::dispatch_for_scope( runtime, tracedecay_hooks::HookHostV1::ClaudeCode, - event, + &event, root.as_deref(), Some(&hook_telemetry), started, @@ -284,7 +219,17 @@ async fn claude_stop_response_for_event( .flatten() .map_or_else( || serde_json::json!({}).to_string(), - |guidance| additional_context_json("Stop", &guidance), + |guidance| additional_context_json(hook_name, &guidance), ); - (root, output) + if !super::write_hook_output( + root.as_deref(), + tracedecay_hooks::HookHostV1::ClaudeCode, + &event, + &output, + ) + .await + { + return 1; + } + 0 } diff --git a/crates/tracedecay-agent-hosts/src/hooks/codex.rs b/crates/tracedecay-agent-hosts/src/hooks/codex.rs index 23877aeb0f..2c6a9c7411 100644 --- a/crates/tracedecay-agent-hosts/src/hooks/codex.rs +++ b/crates/tracedecay-agent-hosts/src/hooks/codex.rs @@ -122,7 +122,8 @@ pub async fn hook_codex_user_prompt_submit(runtime: &HookRuntimeV1) -> i32 { // Keep recall current, but wait for the native Stop receipt before // reflection so one completed turn schedules one review rather than a // prompt-only review followed immediately by a final-turn review. - let _ = ingest_user_codex_session(runtime, session_id, Some(&hook_telemetry)).await; + let _ = + super::ingest_user_session(runtime, "Codex", session_id, Some(&hook_telemetry)).await; } let context = Box::pin(codex_user_prompt_submit_context_with_root( &parsed, @@ -530,14 +531,6 @@ async fn codex_post_compact( } } -async fn ingest_user_codex_session( - runtime: &HookRuntimeV1, - session_id: Option, - telemetry: Option<&super::analytics::HookTimingSpan>, -) -> bool { - super::ingest_user_session(runtime, "Codex", session_id, telemetry).await -} - fn deduped_codex_hint(parsed: &Value, hint_id: &str, hint: ToolHint) -> Option { deduped_project_hint_with_id( event_project_root(parsed).as_deref(), diff --git a/crates/tracedecay-agent-hosts/src/tool_name.rs b/crates/tracedecay-agent-hosts/src/tool_name.rs index eb12bdb4a0..a95abd1026 100644 --- a/crates/tracedecay-agent-hosts/src/tool_name.rs +++ b/crates/tracedecay-agent-hosts/src/tool_name.rs @@ -28,20 +28,11 @@ pub const LEGACY_TOOL_PREFIX: &str = "mcp__tracedecay__"; /// Single-underscore namespace used by hosts that flatten the MCP separator. pub const FLAT_TOOL_PREFIX: &str = "mcp_tracedecay_"; -/// Every namespace a tracedecay tool call can arrive under, longest first so a -/// prefix that contains another is stripped whole. +/// Every namespace a tracedecay tool call can arrive under, longest first. +/// Automation classification restates these literals and must stay aligned. pub const ALL_TOOL_PREFIXES: [&str; 4] = [ PRIOR_PLUGIN_TOOL_PREFIX, PLUGIN_TOOL_PREFIX, LEGACY_TOOL_PREFIX, FLAT_TOOL_PREFIX, ]; - -/// Strips the host MCP namespace from a tracedecay tool name, leaving the bare -/// tool name. Names in no known namespace are returned unchanged. -pub fn strip_tool_prefix(name: &str) -> &str { - ALL_TOOL_PREFIXES - .iter() - .find_map(|prefix| name.strip_prefix(prefix)) - .unwrap_or(name) -} From 5b95fa02ba3651114964d21993fab32f3cd87b4a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:13:13 +0000 Subject: [PATCH 108/182] simplify(pass-4/5): share public catalog route check Co-authored-by: Zack Jackson --- .../src/application_surface.rs | 23 +++++++++++++++++- .../src/application_surface/handoff.rs | 18 +++----------- .../application_surface/multi_root_http.rs | 18 +++----------- .../src/application_surface/retained.rs | 18 +++++--------- .../src/application_surface/work.rs | 24 +++++-------------- .../src/application_surface/workflow.rs | 18 +++----------- 6 files changed, 43 insertions(+), 76 deletions(-) diff --git a/crates/tracedecay-daemon-service/src/application_surface.rs b/crates/tracedecay-daemon-service/src/application_surface.rs index c739179d14..5f2d9327a5 100644 --- a/crates/tracedecay-daemon-service/src/application_surface.rs +++ b/crates/tracedecay-daemon-service/src/application_surface.rs @@ -36,7 +36,8 @@ use tracedecay_daemon_protocol::{ }; use tracedecay_domain::{ProjectId, ScopeOutcome, ScopePartialReasonV1, ScopeUnavailableReasonV1}; use tracedecay_tool_catalog::{ - ApplicationSurfaceOperation, BindingSurface, CapabilityId, CatalogSnapshotV1, UseCaseId, + ApplicationSurfaceOperation, BindingSurface, CapabilityId, CatalogSnapshotV1, + ExecutableBindingRegistryV1, OperationId, RouteExposureV1, UseCaseId, }; mod catalog; @@ -77,6 +78,26 @@ use request_control::application_http_context; pub use workflow::invoke_workflow_operation; use workflow::router_with_executor as workflow_application_router_with_executor; +pub(super) fn require_public_catalog_route( + registry: &ExecutableBindingRegistryV1, + operation_id: &OperationId, + expected_route: &str, +) -> Result<(), ApplicationSurfaceAdapterError> { + let Some(binding) = registry + .get(operation_id) + .and_then(|availability| availability.binding()) + else { + return Err(ApplicationSurfaceAdapterError::UnknownOrNotAuthorized); + }; + let RouteExposureV1::Public { route_path, .. } = binding.exposure() else { + return Err(ApplicationSurfaceAdapterError::UnknownOrNotAuthorized); + }; + if route_path != expected_route { + return Err(ApplicationSurfaceAdapterError::UnknownOrNotAuthorized); + } + Ok(()) +} + const DEFAULT_DEADLINE_MICROS: i64 = 30_000_000; const APPLICATION_PROTOCOL_REVISION: u32 = 1; const HTTP_DEADLINE_HEADER: &str = "x-tracedecay-deadline-micros"; diff --git a/crates/tracedecay-daemon-service/src/application_surface/handoff.rs b/crates/tracedecay-daemon-service/src/application_surface/handoff.rs index 3ae9364f30..e8e450ca5a 100644 --- a/crates/tracedecay-daemon-service/src/application_surface/handoff.rs +++ b/crates/tracedecay-daemon-service/src/application_surface/handoff.rs @@ -1,5 +1,7 @@ use std::sync::Arc; +use super::registered_http::invoke_registered_http; +use super::require_public_catalog_route; use axum::response::Response; use tracedecay_api::HandoffOperation; use tracedecay_contracts::{ @@ -7,9 +9,6 @@ use tracedecay_contracts::{ ListTaskHandoffsResultV1, OpenInvestigationHandoffRequestV1, OpenInvestigationHandoffResultV1, OpenTaskHandoffRequestV1, OpenTaskHandoffResultV1, }; -use tracedecay_tool_catalog::RouteExposureV1; - -use super::registered_http::invoke_registered_http; use tracedecay_daemon_protocol::ApplicationSurfaceAdapterError; use tracedecay_daemon_protocol::DaemonInvocationExecutor; use tracedecay_daemon_protocol::{HandoffApplicationInvocationV1, HandoffApplicationOutcomeV1}; @@ -30,18 +29,7 @@ pub(super) fn validate_catalog_bindings() -> Result<(), ApplicationSurfaceAdapte let operation_id = tracedecay_tool_catalog::OperationId::new(operation.operation_id_str().to_owned()) .map_err(ApplicationSurfaceAdapterError::Identifier)?; - let Some(binding) = registry - .get(&operation_id) - .and_then(|availability| availability.binding()) - else { - return Err(ApplicationSurfaceAdapterError::UnknownOrNotAuthorized); - }; - let RouteExposureV1::Public { route_path, .. } = binding.exposure() else { - return Err(ApplicationSurfaceAdapterError::UnknownOrNotAuthorized); - }; - if route_path != operation.application_route_path() { - return Err(ApplicationSurfaceAdapterError::UnknownOrNotAuthorized); - } + require_public_catalog_route(®istry, &operation_id, operation.application_route_path())?; } Ok(()) } diff --git a/crates/tracedecay-daemon-service/src/application_surface/multi_root_http.rs b/crates/tracedecay-daemon-service/src/application_surface/multi_root_http.rs index 299156ab5a..a30673e9df 100644 --- a/crates/tracedecay-daemon-service/src/application_surface/multi_root_http.rs +++ b/crates/tracedecay-daemon-service/src/application_surface/multi_root_http.rs @@ -10,6 +10,8 @@ use std::sync::Arc; +use super::registered_http::{RegisteredHttpOperation, invoke_registered_http}; +use super::require_public_catalog_route; use axum::response::Response; use tracedecay_api::MultiRootHttpOperation; use tracedecay_contracts::multi_root::{ @@ -20,9 +22,6 @@ use tracedecay_contracts::{ MultiRootQueryPageV1, MultiRootScopeSetCasRequestV1, MultiRootScopeSetCasResultV1, MultiRootScopeSetReadRequestV1, RequestId, RetryDirective, SafeDiagnostic, }; -use tracedecay_tool_catalog::RouteExposureV1; - -use super::registered_http::{RegisteredHttpOperation, invoke_registered_http}; use tracedecay_daemon_protocol::ApplicationSurfaceAdapterError; use tracedecay_daemon_protocol::DaemonInvocationExecutor; use tracedecay_daemon_protocol::{DaemonInvocationOutcome, DaemonInvocationRequest}; @@ -45,18 +44,7 @@ pub(super) fn validate_catalog_bindings() -> Result<(), ApplicationSurfaceAdapte let operation_id = tracedecay_tool_catalog::OperationId::new(operation.operation_id().to_owned()) .map_err(ApplicationSurfaceAdapterError::Identifier)?; - let Some(binding) = registry - .get(&operation_id) - .and_then(|availability| availability.binding()) - else { - return Err(ApplicationSurfaceAdapterError::UnknownOrNotAuthorized); - }; - let RouteExposureV1::Public { route_path, .. } = binding.exposure() else { - return Err(ApplicationSurfaceAdapterError::UnknownOrNotAuthorized); - }; - if route_path != operation.application_route_path() { - return Err(ApplicationSurfaceAdapterError::UnknownOrNotAuthorized); - } + require_public_catalog_route(®istry, &operation_id, operation.application_route_path())?; } Ok(()) } diff --git a/crates/tracedecay-daemon-service/src/application_surface/retained.rs b/crates/tracedecay-daemon-service/src/application_surface/retained.rs index 1ece683e02..4566d3a7b1 100644 --- a/crates/tracedecay-daemon-service/src/application_surface/retained.rs +++ b/crates/tracedecay-daemon-service/src/application_surface/retained.rs @@ -17,6 +17,7 @@ use tracedecay_contracts::retained_surfaces::{ use tracedecay_tool_catalog::RouteExposureV1; use super::registered_http::{RegisteredHttpOperation, invoke_registered_http}; +use super::require_public_catalog_route; use tracedecay_daemon_protocol::ApplicationSurfaceAdapterError; use tracedecay_daemon_protocol::DaemonInvocationExecutor; use tracedecay_daemon_protocol::{DaemonInvocationOutcome, DaemonInvocationRequest}; @@ -38,18 +39,11 @@ fn validate_catalog_bindings() -> Result<(), ApplicationSurfaceAdapterError> { tracedecay_api::retained_operation_id(operation), ) .map_err(ApplicationSurfaceAdapterError::Identifier)?; - let Some(binding) = registry - .get(&operation_id) - .and_then(|availability| availability.binding()) - else { - return Err(ApplicationSurfaceAdapterError::UnknownOrNotAuthorized); - }; - let RouteExposureV1::Public { route_path, .. } = binding.exposure() else { - return Err(ApplicationSurfaceAdapterError::UnknownOrNotAuthorized); - }; - if route_path != &tracedecay_api::retained_application_route_path(operation) { - return Err(ApplicationSurfaceAdapterError::UnknownOrNotAuthorized); - } + require_public_catalog_route( + ®istry, + &operation_id, + &tracedecay_api::retained_application_route_path(operation), + )?; } Ok(()) } diff --git a/crates/tracedecay-daemon-service/src/application_surface/work.rs b/crates/tracedecay-daemon-service/src/application_surface/work.rs index 627b2fba5b..e3b3aa3754 100644 --- a/crates/tracedecay-daemon-service/src/application_surface/work.rs +++ b/crates/tracedecay-daemon-service/src/application_surface/work.rs @@ -6,6 +6,8 @@ use std::sync::Arc; +use super::registered_http::invoke_registered_http; +use super::require_public_catalog_route; use axum::response::Response; use tracedecay_api::{WorkHttpRequest, WorkOperation}; use tracedecay_contracts::{ @@ -26,16 +28,13 @@ use tracedecay_contracts::{ WorkProposalComparisonV1, WorkRunControlReadingV1, WorkRunControlRequestV1, WorkSynthesisAttemptV1, WorkTopologyViewRequestV1, }; +use tracedecay_daemon_protocol::ApplicationSurfaceAdapterError; +use tracedecay_daemon_protocol::DaemonInvocationExecutor; +use tracedecay_daemon_protocol::{WorkApplicationInvocationV1, WorkApplicationOutcomeV1}; use tracedecay_domain::{ WorkAttemptV1, WorkDuplicateAdjudicationCommandV1, WorkPlacementPreflightV1, WorkPlacementV1, WorkRunControlV1, }; -use tracedecay_tool_catalog::RouteExposureV1; - -use super::registered_http::invoke_registered_http; -use tracedecay_daemon_protocol::ApplicationSurfaceAdapterError; -use tracedecay_daemon_protocol::DaemonInvocationExecutor; -use tracedecay_daemon_protocol::{WorkApplicationInvocationV1, WorkApplicationOutcomeV1}; pub(super) fn router_with_executor( executor: Arc, @@ -63,18 +62,7 @@ pub(crate) fn validate_catalog_bindings() -> Result<(), ApplicationSurfaceAdapte for operation in WorkOperation::ALL { let operation_id = tracedecay_tool_catalog::OperationId::new(operation.operation_id()) .map_err(ApplicationSurfaceAdapterError::Identifier)?; - let Some(binding) = registry - .get(&operation_id) - .and_then(|availability| availability.binding()) - else { - return Err(ApplicationSurfaceAdapterError::UnknownOrNotAuthorized); - }; - let RouteExposureV1::Public { route_path, .. } = binding.exposure() else { - return Err(ApplicationSurfaceAdapterError::UnknownOrNotAuthorized); - }; - if route_path != operation.application_route_path() { - return Err(ApplicationSurfaceAdapterError::UnknownOrNotAuthorized); - } + require_public_catalog_route(registry, &operation_id, operation.application_route_path())?; } Ok(()) } diff --git a/crates/tracedecay-daemon-service/src/application_surface/workflow.rs b/crates/tracedecay-daemon-service/src/application_surface/workflow.rs index 93e45d049f..fe796483d5 100644 --- a/crates/tracedecay-daemon-service/src/application_surface/workflow.rs +++ b/crates/tracedecay-daemon-service/src/application_surface/workflow.rs @@ -2,6 +2,8 @@ use std::sync::Arc; +use super::registered_http::invoke_registered_http; +use super::require_public_catalog_route; use axum::response::Response; use tracedecay_api::WorkflowOperation; use tracedecay_contracts::{ @@ -12,9 +14,6 @@ use tracedecay_contracts::{ WorkflowDefinitionValidateRequest, WorkflowRunCancelRequest, WorkflowRunGetRequest, WorkflowRunPauseRequest, WorkflowRunResumeRequest, WorkflowRunStartRequest, }; -use tracedecay_tool_catalog::RouteExposureV1; - -use super::registered_http::invoke_registered_http; use tracedecay_daemon_protocol::ApplicationSurfaceAdapterError; use tracedecay_daemon_protocol::DaemonInvocationExecutor; use tracedecay_daemon_protocol::{WorkflowApplicationInvocation, WorkflowApplicationOutcome}; @@ -35,18 +34,7 @@ pub(super) fn validate_catalog_bindings() -> Result<(), ApplicationSurfaceAdapte let operation_id = tracedecay_tool_catalog::OperationId::new(operation.operation_id_str().to_owned()) .map_err(ApplicationSurfaceAdapterError::Identifier)?; - let Some(binding) = registry - .get(&operation_id) - .and_then(|availability| availability.binding()) - else { - return Err(ApplicationSurfaceAdapterError::UnknownOrNotAuthorized); - }; - let RouteExposureV1::Public { route_path, .. } = binding.exposure() else { - return Err(ApplicationSurfaceAdapterError::UnknownOrNotAuthorized); - }; - if route_path != operation.application_route_path() { - return Err(ApplicationSurfaceAdapterError::UnknownOrNotAuthorized); - } + require_public_catalog_route(registry, &operation_id, operation.application_route_path())?; } Ok(()) } From 3bd0b4d9f72135817a4eeb273d162ac9e3455d14 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:13:14 +0000 Subject: [PATCH 109/182] simplify(pass-5/5): share unix socket identity match Co-authored-by: Zack Jackson --- .../src/connection.rs | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/crates/tracedecay-daemon-identity/src/connection.rs b/crates/tracedecay-daemon-identity/src/connection.rs index 5af26b45a6..122208fe44 100644 --- a/crates/tracedecay-daemon-identity/src/connection.rs +++ b/crates/tracedecay-daemon-identity/src/connection.rs @@ -68,6 +68,23 @@ impl DaemonLivenessProbe for AuthorityLivenessProbe { } } +fn connection_from_record(record: authority::DaemonAuthorityRecord) -> ResolvedDaemonConnection { + ResolvedDaemonConnection { + endpoint: record.endpoint.clone(), + auth_token: Some(record.auth_token.clone()), + authority_record: Some(record), + } +} + +#[cfg(unix)] +fn unix_endpoint_matches_socket(endpoint: &DaemonEndpoint, socket_path: &Path) -> bool { + let DaemonEndpoint::Unix(authority_path) = endpoint else { + return false; + }; + authority::canonical_identity_path(authority_path).ok() + == authority::canonical_identity_path(socket_path).ok() +} + fn ensure_record_current( expected: &authority::DaemonAuthorityRecord, request_label: &str, @@ -113,33 +130,24 @@ pub fn current_daemon_connection() -> Result { "TraceDecay daemon authority record is not available. Start or restart the daemon." .to_string(), })?; - Ok(ResolvedDaemonConnection { - endpoint: record.endpoint.clone(), - auth_token: Some(record.auth_token.clone()), - authority_record: Some(record), - }) + Ok(connection_from_record(record)) } #[cfg(unix)] pub fn connection_for_socket_path(socket_path: &Path) -> ResolvedDaemonConnection { if let Ok(connection) = current_daemon_connection() - && let DaemonEndpoint::Unix(authority_path) = &connection.endpoint - && authority::canonical_identity_path(authority_path).ok() - == authority::canonical_identity_path(socket_path).ok() + && connection + .authority_record + .as_ref() + .is_some_and(|record| unix_endpoint_matches_socket(&record.endpoint, socket_path)) { return connection; } if let Some(profile_root) = socket_path.parent() && let Ok(Some(record)) = authority::current_record(profile_root) - && let DaemonEndpoint::Unix(authority_path) = &record.endpoint - && authority::canonical_identity_path(authority_path).ok() - == authority::canonical_identity_path(socket_path).ok() + && unix_endpoint_matches_socket(&record.endpoint, socket_path) { - return ResolvedDaemonConnection { - endpoint: record.endpoint.clone(), - auth_token: Some(record.auth_token.clone()), - authority_record: Some(record), - }; + return connection_from_record(record); } // Explicit paths are retained for test harnesses and legacy one-shot // callers without a discoverable authority record. Default production From 525030342e14671ce338d4c722008bb872ab5c34 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:15:42 +0000 Subject: [PATCH 110/182] simplify(pass-5/5): share ledger publish, drop dead user run Immediate-versus-deferred ledger append now has one helper. The projectless user run it no longer reached is gone, and simplify is a legal commit type for these passes. Co-authored-by: Zack Jackson --- commitlint.config.cjs | 1 + .../src/automation/host_io.rs | 39 - .../src/automation/jobs.rs | 13 +- .../src/automation/lifecycle.rs | 27 +- .../src/automation/memory_curator.rs | 89 +-- .../src/automation/runner.rs | 37 +- .../src/automation/runner/evidence.rs | 3 +- .../src/automation/runner/retrieval.rs | 63 +- .../automation/runner/session_reflector.rs | 34 +- .../src/automation/runner/skill_writer.rs | 86 +-- .../src/automation/runner/tests/early_gate.rs | 7 +- .../runner/user_evidence_preflight.rs | 118 --- .../src/automation/runner/user_scope_tests.rs | 706 ------------------ .../user_scope_graph_runtime.rs | 109 --- .../src/automation/session_reflector.rs | 1 - .../src/automation/skill_writer.rs | 1 - crates/tracedecay-capture/src/cursor.rs | 8 +- 17 files changed, 57 insertions(+), 1285 deletions(-) delete mode 100644 crates/tracedecay-automation-runtime/src/automation/runner/user_evidence_preflight.rs delete mode 100644 crates/tracedecay-automation-runtime/src/automation/runner/user_scope_tests.rs delete mode 100644 crates/tracedecay-automation-runtime/src/automation/runner/user_scope_tests/user_scope_graph_runtime.rs diff --git a/commitlint.config.cjs b/commitlint.config.cjs index 18b34ae514..3f19d02c48 100644 --- a/commitlint.config.cjs +++ b/commitlint.config.cjs @@ -8,6 +8,7 @@ const allowedTypes = [ "perf", "refactor", "revert", + "simplify", "style", "test", ]; diff --git a/crates/tracedecay-automation-runtime/src/automation/host_io.rs b/crates/tracedecay-automation-runtime/src/automation/host_io.rs index cabb9f411d..9006aae9e4 100644 --- a/crates/tracedecay-automation-runtime/src/automation/host_io.rs +++ b/crates/tracedecay-automation-runtime/src/automation/host_io.rs @@ -121,42 +121,3 @@ pub fn home_dir() -> Option { pub fn uses_default_user_profile(home: &Path, profile_root: &Path) -> bool { profile_root == home.join(".tracedecay") } - -/// A bundle whose file writes land on disk plainly and whose export sweeps -/// touch no agent host, for tests that exercise automation without a host -/// installer. -#[cfg(test)] -pub(crate) fn plain_file_host_io() -> HostIo { - fn export_to_agents(_: &Path, _: &Path) -> Vec { - Vec::new() - } - - fn export_to_agent_hosts(_: &Path, _: &Path, _: &Path) -> Vec { - Vec::new() - } - - fn write_text(path: &Path, contents: &str, _: Option<&Path>) -> Result<()> { - Ok(std::fs::write(path, contents)?) - } - - fn write_json(path: &Path, value: &Value, _: Option<&Path>) -> Result<()> { - Ok(std::fs::write(path, serde_json::to_vec_pretty(value)?)?) - } - - fn remove_host_file(path: &Path) -> std::io::Result<()> { - std::fs::remove_file(path) - } - - fn codex_agent_files() -> &'static [PluginFile] { - &[] - } - - HostIo { - export_to_agents, - export_to_agent_hosts, - write_text, - write_json, - remove_host_file, - codex_agent_files, - } -} diff --git a/crates/tracedecay-automation-runtime/src/automation/jobs.rs b/crates/tracedecay-automation-runtime/src/automation/jobs.rs index a6e00c764c..ee3d208cb7 100644 --- a/crates/tracedecay-automation-runtime/src/automation/jobs.rs +++ b/crates/tracedecay-automation-runtime/src/automation/jobs.rs @@ -16,13 +16,13 @@ use super::job_error; use super::job_webhook; use super::lifecycle::{ AutomationRunLedgerPublication, AutomationRunSettlementGuard, RetainedAutomationRun, - generated_run_id, + generated_run_id, publish_ledger_record, }; use super::managed_skills::{ManagedSkillState, load_managed_skill}; use super::run_ledger::{ AutomationRunLedgerRecord, AutomationRunLedgerTaskSummary, AutomationRunStatus, - AutomationTrigger, append_or_reuse_scheduler_diagnostic, append_run_record, - latest_record_by_canonical_completion, load_run_ledger_task_summary, + AutomationTrigger, append_or_reuse_scheduler_diagnostic, latest_record_by_canonical_completion, + load_run_ledger_task_summary, }; use super::scheduler::{ AutomationSchedule, AutomationTaskLock, cron_is_due, elapsed_secs, parse_schedule, @@ -923,12 +923,7 @@ impl JobRunContext<'_> { } async fn publish_terminal(&self, record: &AutomationRunLedgerRecord) -> Result<()> { - match self.ledger_publication { - AutomationRunLedgerPublication::Immediate => { - append_run_record(self.dashboard_root, record).await - } - AutomationRunLedgerPublication::DeferredUntilApplicationSettlement => Ok(()), - } + publish_ledger_record(self.ledger_publication, self.dashboard_root, record).await } } diff --git a/crates/tracedecay-automation-runtime/src/automation/lifecycle.rs b/crates/tracedecay-automation-runtime/src/automation/lifecycle.rs index 3248c30af4..262a419e8a 100644 --- a/crates/tracedecay-automation-runtime/src/automation/lifecycle.rs +++ b/crates/tracedecay-automation-runtime/src/automation/lifecycle.rs @@ -215,6 +215,19 @@ pub(crate) enum AutomationRunLedgerPublication { DeferredUntilApplicationSettlement, } +pub(crate) async fn publish_ledger_record( + publication: AutomationRunLedgerPublication, + dashboard_root: &Path, + record: &AutomationRunLedgerRecord, +) -> Result<()> { + match publication { + AutomationRunLedgerPublication::Immediate => { + append_run_record(dashboard_root, record).await + } + AutomationRunLedgerPublication::DeferredUntilApplicationSettlement => Ok(()), + } +} + impl From for AutomationRunError { fn from(error: TraceDecayError) -> Self { Self::Runtime(error) @@ -453,12 +466,7 @@ impl<'a> AgentTaskRunContext<'a> { } async fn publish_terminal_record(&self, record: &AutomationRunLedgerRecord) -> Result<()> { - match self.ledger_publication { - AutomationRunLedgerPublication::Immediate => { - append_run_record(&self.dashboard_root, record).await - } - AutomationRunLedgerPublication::DeferredUntilApplicationSettlement => Ok(()), - } + publish_ledger_record(self.ledger_publication, &self.dashboard_root, record).await } } @@ -850,12 +858,7 @@ impl<'a> AgentRunFinalizer<'a> { } async fn publish_terminal_record(&self, record: &AutomationRunLedgerRecord) -> Result<()> { - match self.ledger_publication { - AutomationRunLedgerPublication::Immediate => { - append_run_record(self.dashboard_root, record).await - } - AutomationRunLedgerPublication::DeferredUntilApplicationSettlement => Ok(()), - } + publish_ledger_record(self.ledger_publication, self.dashboard_root, record).await } #[must_use] diff --git a/crates/tracedecay-automation-runtime/src/automation/memory_curator.rs b/crates/tracedecay-automation-runtime/src/automation/memory_curator.rs index cdd393a450..31ecc29e17 100644 --- a/crates/tracedecay-automation-runtime/src/automation/memory_curator.rs +++ b/crates/tracedecay-automation-runtime/src/automation/memory_curator.rs @@ -1,7 +1,6 @@ use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; use std::collections::BTreeMap; -use std::sync::Arc; use tracedecay_domain::configuration::ConfigurationRevisionId; use tracedecay_domain::{ ActorId, Confidence, FactEventId, FactId, FactOwnerV1, ManifestDigest, canonical_sha256, @@ -20,7 +19,7 @@ use super::lifecycle::{ failed_backend_fallback_report, }; use super::run_ledger::{AutomationRunLedgerRecord, AutomationTrigger}; -use crate::ports::project_runtime::{AutomationProjectContext, ProfileRuntime}; +use crate::ports::project_runtime::AutomationProjectContext; use tracedecay_domain::errors::{Result, TraceDecayError}; use tracedecay_global_db::RegisteredGlobalDbLeaseV1; use tracedecay_policy::{ @@ -87,7 +86,7 @@ pub async fn run_memory_curator_with_backend( ) -> AutomationRunResult { let sessions_db = super::runner::project_automation_sessions(cg); run_memory_curator_for_store_with_publication( - MemoryCuratorStore::Project { cg, sessions_db }, + MemoryCuratorStore { cg, sessions_db }, config, configuration_revision_id, backend, @@ -114,7 +113,7 @@ pub async fn run_memory_curator_with_backend_for_retained_settlement( let settlement_guard = AutomationRunSettlementGuard::new(); let sessions_db = super::runner::project_automation_sessions(cg); let result = run_memory_curator_for_store_with_publication( - MemoryCuratorStore::Project { cg, sessions_db }, + MemoryCuratorStore { cg, sessions_db }, config, configuration_revision_id, backend, @@ -129,71 +128,24 @@ pub async fn run_memory_curator_with_backend_for_retained_settlement( RetainedAutomationRun::new(result, settlement_guard) } -/// Runs autonomous curation against profile-level user memory. -pub(crate) async fn run_user_memory_curator_with_backend( - profile_root: &std::path::Path, - session_registry: Arc, - config: &AutomationConfig, - configuration_revision_id: &ConfigurationRevisionId, - backend: &dyn AgentTaskBackend, - options: MemoryCuratorAutomationOptions, - run_control: &AutomationRunControl, -) -> AutomationRunResult { - let sessions_db = session_registry.profile_sessions().await?; - run_memory_curator_for_store_with_publication( - MemoryCuratorStore::User { - profile_root, - runtime: session_registry.as_ref(), - sessions_db, - }, - config, - configuration_revision_id, - backend, - options, - run_control, - AutomationRunPublication { - ledger: AutomationRunLedgerPublication::Immediate, - settlement_guard: None, - }, - ) - .await -} - -enum MemoryCuratorStore<'a> { - Project { - cg: &'a AutomationProjectContext, - sessions_db: RegisteredGlobalDbLeaseV1, - }, - User { - profile_root: &'a std::path::Path, - runtime: &'a dyn ProfileRuntime, - sessions_db: RegisteredGlobalDbLeaseV1, - }, +struct MemoryCuratorStore<'a> { + cg: &'a AutomationProjectContext, + sessions_db: RegisteredGlobalDbLeaseV1, } impl MemoryCuratorStore<'_> { fn dashboard_root(&self) -> std::path::PathBuf { - match self { - Self::Project { cg, .. } => cg.dashboard_root.clone(), - Self::User { profile_root, .. } => super::runner::user_automation_root(profile_root), - } + self.cg.dashboard_root.clone() } fn sessions_db(&self) -> RegisteredGlobalDbLeaseV1 { - match self { - Self::Project { sessions_db, .. } | Self::User { sessions_db, .. } => { - sessions_db.clone() - } - } + self.sessions_db.clone() } fn owner(&self) -> Result { - match self { - Self::Project { cg, .. } => Ok(FactOwnerV1::Project { - project_id: cg.project_id.clone(), - }), - Self::User { .. } => Ok(FactOwnerV1::Profile), - } + Ok(FactOwnerV1::Project { + project_id: self.cg.project_id.clone(), + }) } fn curation_authority( @@ -201,27 +153,20 @@ impl MemoryCuratorStore<'_> { configuration_revision_id: &ConfigurationRevisionId, ) -> Result { let actor_id = ActorId::new("automation:memory-curator").map_err(memory_contract_error)?; - let (project_id, profile_id) = match self { - Self::Project { cg, .. } => (Some(cg.project_id.clone()), cg.profile_id.clone()), - Self::User { runtime, .. } => (None, runtime.profile_id().clone()), - }; Ok(CurationApplyAuthorityV1 { actor_id, - project_id, - profile_id, + project_id: Some(self.cg.project_id.clone()), + profile_id: self.cg.profile_id.clone(), configuration_revision_id: configuration_revision_id.clone(), }) } - async fn open_memory_database(&self) -> Result { - match self { - Self::Project { cg, .. } => Ok(cg.project_memory_database.clone()), - Self::User { runtime, .. } => runtime.open_user_memory_db().await, - } + fn open_memory_database(&self) -> tracedecay_runtime_core::db::Database { + self.cg.project_memory_database.clone() } } -// The single funnel every curator entry point (project, user, retained +// The single funnel every curator entry point (project and retained // settlement) flows through: one static run-lifetime span in the futures lane // so suspension and cancellation of long runs stay visible. #[hotpath::measure(future = true, label = "automation.run.memory_curator")] @@ -277,7 +222,7 @@ async fn run_memory_curator_for_store_with_publication( } let owner = store.owner()?; - let database = store.open_memory_database().await?; + let database = store.open_memory_database(); let memory = MemoryApplication::new(owner.clone(), DatabaseFactStore::new(&database)).map_err( |error| TraceDecayError::Config { message: format!("initialize memory curator authority: {error}"), diff --git a/crates/tracedecay-automation-runtime/src/automation/runner.rs b/crates/tracedecay-automation-runtime/src/automation/runner.rs index 8c2cd953f5..3a34654a92 100644 --- a/crates/tracedecay-automation-runtime/src/automation/runner.rs +++ b/crates/tracedecay-automation-runtime/src/automation/runner.rs @@ -1,5 +1,4 @@ use std::path::{Path, PathBuf}; -use std::sync::Arc; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; @@ -26,7 +25,7 @@ use super::skill_writer::{ activation_policy as skill_writer_activation_policy, validate_and_apply_skill_proposals, validate_skill_proposals, }; -use crate::ports::project_runtime::{AutomationProjectContext, ProfileRuntime}; +use crate::ports::project_runtime::AutomationProjectContext; use crate::ports::session_store::AutomationSessionStore; use tracedecay_domain::errors::{Result, TraceDecayError}; use tracedecay_global_db::{RegisteredGlobalDb, RegisteredGlobalDbLeaseV1}; @@ -40,10 +39,6 @@ mod evidence; mod retrieval; mod session_reflector; mod skill_writer; -mod user_evidence_preflight; -#[cfg(test)] -mod user_scope_tests; - use curation::{combined_review_output, evaluate_skill_curation}; use evidence::{ SessionReflectorEvidenceBundle, SessionReflectorEvidenceOutcome, SkillWriterEvidenceBundle, @@ -59,12 +54,16 @@ use skill_writer::{ ProposedSkillOutput, SkillWriterFinalization, build_skill_writer_prompt, finalize_skill_writer_success, }; -pub(crate) use skill_writer::run_user_skill_writer_with_backend_and_retrieval; pub use super::lifecycle::{ AutomationRunSettlementGuard, RetainedAutomationRun, RetainedAutomationSettlementDisposition, ReusedSchedulerSkip, }; +pub use super::memory_curator::{ + CURATION_DEFAULT_FACT_REVIEW_LIMIT, CURATION_DEFAULT_MIN_CONFIDENCE, + MemoryCuratorAutomationOptions, MemoryCuratorAutomationRun, run_memory_curator_with_backend, + run_memory_curator_with_backend_for_retained_settlement, +}; pub use evidence::{AutomationTemporalEvidence, AutomationTemporalEvidenceItem}; pub use retrieval::registered_project_automation_retrieval; pub use retrieval::{ @@ -82,14 +81,6 @@ pub use skill_writer::{ run_skill_writer_with_backend_and_retrieval, run_skill_writer_with_backend_and_retrieval_for_retained_settlement, }; -pub(crate) use user_evidence_preflight::run_user_session_reflector_with_backend_and_retrieval; - -pub(crate) use super::memory_curator::run_user_memory_curator_with_backend; -pub use super::memory_curator::{ - CURATION_DEFAULT_FACT_REVIEW_LIMIT, CURATION_DEFAULT_MIN_CONFIDENCE, - MemoryCuratorAutomationOptions, MemoryCuratorAutomationRun, run_memory_curator_with_backend, - run_memory_curator_with_backend_for_retained_settlement, -}; const USER_AUTOMATION_DIR: &str = "user-automation"; @@ -120,22 +111,6 @@ fn project_curation_authority( }) } -fn profile_curation_authority( - runtime: &dyn ProfileRuntime, - actor: &'static str, - configuration_revision_id: &ConfigurationRevisionId, -) -> Result { - let actor_id = ActorId::new(actor).map_err(|error| TraceDecayError::Config { - message: format!("invalid curation actor identity: {error}"), - })?; - Ok(CurationApplyAuthorityV1 { - actor_id, - project_id: None, - profile_id: runtime.profile_id().clone(), - configuration_revision_id: configuration_revision_id.clone(), - }) -} - #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct UserSessionAutomationRun { pub session_reflector: SessionReflectorAutomationRun, diff --git a/crates/tracedecay-automation-runtime/src/automation/runner/evidence.rs b/crates/tracedecay-automation-runtime/src/automation/runner/evidence.rs index 9967afcb52..3b6e3057c2 100644 --- a/crates/tracedecay-automation-runtime/src/automation/runner/evidence.rs +++ b/crates/tracedecay-automation-runtime/src/automation/runner/evidence.rs @@ -5,8 +5,8 @@ use tracedecay_domain::TemporalCoverageCountsV1; use crate::ports::session_evidence::{LcmGrepHit, LcmGrepSort, LcmScope}; use crate::automation::artifacts::sha256_json; -use crate::automation::normalized_non_empty; use crate::automation::managed_skills::list_managed_skills; +use crate::automation::normalized_non_empty; use crate::automation::skill_usage::{ DEFAULT_SKILL_OVERLAP_LIMIT, ingest_project_analytics_events, skill_overlap_candidates, stale_skill_recommendations, summarize_skill_usage, @@ -281,7 +281,6 @@ fn session_reflector_replay_allowed( matches!(scope, LcmScope::All) || session_id.is_some() } - fn compare_evidence_items( left: &AutomationTemporalEvidenceItem, right: &AutomationTemporalEvidenceItem, diff --git a/crates/tracedecay-automation-runtime/src/automation/runner/retrieval.rs b/crates/tracedecay-automation-runtime/src/automation/runner/retrieval.rs index e00704d211..95d1dbc000 100644 --- a/crates/tracedecay-automation-runtime/src/automation/runner/retrieval.rs +++ b/crates/tracedecay-automation-runtime/src/automation/runner/retrieval.rs @@ -6,9 +6,8 @@ use super::evidence::{ use std::collections::{BTreeMap, BTreeSet}; use std::future::Future; -use std::path::Path; #[cfg(test)] -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::pin::Pin; use std::time::Duration; @@ -788,12 +787,6 @@ pub(super) fn unavailable_automation_retrieval( }) } -pub(super) async fn production_user_automation_retrieval( - _profile_root: &Path, -) -> Box { - unavailable_automation_retrieval("session_evidence_retrieval_unavailable") -} - #[cfg(test)] mod authority_tests { use tempfile::tempdir; @@ -883,46 +876,6 @@ mod authority_tests { )); } - #[tokio::test] - async fn convenience_retrieval_does_not_create_a_profile_session_database() { - let directory = tempdir().expect("temporary profile"); - let database_path = directory.path().join("user-sessions.db"); - assert!(!database_path.exists()); - - let retrieval = production_user_automation_retrieval(directory.path()).await; - - assert!(!database_path.exists()); - assert!(matches!( - retrieval - .retrieve( - SessionTemporalQuery::new( - SessionId::new("session.automation.test").expect("session id"), - None, - "test", - None, - TemporalModeV1::Forensic, - RetrievalGrainV1::LogicalMessage, - 1, - DiversityLimits { - per_logical_message: 1, - per_turn: 1, - per_session: 1, - per_source: 1, - per_evidence_role: 1, - }, - ContextBudget { - max_bytes: 1024, - max_tokens: 256, - estimator_version: AUTOMATION_SESSION_ESTIMATOR_VERSION.to_string(), - }, - ) - .expect("bounded query"), - ) - .await, - AutomationTemporalRetrieval::Rejected("session_evidence_retrieval_unavailable") - )); - } - #[tokio::test] async fn project_retrieval_rejects_non_project_scope_without_fallback() { let directory = tempdir().expect("temporary profile"); @@ -1023,18 +976,4 @@ mod authority_tests { "session_evidence_retrieval_unavailable" ); } - - #[tokio::test] - async fn path_only_user_convenience_never_fabricates_empty_hits() { - let directory = tempdir().expect("temporary profile"); - let retrieval = production_user_automation_retrieval(directory.path()).await; - assert_eq!( - typed_reject_reason(retrieval.as_ref()).await, - "session_evidence_retrieval_unavailable" - ); - assert!( - !directory.path().join("user-sessions.db").exists(), - "path-only convenience must not invent a session database" - ); - } } diff --git a/crates/tracedecay-automation-runtime/src/automation/runner/session_reflector.rs b/crates/tracedecay-automation-runtime/src/automation/runner/session_reflector.rs index 1fff51cebe..8e0407fbf6 100644 --- a/crates/tracedecay-automation-runtime/src/automation/runner/session_reflector.rs +++ b/crates/tracedecay-automation-runtime/src/automation/runner/session_reflector.rs @@ -615,42 +615,12 @@ pub(super) async fn finalize_session_reflector_success( - dashboard_root: PathBuf, - sessions_db: RegisteredGlobalDbLeaseV1, - retrieval: &dyn AutomationSessionRetrieval, - memory: &MemoryApplication, - config: &AutomationConfig, - run_control: &AutomationRunControl, - authority: &tracedecay_policy::CurationApplyAuthorityV1, - backend: &dyn AgentTaskBackend, - options: SessionReflectorAutomationOptions, - prebuilt_evidence: Option, -) -> AutomationRunResult { - run_session_reflector_for_store_with_publication( - dashboard_root, - sessions_db, - retrieval, - memory, - config, - run_control, - authority, - backend, - options, - prebuilt_evidence, - AutomationRunLedgerPublication::Immediate, - None, - ) - .await -} - -// The single funnel every reflector entry point (project, user, retained +// The single funnel every reflector entry point (project and retained // settlement) flows through: one static run-lifetime span in the futures lane // so suspension and cancellation of long runs stay visible. #[hotpath::measure(future = true, label = "automation.run.session_reflector")] #[allow(clippy::too_many_arguments)] -async fn run_session_reflector_for_store_with_publication( +pub(super) async fn run_session_reflector_for_store_with_publication( dashboard_root: PathBuf, sessions_db: RegisteredGlobalDbLeaseV1, retrieval: &dyn AutomationSessionRetrieval, diff --git a/crates/tracedecay-automation-runtime/src/automation/runner/skill_writer.rs b/crates/tracedecay-automation-runtime/src/automation/runner/skill_writer.rs index 6d9fc4316d..8a989f1ce6 100644 --- a/crates/tracedecay-automation-runtime/src/automation/runner/skill_writer.rs +++ b/crates/tracedecay-automation-runtime/src/automation/runner/skill_writer.rs @@ -16,7 +16,6 @@ use tracedecay_domain::errors::{Result, TraceDecayError}; use super::curation::unpersisted_rejected_parts; use super::session_reflector::{default_include_recent_sessions, default_recent_sessions_limit}; -use super::user_evidence_preflight::preflight_user_skill_writer_evidence; use super::*; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] @@ -219,66 +218,6 @@ async fn run_skill_writer_with_backend_and_retrieval_publication( .await } -pub(crate) async fn run_user_skill_writer_with_backend_and_retrieval( - host_io: HostIo, - profile_root: &std::path::Path, - session_registry: Arc, - config: &AutomationConfig, - configuration_revision_id: &ConfigurationRevisionId, - backend: &dyn AgentTaskBackend, - retrieval: &dyn AutomationSessionRetrieval, - mut options: SkillWriterAutomationOptions, -) -> AutomationRunResult { - options.profile_root = Some(profile_root.to_path_buf()); - let sessions_db = session_registry.profile_sessions().await?; - let authority = profile_curation_authority( - session_registry.as_ref(), - "automation:skill-writer", - configuration_revision_id, - )?; - let prebuilt_evidence = - match preflight_user_skill_writer_evidence(retrieval, config, options.clone()).await? { - Some(SkillWriterEvidenceOutcome::Ready(bundle)) => Some(bundle), - Some(SkillWriterEvidenceOutcome::Skipped { - reason, - evidence_hash, - }) => { - let run = AgentTaskRunContext::new( - user_automation_root(profile_root), - sessions_db.clone(), - options.run_id.clone(), - "skill_writer", - options.trigger, - config, - AgentTaskKind::SkillWriter, - ); - return Ok(rejected_skill_writer_run( - &run, - config, - reason, - evidence_hash, - )); - } - None => None, - }; - run_skill_writer_for_store( - SkillWriterStoreRuntime { - host_io, - dashboard_root: user_automation_root(profile_root), - sessions_db, - analytics_project_root: None, - analytics_db: None, - authority, - }, - retrieval, - config, - backend, - options, - prebuilt_evidence, - ) - .await -} - pub(super) struct SkillWriterStoreRuntime<'a> { pub(super) host_io: HostIo, pub(super) dashboard_root: PathBuf, @@ -288,30 +227,7 @@ pub(super) struct SkillWriterStoreRuntime<'a> { pub(super) authority: CurationApplyAuthorityV1, } -pub(super) async fn run_skill_writer_for_store( - runtime: SkillWriterStoreRuntime<'_>, - retrieval: &dyn AutomationSessionRetrieval, - config: &AutomationConfig, - backend: &dyn AgentTaskBackend, - options: SkillWriterAutomationOptions, - prebuilt_evidence: Option, -) -> AutomationRunResult { - run_skill_writer_for_store_with_publication( - runtime, - retrieval, - config, - backend, - options, - prebuilt_evidence, - AutomationRunPublication { - ledger: AutomationRunLedgerPublication::Immediate, - settlement_guard: None, - }, - ) - .await -} - -// The single funnel every skill-writer entry point (project, user, retained +// The single funnel every skill-writer entry point (project and retained // settlement) flows through: one static run-lifetime span in the futures lane // so suspension and cancellation of long runs stay visible. #[hotpath::measure(future = true, label = "automation.run.skill_writer")] diff --git a/crates/tracedecay-automation-runtime/src/automation/runner/tests/early_gate.rs b/crates/tracedecay-automation-runtime/src/automation/runner/tests/early_gate.rs index 8b66a024e4..ec7b66ba42 100644 --- a/crates/tracedecay-automation-runtime/src/automation/runner/tests/early_gate.rs +++ b/crates/tracedecay-automation-runtime/src/automation/runner/tests/early_gate.rs @@ -7,12 +7,13 @@ use tracedecay_domain::{ActorId, FactOwnerV1, SessionId}; use tracedecay_global_db::tests::harness::RegisteredGlobalDbTestRuntime; use tracedecay_policy::CurationApplyAuthorityV1; -use super::super::session_reflector::run_session_reflector_for_store; +use super::super::session_reflector::run_session_reflector_for_store_with_publication; use super::super::*; use crate::automation::backend::{AgentTaskBackend, AgentTaskRequest, AgentTaskResponse}; use crate::automation::config::{ AutomationBackend, AutomationHostMode, AutomationTaskConfig, AutomationTaskSet, }; +use crate::automation::lifecycle::AutomationRunLedgerPublication; use crate::automation::run_ledger::AutomationRunStatus; use tracedecay_runtime_core::db::{Database, DatabaseAuthority, TestDatabaseRuntimeMode}; use tracedecay_session_memory::session::SessionTemporalQuery; @@ -124,7 +125,7 @@ async fn scheduled_disabled_session_reflector_reads_no_evidence_and_runs_no_back let control = run_control(); let authority = curation_authority(); - let run = run_session_reflector_for_store( + let run = run_session_reflector_for_store_with_publication( directory.path().join("automation"), sessions.profile_database_arc(), &retrieval, @@ -139,6 +140,8 @@ async fn scheduled_disabled_session_reflector_reads_no_evidence_and_runs_no_back ..SessionReflectorAutomationOptions::default() }, None, + AutomationRunLedgerPublication::Immediate, + None, ) .await .expect("scheduled-disabled reflector skip"); diff --git a/crates/tracedecay-automation-runtime/src/automation/runner/user_evidence_preflight.rs b/crates/tracedecay-automation-runtime/src/automation/runner/user_evidence_preflight.rs deleted file mode 100644 index ea8a58eff5..0000000000 --- a/crates/tracedecay-automation-runtime/src/automation/runner/user_evidence_preflight.rs +++ /dev/null @@ -1,118 +0,0 @@ -use std::sync::Arc; - -use crate::automation::backend::AgentTaskKind; -use crate::automation::config::AutomationConfig; -use crate::automation::lifecycle::{ - AgentTaskRunContext, AutomationRunControl, AutomationRunResult, task_skip_reason, -}; -use crate::ports::project_runtime::ProfileRuntime; -use tracedecay_domain::FactOwnerV1; -use tracedecay_domain::configuration::ConfigurationRevisionId; -use tracedecay_domain::errors::{Result, TraceDecayError}; -use tracedecay_session_memory::fact_store::DatabaseFactStore; -use tracedecay_session_memory::memory::MemoryApplication; - -use super::AutomationTaskIo; -use super::evidence::{ - SessionReflectorEvidenceOutcome, SkillWriterEvidenceOutcome, build_session_reflector_evidence, - build_skill_writer_evidence, -}; -use super::retrieval::AutomationSessionRetrieval; -use super::session_reflector::{ - SessionReflectorAutomationOptions, SessionReflectorAutomationRun, - rejected_session_reflector_run, run_session_reflector_for_store, -}; -use super::skill_writer::SkillWriterAutomationOptions; - -pub(super) async fn preflight_user_session_reflector_evidence( - retrieval: &dyn AutomationSessionRetrieval, - config: &AutomationConfig, - options: &SessionReflectorAutomationOptions, -) -> Result> { - if !options.trigger.is_on_demand() - || task_skip_reason(config, AgentTaskKind::SessionReflector).is_some() - { - return Ok(None); - } - build_session_reflector_evidence(retrieval, options) - .await - .map(Some) -} - -pub(crate) async fn run_user_session_reflector_with_backend_and_retrieval( - profile_root: &std::path::Path, - session_registry: Arc, - config: &AutomationConfig, - run_control: &AutomationRunControl, - configuration_revision_id: &ConfigurationRevisionId, - io: AutomationTaskIo<'_>, - options: SessionReflectorAutomationOptions, -) -> AutomationRunResult { - let AutomationTaskIo { backend, retrieval } = io; - let authority = super::profile_curation_authority( - session_registry.as_ref(), - "automation:session-reflector", - configuration_revision_id, - )?; - let sessions_db = session_registry.profile_sessions().await?; - let prebuilt_evidence = - match preflight_user_session_reflector_evidence(retrieval, config, &options).await? { - Some(SessionReflectorEvidenceOutcome::Ready(bundle)) => Some(bundle), - Some(SessionReflectorEvidenceOutcome::Skipped { - reason, - evidence_hash, - }) => { - let run = AgentTaskRunContext::new( - super::user_automation_root(profile_root), - sessions_db.clone(), - options.run_id.clone(), - "session_reflector", - options.trigger, - config, - AgentTaskKind::SessionReflector, - ); - return Ok(rejected_session_reflector_run( - &run, - config, - reason, - evidence_hash, - )); - } - None => None, - }; - let memory_db = session_registry.open_user_memory_db().await?; - let memory = MemoryApplication::new(FactOwnerV1::Profile, DatabaseFactStore::new(&memory_db)) - .map_err(|error| TraceDecayError::Config { - message: format!( - "could not initialize profile session reflector memory authority: {error}" - ), - })?; - run_session_reflector_for_store( - super::user_automation_root(profile_root), - sessions_db, - retrieval, - &memory, - config, - run_control, - &authority, - backend, - options, - prebuilt_evidence, - ) - .await -} - -pub(super) async fn preflight_user_skill_writer_evidence( - retrieval: &dyn AutomationSessionRetrieval, - config: &AutomationConfig, - options: SkillWriterAutomationOptions, -) -> Result> { - if !options.trigger.is_on_demand() - || task_skip_reason(config, AgentTaskKind::SkillWriter).is_some() - { - return Ok(None); - } - build_skill_writer_evidence(retrieval, None, None, options) - .await - .map(Some) -} diff --git a/crates/tracedecay-automation-runtime/src/automation/runner/user_scope_tests.rs b/crates/tracedecay-automation-runtime/src/automation/runner/user_scope_tests.rs deleted file mode 100644 index 4fa3e6d362..0000000000 --- a/crates/tracedecay-automation-runtime/src/automation/runner/user_scope_tests.rs +++ /dev/null @@ -1,706 +0,0 @@ -use std::path::PathBuf; -use std::sync::Arc; -use std::sync::atomic::{AtomicUsize, Ordering}; - -use serde_json::{Value, json}; -use tempfile::TempDir; -use tracedecay_domain::configuration::{ConfigurationRevisionId, UserProfileId}; -use tracedecay_domain::{Confidence, FactId, FactOwnerV1, SessionId, TemporalCoverageCountsV1}; -use tracedecay_global_db::RegisteredGlobalDbLeaseV1; -use tracedecay_global_db::tests::harness::RegisteredGlobalDbTestRuntime; - -use super::*; -use crate::automation::AutomationRunControl; -use crate::automation::backend::{ - AgentTaskBackend, AgentTaskKind, AgentTaskRequest, AgentTaskResponse, -}; -use crate::automation::config::{ - AutomationBackend, AutomationHostMode, AutomationTaskConfig, AutomationTaskSet, -}; -use crate::automation::run_ledger::AutomationRunStatus; -use crate::ports::project_runtime::{ProfileRuntime, RuntimeFuture}; -use tracedecay_runtime_core::db::{Database, DatabaseAuthority, TestDatabaseRuntimeMode}; -use tracedecay_runtime_core::shard_runtime::VerifiedGraphRuntimePortV1; -use tracedecay_session_memory::fact_store::DatabaseFactStore; -use tracedecay_session_memory::memory::MemoryApplicationError; -use tracedecay_session_memory::memory::{ - MemoryApplication, ProjectMemoryFactAddRequest, ProjectMemoryFactAddRequestOutcome, -}; -use tracedecay_store::{FactStoreError, ProjectMemoryGraphQueryV1}; - -mod user_scope_graph_runtime; -use user_scope_graph_runtime::bind_profile_memory_graph_runtime; - -struct FixtureProfileRuntime { - profile_id: UserProfileId, - sessions: RegisteredGlobalDbLeaseV1, - memory: Database, -} - -impl ProfileRuntime for FixtureProfileRuntime { - fn profile_id(&self) -> &UserProfileId { - &self.profile_id - } - - fn profile_sessions(&self) -> RuntimeFuture<'_, RegisteredGlobalDbLeaseV1> { - Box::pin(async { Ok(self.sessions.clone()) }) - } - - fn open_user_memory_db(&self) -> RuntimeFuture<'_, Database> { - Box::pin(async { Ok(self.memory.clone()) }) - } -} - -struct UserRuntimeHarness { - profile_root: PathBuf, - registry: Arc, - /// Strong graph port; the database keeps only a weak binding, so this - /// handle keeps the profile memory graph mountable for the test lifetime. - _memory_graph_runtime: Arc, - _session_runtime: RegisteredGlobalDbTestRuntime, - _directory: TempDir, -} - -impl UserRuntimeHarness { - async fn open(_label: &str) -> Self { - let directory = tempfile::tempdir().expect("user automation profile"); - let profile_root = directory.path().join("profile"); - let session_runtime = RegisteredGlobalDbTestRuntime::profile(&profile_root) - .await - .expect("registered profile session runtime"); - let memory_path = - tracedecay_session_memory::memory::user::user_memory_db_path(&profile_root); - let authority = - DatabaseAuthority::acquire_test(&memory_path, "profile automation memory fixture") - .expect("profile memory authority"); - let (memory, _) = Database::publish_profile_memory_test_runtime( - &memory_path, - &authority, - TestDatabaseRuntimeMode::Initialize, - ) - .await - .expect("registered profile memory"); - let memory_graph_runtime = bind_profile_memory_graph_runtime(&memory); - let registry: Arc = Arc::new(FixtureProfileRuntime { - profile_id: UserProfileId::new("profile.automation.fixture").expect("profile id"), - sessions: session_runtime.profile_database_arc(), - memory, - }); - Self { - profile_root, - registry, - _memory_graph_runtime: memory_graph_runtime, - _session_runtime: session_runtime, - _directory: directory, - } - } - - async fn memory(&self) -> Database { - self.registry - .open_user_memory_db() - .await - .expect("profile memory") - } -} - -fn configuration_revision() -> ConfigurationRevisionId { - ConfigurationRevisionId::new("config.user-automation-test.v1").expect("configuration revision") -} - -fn no_skill_needed_output() -> Value { - json!({ - "skills": [], - "outcome": "no_skill_needed", - "decision": { - "reason": "No repeated user-scoped evidence warrants a managed skill mutation.", - "remedy": "insufficient_repeated_evidence" - } - }) -} - -fn test_run_control() -> AutomationRunControl { - let interrupted = Arc::new(std::sync::atomic::AtomicBool::new(false)); - AutomationRunControl::from_interrupted({ - let interrupted = Arc::clone(&interrupted); - Arc::new(move || interrupted.load(std::sync::atomic::Ordering::Acquire)) - }) -} - -struct JsonBackend { - task: AgentTaskKind, - output: Value, - calls: AtomicUsize, -} - -impl JsonBackend { - fn new(task: AgentTaskKind, output: Value) -> Self { - Self { - task, - output, - calls: AtomicUsize::new(0), - } - } - - fn calls(&self) -> usize { - self.calls.load(Ordering::SeqCst) - } -} - -impl AgentTaskBackend for JsonBackend { - fn run_task( - &self, - request: &AgentTaskRequest, - ) -> std::result::Result - { - self.calls.fetch_add(1, Ordering::SeqCst); - assert_eq!(request.task, self.task); - Ok(AgentTaskResponse { - run_id: request.run_id.clone(), - task: request.task, - output_text: self.output.to_string(), - output_json: Some(self.output.clone()), - model: Some("fixture-model".to_string()), - provider: Some("fixture-provider".to_string()), - input_tokens: Some(10), - output_tokens: Some(20), - }) - } -} - -enum RetrievalOutcome { - Complete(Box), - Rejected(&'static str), - Empty, -} - -struct TestRetrieval { - anchor_session_id: SessionId, - outcome: RetrievalOutcome, -} - -impl TestRetrieval { - fn message(provider: &str, session_id: &str, message_id: &str, text: &str) -> Self { - Self { - anchor_session_id: SessionId::new(session_id).expect("session id"), - outcome: RetrievalOutcome::Complete(Box::new(AutomationTemporalEvidenceItem { - anchor_id: "user-scope-anchor".to_string(), - stable_id: "user-scope-stable".to_string(), - provider: provider.to_string(), - session_id: session_id.to_string(), - message_id: Some(message_id.to_string()), - source_id: Some("user-scope-occurrence".to_string()), - store_id: Some(1), - role: Some("user".to_string()), - ordinal: Some(1), - session_total_messages: Some(1), - knowledge_at_micros: 1_715_100_001_000_000, - normalized_score_micros: 1_000_000, - snippet: text.to_string(), - })), - } - } - - fn rejected(reason: &'static str) -> Self { - Self { - anchor_session_id: SessionId::new("rejected-user-scope").expect("session id"), - outcome: RetrievalOutcome::Rejected(reason), - } - } - - fn empty() -> Self { - Self { - anchor_session_id: SessionId::new("empty-user-scope").expect("session id"), - outcome: RetrievalOutcome::Empty, - } - } -} - -impl AutomationSessionRetrieval for TestRetrieval { - fn anchor_session_id(&self) -> &SessionId { - &self.anchor_session_id - } - - fn retrieve( - &self, - _query: tracedecay_session_memory::session::SessionTemporalQuery, - ) -> AutomationSessionRetrievalFuture<'_> { - Box::pin(async move { - match &self.outcome { - RetrievalOutcome::Complete(item) => { - AutomationTemporalRetrieval::Complete(AutomationTemporalEvidence { - items: vec![item.as_ref().clone()], - coverage: TemporalCoverageCountsV1 { - visible: 1, - hidden: 0, - unknown: 0, - redacted: 0, - }, - }) - } - RetrievalOutcome::Rejected(reason) => AutomationTemporalRetrieval::Rejected(reason), - RetrievalOutcome::Empty => AutomationTemporalRetrieval::CompleteZero, - } - }) - } -} - -fn enabled_user_config() -> AutomationConfig { - AutomationConfig { - enabled: true, - backend: AutomationBackend::CodexAppServer, - host_mode: AutomationHostMode::Standalone, - tasks: AutomationTaskSet { - memory_curator: AutomationTaskConfig { - enabled: true, - schedule: Some("manual".to_string()), - ..AutomationTaskConfig::default() - }, - session_reflector: AutomationTaskConfig { - enabled: true, - schedule: Some("manual".to_string()), - ..AutomationTaskConfig::default() - }, - skill_writer: AutomationTaskConfig { - enabled: true, - schedule: Some("manual".to_string()), - ..AutomationTaskConfig::default() - }, - }, - ..AutomationConfig::default() - } -} - -#[tokio::test] -async fn projectless_reflection_uses_caller_supplied_automation_configuration() { - let harness = UserRuntimeHarness::open("user-reflection").await; - let backend = JsonBackend::new( - AgentTaskKind::SessionReflector, - json!({ - "facts": [{ - "content": "The user wants projectless conversations stored in profile memory", - "category": "user_pref", - "tags": ["memory", "projectless"], - "entities": ["TraceDecay"], - "trust": 0.9, - "source_span": { - "session_id": "user-session-1", - "message_id": "user-message-1" - }, - "reason": "The user explicitly stated this durable preference" - }] - }), - ); - let config = enabled_user_config(); - let retrieval = TestRetrieval::message( - "hermes", - "user-session-1", - "user-message-1", - "Always keep general conversations in user memory.", - ); - - let run = run_user_session_reflector_with_backend_and_retrieval( - &harness.profile_root, - Arc::clone(&harness.registry), - &config, - &test_run_control(), - &configuration_revision(), - AutomationTaskIo { - backend: &backend, - retrieval: &retrieval, - }, - SessionReflectorAutomationOptions { - provider: "hermes".to_string(), - query: "user memory".to_string(), - ..SessionReflectorAutomationOptions::default() - }, - ) - .await - .expect("profile reflection"); - - assert_eq!(run.report["status"], json!("applied")); - let database = harness.memory().await; - let memory = MemoryApplication::new(FactOwnerV1::Profile, DatabaseFactStore::new(&database)) - .expect("profile memory authority"); - assert_eq!( - memory - .query_current_facts( - tracedecay_store::CurrentFactsQuery::new(FactOwnerV1::Profile, None, 10) - .expect("canonical profile fact query"), - ) - .await - .expect("profile facts") - .len(), - 1 - ); - assert!(database.database_path().is_file()); -} - -#[tokio::test] -async fn projectless_skill_writer_uses_user_ledger() { - let harness = UserRuntimeHarness::open("user-skill-writer").await; - let backend = JsonBackend::new(AgentTaskKind::SkillWriter, no_skill_needed_output()); - let retrieval = TestRetrieval::message( - "hermes", - "user-session-1", - "user-message-1", - "Review recurring automation workflows.", - ); - - let run = run_user_skill_writer_with_backend_and_retrieval( - crate::automation::host_io::plain_file_host_io(), - &harness.profile_root, - Arc::clone(&harness.registry), - &enabled_user_config(), - &configuration_revision(), - &backend, - &retrieval, - SkillWriterAutomationOptions { - provider: "hermes".to_string(), - query: "automation workflows".to_string(), - ..SkillWriterAutomationOptions::default() - }, - ) - .await - .expect("profile skill writer"); - - assert_eq!(backend.calls(), 1); - assert_eq!(run.ledger_record.status, AutomationRunStatus::Succeeded); - let records = crate::automation::run_ledger::load_run_records( - &user_automation_root(&harness.profile_root), - 10, - ) - .await - .expect("user ledger"); - assert_eq!(records.len(), 1); - assert_eq!(records[0].task, AgentTaskKind::SkillWriter); -} - -#[tokio::test] -async fn terminal_evidence_rejections_do_not_run_user_backends() { - for reason in [ - "session_evidence_denied", - "session_evidence_stale", - "session_evidence_partial", - "session_evidence_unavailable", - "session_evidence_budget_exhausted", - "session_evidence_cancelled", - ] { - let harness = UserRuntimeHarness::open("user-rejection").await; - let retrieval = TestRetrieval::rejected(reason); - let reflector_backend = - JsonBackend::new(AgentTaskKind::SessionReflector, json!({ "facts": [] })); - let skill_backend = JsonBackend::new(AgentTaskKind::SkillWriter, no_skill_needed_output()); - let config = enabled_user_config(); - - let reflector = run_user_session_reflector_with_backend_and_retrieval( - &harness.profile_root, - Arc::clone(&harness.registry), - &config, - &test_run_control(), - &configuration_revision(), - AutomationTaskIo { - backend: &reflector_backend, - retrieval: &retrieval, - }, - SessionReflectorAutomationOptions::default(), - ) - .await - .expect("rejected reflector"); - let skill = run_user_skill_writer_with_backend_and_retrieval( - crate::automation::host_io::plain_file_host_io(), - &harness.profile_root, - Arc::clone(&harness.registry), - &config, - &configuration_revision(), - &skill_backend, - &retrieval, - SkillWriterAutomationOptions::default(), - ) - .await - .expect("rejected skill writer"); - - assert_eq!(reflector.ledger_record.error.as_deref(), Some(reason)); - assert_eq!(skill.ledger_record.error.as_deref(), Some(reason)); - assert_eq!(reflector_backend.calls(), 0); - assert_eq!(skill_backend.calls(), 0); - assert!(!user_automation_root(&harness.profile_root).exists()); - } - - let harness = UserRuntimeHarness::open("user-empty-evidence").await; - let retrieval = TestRetrieval::empty(); - let reflector_backend = - JsonBackend::new(AgentTaskKind::SessionReflector, json!({ "facts": [] })); - let skill_backend = JsonBackend::new(AgentTaskKind::SkillWriter, no_skill_needed_output()); - let config = enabled_user_config(); - let reflector = run_user_session_reflector_with_backend_and_retrieval( - &harness.profile_root, - Arc::clone(&harness.registry), - &config, - &test_run_control(), - &configuration_revision(), - AutomationTaskIo { - backend: &reflector_backend, - retrieval: &retrieval, - }, - SessionReflectorAutomationOptions::default(), - ) - .await - .expect("empty reflector"); - let skill = run_user_skill_writer_with_backend_and_retrieval( - crate::automation::host_io::plain_file_host_io(), - &harness.profile_root, - Arc::clone(&harness.registry), - &config, - &configuration_revision(), - &skill_backend, - &retrieval, - SkillWriterAutomationOptions::default(), - ) - .await - .expect("empty skill writer"); - assert_eq!( - reflector.ledger_record.error.as_deref(), - Some("no_session_evidence") - ); - assert_eq!( - skill.ledger_record.error.as_deref(), - Some("no_skill_writer_evidence") - ); -} - -#[tokio::test] -async fn projectless_memory_curator_quarantines_deprecated_operations() { - let harness = UserRuntimeHarness::open("user-curator-deprecated").await; - let database = harness.memory().await; - let run_control = test_run_control(); - let seeded = seed_user_duplicate_facts(&database, &run_control).await; - let backend = JsonBackend::new( - AgentTaskKind::MemoryCurator, - json!({ - "ops": [{ - "op": "delete", - "fact_id": seeded.loser_id, - "confidence": 0.99, - "reason": "legacy operation must not mutate canonical memory" - }] - }), - ); - - let error = run_user_memory_curator_with_backend( - &harness.profile_root, - Arc::clone(&harness.registry), - &enabled_user_config(), - &configuration_revision(), - &backend, - MemoryCuratorAutomationOptions::default(), - &run_control, - ) - .await - .expect_err("deprecated operation must exhaust bounded repair and quarantine"); - - assert_eq!( - error.to_string(), - "config error: memory curator validation repair budget exhausted; output quarantined" - ); - let memory = MemoryApplication::new(FactOwnerV1::Profile, DatabaseFactStore::new(&database)) - .expect("profile memory authority"); - assert!( - memory - .get_project_memory_fact( - tracedecay_store::ProjectMemoryFactIdV1::new(FactOwnerV1::Profile, seeded.loser_id) - .expect("canonical loser target"), - run_control.read_control(), - ) - .await - .expect("canonical loser projection") - .is_some() - ); -} - -#[tokio::test] -async fn projectless_memory_curator_links_profile_memory_with_canonical_ids() { - let harness = UserRuntimeHarness::open("user-curator-link").await; - let database = harness.memory().await; - let run_control = test_run_control(); - let seeded = seed_user_duplicate_facts(&database, &run_control).await; - let backend = JsonBackend::new( - AgentTaskKind::MemoryCurator, - json!({ - "ops": [{ - "op": "link_facts", - "source": { - "fact_id": seeded.winner_id, - "expected_last_event_id": seeded.winner_event_id, - }, - "target": { - "fact_id": seeded.loser_id, - "expected_last_event_id": seeded.loser_event_id, - }, - "relation": "supports", - "evidence_facts": [{ - "fact_id": seeded.winner_id, - "expected_last_event_id": seeded.winner_event_id, - }], - "confidence": 0.99, - "source_label": "user-scope-fixture", - "metadata": {"reason": "The durable profile preferences support each other"} - }] - }), - ); - - let run = run_user_memory_curator_with_backend( - &harness.profile_root, - Arc::clone(&harness.registry), - &enabled_user_config(), - &configuration_revision(), - &backend, - MemoryCuratorAutomationOptions::default(), - &run_control, - ) - .await - .expect("profile memory curator"); - - assert_eq!(run.report["llm_apply"]["applied"], json!(1)); - assert_eq!( - run.report["llm_apply"]["receipts"][0]["receipt"]["facts_linked"], - json!(1) - ); -} - -#[tokio::test] -async fn projectless_memory_curator_normalizes_profile_fact_tags() { - let harness = UserRuntimeHarness::open("user-curator-normalize-tags").await; - let database = harness.memory().await; - let run_control = test_run_control(); - let seeded = seed_user_duplicate_facts(&database, &run_control).await; - let backend = JsonBackend::new( - AgentTaskKind::MemoryCurator, - json!({ - "ops": [{ - "op": "normalize_tags", - "target": { - "fact_id": seeded.winner_id, - "expected_last_event_id": seeded.winner_event_id, - }, - "tags": ["memory", "projectless"], - "evidence_facts": [{ - "fact_id": seeded.loser_id, - "expected_last_event_id": seeded.loser_event_id, - }], - "confidence": 0.99, - }] - }), - ); - - run_user_memory_curator_with_backend( - &harness.profile_root, - Arc::clone(&harness.registry), - &enabled_user_config(), - &configuration_revision(), - &backend, - MemoryCuratorAutomationOptions::default(), - &run_control, - ) - .await - .expect("profile memory curator"); - - let memory = MemoryApplication::new(FactOwnerV1::Profile, DatabaseFactStore::new(&database)) - .expect("profile memory authority"); - let fact = memory - .get_project_memory_fact( - tracedecay_store::ProjectMemoryFactIdV1::new(FactOwnerV1::Profile, seeded.winner_id) - .expect("canonical winner target"), - run_control.read_control(), - ) - .await - .expect("normalized fact") - .expect("fact remains"); - let tracedecay_store::ProjectMemoryFactProjectionV1::Available(fact) = fact else { - panic!("normalized fact payload must remain available"); - }; - assert_eq!(fact.tags(), ["memory", "projectless"]); -} - -#[derive(Clone)] -struct SeededUserDuplicateFacts { - winner_id: FactId, - winner_event_id: tracedecay_domain::FactEventId, - loser_id: FactId, - loser_event_id: tracedecay_domain::FactEventId, -} - -async fn seed_user_duplicate_facts( - database: &Database, - run_control: &AutomationRunControl, -) -> SeededUserDuplicateFacts { - let owner = FactOwnerV1::Profile; - let memory = MemoryApplication::new(owner.clone(), DatabaseFactStore::new(database)) - .expect("profile memory authority"); - let mut facts = Vec::with_capacity(2); - for content in [ - "General conversations belong in user memory.", - "General conversations belong in user memory!", - ] { - let preflight = memory - .preflight_project_memory_fact_add( - ProjectMemoryFactAddRequest { - content: content.to_string(), - category: tracedecay_domain::FactCategoryV1::UserPref, - source_label: Some("user-scope-fixture".to_string()), - tags: vec!["memory".to_string()], - entities: Vec::new(), - trust: Some(Confidence::new(0.95).expect("fixture confidence")), - metadata: json!({}), - }, - None, - ) - .expect("preflight profile fact"); - let write_control = run_control.write_control(); - let outcome = memory - .add_preflighted_project_memory_fact(preflight, &write_control) - .await - .expect("seed profile fact"); - let ProjectMemoryFactAddRequestOutcome::Applied(outcome) = outcome else { - panic!("fixture add must apply"); - }; - let tracedecay_store::ProjectMemoryFactProjectionV1::Available(fact) = outcome.fact() - else { - panic!("fixture fact payload must remain available"); - }; - facts.push((fact.fact_id().clone(), fact.last_event_id().clone())); - } - let roots = facts - .iter() - .map(|(fact_id, _)| fact_id.clone()) - .collect::>(); - let mut graph_current = false; - for _ in 0..512 { - let query = ProjectMemoryGraphQueryV1::new(owner.clone(), roots.clone(), 4_096) - .expect("profile graph readiness query"); - match memory - .project_memory_graph(query, run_control.read_control()) - .await - { - Ok(_) => { - graph_current = true; - break; - } - Err(MemoryApplicationError::Store( - FactStoreError::GraphConflict | FactStoreError::GraphUnavailable, - )) => tokio::task::yield_now().await, - Err(error) => panic!("profile graph readiness failed: {error}"), - } - } - assert!( - graph_current, - "profile graph did not reach the seeded facts" - ); - let (winner_id, winner_event_id) = facts.remove(0); - let (loser_id, loser_event_id) = facts.remove(0); - SeededUserDuplicateFacts { - winner_id, - winner_event_id, - loser_id, - loser_event_id, - } -} diff --git a/crates/tracedecay-automation-runtime/src/automation/runner/user_scope_tests/user_scope_graph_runtime.rs b/crates/tracedecay-automation-runtime/src/automation/runner/user_scope_tests/user_scope_graph_runtime.rs deleted file mode 100644 index b3c0717f32..0000000000 --- a/crates/tracedecay-automation-runtime/src/automation/runner/user_scope_tests/user_scope_graph_runtime.rs +++ /dev/null @@ -1,109 +0,0 @@ -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; - -use tracedecay_graph_db::{ - GraphDbError, GraphGenerationManifest, GraphIdempotencyKey, GraphProjectionIdentity, - NeverCancelled, VerifiedGraphSnapshot, -}; -use tracedecay_store::{FactReadControl, StoreRuntimeBindingV1, VerifiedStoreLocatorV1}; - -use tracedecay_runtime_core::db::Database; -use tracedecay_runtime_core::shard_runtime::VerifiedGraphRuntimePortV1; - -struct ProfileMemoryGraphRuntime { - binding: StoreRuntimeBindingV1, - locator: VerifiedStoreLocatorV1, - manifest: Mutex>, -} - -impl ProfileMemoryGraphRuntime { - fn new(database: &Database) -> Self { - Self { - binding: database.registered_binding().clone(), - locator: database.registered_verified_locator().clone(), - manifest: Mutex::new(None), - } - } - - fn remember( - &self, - manifest: &GraphGenerationManifest, - ) -> Result { - let snapshot = VerifiedGraphSnapshot::memory(manifest.clone(), Arc::new(NeverCancelled))?; - *self - .manifest - .lock() - .map_err(|_| GraphDbError::invalid("profile memory graph fixture lock poisoned"))? = - Some(manifest.clone()); - Ok(snapshot) - } -} - -impl VerifiedGraphRuntimePortV1 for ProfileMemoryGraphRuntime { - fn relational_binding(&self) -> &StoreRuntimeBindingV1 { - &self.binding - } - - fn relational_verified_locator(&self) -> &VerifiedStoreLocatorV1 { - &self.locator - } - - fn cancel_reconciliation(&self) {} - - fn publish_verified_manifest( - &self, - manifest: &GraphGenerationManifest, - _idempotency_key: GraphIdempotencyKey, - cancelled: Arc, - ) -> Result { - if cancelled.load(Ordering::Acquire) { - return Err(GraphDbError::Cancelled); - } - self.remember(manifest) - } - - fn reconcile_verified_manifest( - &self, - manifest: &GraphGenerationManifest, - _idempotency_key: GraphIdempotencyKey, - ) -> Result { - self.remember(manifest) - } - - fn verified_snapshot( - &self, - projection: &GraphProjectionIdentity, - read_control: FactReadControl, - ) -> Result, GraphDbError> { - if read_control.interrupted() { - return Err(GraphDbError::Cancelled); - } - let manifest = self - .manifest - .lock() - .map_err(|_| GraphDbError::invalid("profile memory graph fixture lock poisoned"))? - .clone(); - match manifest { - Some(manifest) if &manifest.projection == projection => Ok(Some( - VerifiedGraphSnapshot::memory(manifest, Arc::new(NeverCancelled))?, - )), - Some(_) | None => Ok(None), - } - } -} - -/// Binds the profile memory graph fixture and returns the strong port. -/// -/// `Database::bind_memory_graph_runtime` retains only a weak binding, so the -/// caller must hold the returned `Arc` for as long as graph operations should -/// stay mountable. -pub(super) fn bind_profile_memory_graph_runtime( - database: &Database, -) -> Arc { - let runtime: Arc = - Arc::new(ProfileMemoryGraphRuntime::new(database)); - database - .bind_memory_graph_runtime(Arc::clone(&runtime)) - .expect("bind profile memory graph fixture"); - runtime -} diff --git a/crates/tracedecay-automation-runtime/src/automation/session_reflector.rs b/crates/tracedecay-automation-runtime/src/automation/session_reflector.rs index 7e7088b4e5..91bc27d703 100644 --- a/crates/tracedecay-automation-runtime/src/automation/session_reflector.rs +++ b/crates/tracedecay-automation-runtime/src/automation/session_reflector.rs @@ -560,4 +560,3 @@ fn quarantined_fact_with_validation( "validation": validation, })) } - diff --git a/crates/tracedecay-automation-runtime/src/automation/skill_writer.rs b/crates/tracedecay-automation-runtime/src/automation/skill_writer.rs index 3a0d480fab..106db47adc 100644 --- a/crates/tracedecay-automation-runtime/src/automation/skill_writer.rs +++ b/crates/tracedecay-automation-runtime/src/automation/skill_writer.rs @@ -936,7 +936,6 @@ fn rejected_skill(proposal: &Value, reason: &str) -> Value { }) } - #[cfg(test)] mod tests { use super::*; diff --git a/crates/tracedecay-capture/src/cursor.rs b/crates/tracedecay-capture/src/cursor.rs index bf554a89c2..f2d88bd9bb 100644 --- a/crates/tracedecay-capture/src/cursor.rs +++ b/crates/tracedecay-capture/src/cursor.rs @@ -1,10 +1,10 @@ use serde_json::Value; use sha2::{Digest, Sha256}; use tracedecay_domain::{ - CanonicalGitEvidenceKindV1, CanonicalObservationEnvelopeV1, - CanonicalObservationEvidenceV1, CanonicalObservationFactV1, CanonicalObservationRelationsV1, - CanonicalReasoningVisibilityV1, CanonicalUnknownStateV1, CanonicalWorkflowEvidenceKindV1, - ObservationId, ObservationOrderingDomainV1, ObservationPositionalOccurrenceV1, ProviderId, + CanonicalGitEvidenceKindV1, CanonicalObservationEnvelopeV1, CanonicalObservationEvidenceV1, + CanonicalObservationFactV1, CanonicalObservationRelationsV1, CanonicalReasoningVisibilityV1, + CanonicalUnknownStateV1, CanonicalWorkflowEvidenceKindV1, ObservationId, + ObservationOrderingDomainV1, ObservationPositionalOccurrenceV1, ProviderId, ProviderUsageCounterSemanticsV1, ProviderUsageCountersV1, ProviderUsageModelV1, ProviderUsageScopeV1, SessionId, }; From caa80457f0438e8d04803d0d9a37d0810827ce25 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:16:04 +0000 Subject: [PATCH 111/182] simplify(pass-5/5): drop duplicated extractor walkers Co-authored-by: Zack Jackson --- .../src/cpp_extractor.rs | 3 +- .../src/csharp_extractor.rs | 3 +- .../src/dart_extractor.rs | 3 +- .../src/dockerfile_extractor.rs | 3 +- .../src/java_extractor.rs | 21 ++---------- .../src/kotlin_extractor.rs | 3 +- .../src/markdown_extractor.rs | 34 ++++++------------- .../src/objc_extractor.rs | 3 +- .../src/pascal_extractor.rs | 3 +- .../src/perl_extractor.rs | 3 +- .../src/php_extractor.rs | 3 +- .../src/powershell_extractor.rs | 32 ++--------------- .../src/proto_extractor.rs | 3 +- .../src/python_extractor.rs | 3 +- .../src/ruby_extractor.rs | 3 +- .../src/rust_extractor.rs | 22 ++---------- .../src/scala_extractor.rs | 3 +- .../src/swift_extractor.rs | 3 +- .../src/typescript_extractor.rs | 3 +- .../src/vbnet_extractor.rs | 3 +- .../src/zig_extractor.rs | 3 +- 21 files changed, 37 insertions(+), 123 deletions(-) diff --git a/crates/tracedecay-code-extraction/src/cpp_extractor.rs b/crates/tracedecay-code-extraction/src/cpp_extractor.rs index f53229c7fd..0684748115 100644 --- a/crates/tracedecay-code-extraction/src/cpp_extractor.rs +++ b/crates/tracedecay-code-extraction/src/cpp_extractor.rs @@ -58,8 +58,7 @@ impl<'s> ExtractionState<'s> { /// /// The file root is pushed onto `node_stack` as the first frame when /// extraction begins, so iterating the stack already yields the file - /// path as the leading segment. Prepending `self.file_path` here was - /// a leftover that duplicated the prefix (`::::Type::method`). + /// path as the leading segment. fn qualified_prefix(&self) -> String { self.node_stack .iter() diff --git a/crates/tracedecay-code-extraction/src/csharp_extractor.rs b/crates/tracedecay-code-extraction/src/csharp_extractor.rs index e15dcc1e6c..e2fd772a5e 100644 --- a/crates/tracedecay-code-extraction/src/csharp_extractor.rs +++ b/crates/tracedecay-code-extraction/src/csharp_extractor.rs @@ -50,8 +50,7 @@ impl<'s> ExtractionState<'s> { /// /// The file root is pushed onto `node_stack` as the first frame when /// extraction begins, so iterating the stack already yields the file - /// path as the leading segment. Prepending `self.file_path` here was - /// a leftover that duplicated the prefix (`::::Type::method`). + /// path as the leading segment. fn qualified_prefix(&self) -> String { self.node_stack .iter() diff --git a/crates/tracedecay-code-extraction/src/dart_extractor.rs b/crates/tracedecay-code-extraction/src/dart_extractor.rs index 9828268cf3..a1cedffd37 100644 --- a/crates/tracedecay-code-extraction/src/dart_extractor.rs +++ b/crates/tracedecay-code-extraction/src/dart_extractor.rs @@ -51,8 +51,7 @@ impl<'s> ExtractionState<'s> { /// /// The file root is pushed onto `node_stack` as the first frame when /// extraction begins, so iterating the stack already yields the file - /// path as the leading segment. Prepending `self.file_path` here was - /// a leftover that duplicated the prefix (`::::Type::method`). + /// path as the leading segment. fn qualified_prefix(&self) -> String { self.node_stack .iter() diff --git a/crates/tracedecay-code-extraction/src/dockerfile_extractor.rs b/crates/tracedecay-code-extraction/src/dockerfile_extractor.rs index 7226ca9961..911ac977c1 100644 --- a/crates/tracedecay-code-extraction/src/dockerfile_extractor.rs +++ b/crates/tracedecay-code-extraction/src/dockerfile_extractor.rs @@ -49,8 +49,7 @@ impl<'s> ExtractionState<'s> { /// /// The file root is pushed onto `node_stack` as the first frame when /// extraction begins, so iterating the stack already yields the file - /// path as the leading segment. Prepending `self.file_path` here was - /// a leftover that duplicated the prefix (`::::Type::method`). + /// path as the leading segment. fn qualified_prefix(&self) -> String { self.node_stack .iter() diff --git a/crates/tracedecay-code-extraction/src/java_extractor.rs b/crates/tracedecay-code-extraction/src/java_extractor.rs index c03766640a..350d2522f1 100644 --- a/crates/tracedecay-code-extraction/src/java_extractor.rs +++ b/crates/tracedecay-code-extraction/src/java_extractor.rs @@ -15,6 +15,7 @@ use crate::{ AnnotationEmitterState, emit_annotation_usage, scan_children_for_annotation_kinds, }, complexity::{JAVA_COMPLEXITY, count_complexity}, + traversal::has_direct_child_kind, }; /// Extracts code graph nodes and edges from Java source files using tree-sitter. @@ -58,8 +59,7 @@ impl<'s> ExtractionState<'s> { /// /// The file root is pushed onto `node_stack` as the first frame when /// extraction begins, so iterating the stack already yields the file - /// path as the leading segment. Prepending `self.file_path` here was - /// a leftover that duplicated the prefix (`::::Type::method`). + /// path as the leading segment. fn qualified_prefix(&self) -> String { self.node_stack .iter() @@ -683,7 +683,7 @@ impl JavaExtractor { // 2. It is in an interface and has no body (no `block` child). let has_abstract_modifier = Self::has_modifier(node, state, "abstract"); let has_body = - node.child_by_field_name("body").is_some() || Self::has_child_of_kind(node, "block"); + node.child_by_field_name("body").is_some() || has_direct_child_kind(node, "block"); let is_abstract = has_abstract_modifier || (state.inside_interface && !has_body); let kind = if is_abstract { @@ -984,21 +984,6 @@ impl JavaExtractor { false } - fn has_child_of_kind(node: TsNode<'_>, kind: &str) -> bool { - let mut cursor = node.walk(); - if cursor.goto_first_child() { - loop { - if cursor.node().kind() == kind { - return true; - } - if !cursor.goto_next_sibling() { - break; - } - } - } - false - } - /// Extract the declaration signature (text from start up to the opening `{`). fn extract_declaration_signature(state: &ExtractionState, node: TsNode<'_>) -> String { let text = state.node_text(node); diff --git a/crates/tracedecay-code-extraction/src/kotlin_extractor.rs b/crates/tracedecay-code-extraction/src/kotlin_extractor.rs index 6e9d5be644..f0935f8aa8 100644 --- a/crates/tracedecay-code-extraction/src/kotlin_extractor.rs +++ b/crates/tracedecay-code-extraction/src/kotlin_extractor.rs @@ -61,8 +61,7 @@ impl<'s> ExtractionState<'s> { /// /// The file root is pushed onto `node_stack` as the first frame when /// extraction begins, so iterating the stack already yields the file - /// path as the leading segment. Prepending `self.file_path` here was - /// a leftover that duplicated the prefix (`::::Type::method`). + /// path as the leading segment. fn qualified_prefix(&self) -> String { self.node_stack .iter() diff --git a/crates/tracedecay-code-extraction/src/markdown_extractor.rs b/crates/tracedecay-code-extraction/src/markdown_extractor.rs index 367a48810f..527b9ba56a 100644 --- a/crates/tracedecay-code-extraction/src/markdown_extractor.rs +++ b/crates/tracedecay-code-extraction/src/markdown_extractor.rs @@ -18,6 +18,7 @@ use std::time::Instant; use tree_sitter::{Node as TsNode, Parser, Range, Tree}; use crate::common::local_node_id; +use crate::traversal::find_direct_child_by_kind; use crate::types::{ ComplexityAnalysisV1, Edge, EdgeKind, ExtractionResult, Node, NodeKind, Visibility, generate_node_id, @@ -448,7 +449,7 @@ impl MarkdownExtractor { match node.kind() { "inline_link" => Self::visit_link(state, node), "image" => { - if child_of_kind(node, "link_destination").is_some() { + if find_direct_child_by_kind(node, "link_destination").is_some() { Self::visit_link(state, node); } else { Self::queue_reference_link(state, node); @@ -471,10 +472,10 @@ impl MarkdownExtractor { } fn visit_reference_definition(state: &mut ExtractionState, node: TsNode<'_>) { - let Some(label_node) = child_of_kind(node, "link_label") else { + let Some(label_node) = find_direct_child_by_kind(node, "link_label") else { return; }; - let Some(dest_node) = child_of_kind(node, "link_destination") else { + let Some(dest_node) = find_direct_child_by_kind(node, "link_destination") else { return; }; let label = normalize_link_label(&strip_label_brackets(state.node_text(label_node))); @@ -495,12 +496,15 @@ impl MarkdownExtractor { return; }; let label = match node.kind() { - "full_reference_link" | "image" => child_of_kind(node, "link_label") + "full_reference_link" | "image" => find_direct_child_by_kind(node, "link_label") .map(|n| strip_label_brackets(state.node_text(n))) .or_else(|| { - child_of_kind(node, "link_text").map(|n| state.node_text(n).to_string()) + find_direct_child_by_kind(node, "link_text") + .map(|n| state.node_text(n).to_string()) }), - _ => child_of_kind(node, "link_text").map(|n| state.node_text(n).to_string()), + _ => { + find_direct_child_by_kind(node, "link_text").map(|n| state.node_text(n).to_string()) + } }; let Some(label) = label.map(|raw| normalize_link_label(&raw)) else { return; @@ -526,7 +530,7 @@ impl MarkdownExtractor { } fn visit_link(state: &mut ExtractionState, node: TsNode<'_>) { - let Some(url_node) = child_of_kind(node, "link_destination") else { + let Some(url_node) = find_direct_child_by_kind(node, "link_destination") else { return; }; let Some(parent_id) = state @@ -562,22 +566,6 @@ impl MarkdownExtractor { } } -fn child_of_kind<'tree>(node: TsNode<'tree>, kind: &str) -> Option> { - let mut cursor = node.walk(); - if cursor.goto_first_child() { - loop { - let child = cursor.node(); - if child.kind() == kind { - return Some(child); - } - if !cursor.goto_next_sibling() { - break; - } - } - } - None -} - /// CommonMark label matching: trim, collapse whitespace, case-fold. fn normalize_link_label(label: &str) -> String { label diff --git a/crates/tracedecay-code-extraction/src/objc_extractor.rs b/crates/tracedecay-code-extraction/src/objc_extractor.rs index 2ad5f39139..f5ff046285 100644 --- a/crates/tracedecay-code-extraction/src/objc_extractor.rs +++ b/crates/tracedecay-code-extraction/src/objc_extractor.rs @@ -52,8 +52,7 @@ impl<'s> ExtractionState<'s> { /// /// The file root is pushed onto `node_stack` as the first frame when /// extraction begins, so iterating the stack already yields the file - /// path as the leading segment. Prepending `self.file_path` here was - /// a leftover that duplicated the prefix (`::::Type::method`). + /// path as the leading segment. fn qualified_prefix(&self) -> String { self.node_stack .iter() diff --git a/crates/tracedecay-code-extraction/src/pascal_extractor.rs b/crates/tracedecay-code-extraction/src/pascal_extractor.rs index 8d4cf2882b..a4f592afec 100644 --- a/crates/tracedecay-code-extraction/src/pascal_extractor.rs +++ b/crates/tracedecay-code-extraction/src/pascal_extractor.rs @@ -60,8 +60,7 @@ impl<'s> ExtractionState<'s> { /// /// The file root is pushed onto `node_stack` as the first frame when /// extraction begins, so iterating the stack already yields the file - /// path as the leading segment. Prepending `self.file_path` here was - /// a leftover that duplicated the prefix (`::::Type::method`). + /// path as the leading segment. fn qualified_prefix(&self) -> String { self.node_stack .iter() diff --git a/crates/tracedecay-code-extraction/src/perl_extractor.rs b/crates/tracedecay-code-extraction/src/perl_extractor.rs index 770e48311a..6b9a471709 100644 --- a/crates/tracedecay-code-extraction/src/perl_extractor.rs +++ b/crates/tracedecay-code-extraction/src/perl_extractor.rs @@ -51,8 +51,7 @@ impl<'s> ExtractionState<'s> { /// /// The file root is pushed onto `node_stack` as the first frame when /// extraction begins, so iterating the stack already yields the file - /// path as the leading segment. Prepending `self.file_path` here was - /// a leftover that duplicated the prefix (`::::Type::method`). + /// path as the leading segment. fn qualified_prefix(&self) -> String { self.node_stack .iter() diff --git a/crates/tracedecay-code-extraction/src/php_extractor.rs b/crates/tracedecay-code-extraction/src/php_extractor.rs index b58e0ca423..479071f53b 100644 --- a/crates/tracedecay-code-extraction/src/php_extractor.rs +++ b/crates/tracedecay-code-extraction/src/php_extractor.rs @@ -51,8 +51,7 @@ impl<'s> ExtractionState<'s> { /// /// The file root is pushed onto `node_stack` as the first frame when /// extraction begins, so iterating the stack already yields the file - /// path as the leading segment. Prepending `self.file_path` here was - /// a leftover that duplicated the prefix (`::::Type::method`). + /// path as the leading segment. fn qualified_prefix(&self) -> String { self.node_stack .iter() diff --git a/crates/tracedecay-code-extraction/src/powershell_extractor.rs b/crates/tracedecay-code-extraction/src/powershell_extractor.rs index 11646da95b..f0117a895a 100644 --- a/crates/tracedecay-code-extraction/src/powershell_extractor.rs +++ b/crates/tracedecay-code-extraction/src/powershell_extractor.rs @@ -7,7 +7,7 @@ use tree_sitter::{Node as TsNode, Tree}; use crate::common::{ExtractionState, local_node_id}; use crate::complexity::{POWERSHELL_COMPLEXITY, count_complexity}; -use crate::traversal::find_direct_child_by_kind; +use crate::traversal::{find_descendant_by_kind, find_direct_child_by_kind}; use crate::types::{ ComplexityAnalysisV1, Edge, EdgeKind, ExtractionResult, Node, NodeKind, UnresolvedRef, Visibility, generate_node_id, @@ -173,12 +173,12 @@ impl PowerShellExtractor { return; }; - let Some(cast) = Self::find_descendant_by_kind(left, "cast_expression") else { + let Some(cast) = find_descendant_by_kind(left, "cast_expression") else { return; }; // The variable is a child of the cast_expression. - let Some(var_node) = Self::find_descendant_by_kind(cast, "variable") else { + let Some(var_node) = find_descendant_by_kind(cast, "variable") else { return; }; @@ -387,32 +387,6 @@ impl PowerShellExtractor { } } - /// Find the first descendant of a node with a given kind (recursive DFS). - fn find_descendant_by_kind<'a>(node: TsNode<'a>, kind: &str) -> Option> { - let mut stack = vec![node]; - while let Some(current) = stack.pop() { - if current.kind() == kind { - return Some(current); - } - // Push children via cursor (O(N) per node) and reverse so the - // first child pops first. Previous revision used `current.child(i)` - // in a `for i in (0..N).rev()` loop, which is O(N²) per node - // because `child(i)` walks sibling links from index 0. - let start = stack.len(); - let mut cursor = current.walk(); - if cursor.goto_first_child() { - loop { - stack.push(cursor.node()); - if !cursor.goto_next_sibling() { - break; - } - } - } - stack[start..].reverse(); - } - None - } - /// Build the final `ExtractionResult` from the accumulated state. fn build_result(state: ExtractionState, start: Instant) -> ExtractionResult { ExtractionResult { diff --git a/crates/tracedecay-code-extraction/src/proto_extractor.rs b/crates/tracedecay-code-extraction/src/proto_extractor.rs index c077280ddf..c7ea37cd3c 100644 --- a/crates/tracedecay-code-extraction/src/proto_extractor.rs +++ b/crates/tracedecay-code-extraction/src/proto_extractor.rs @@ -55,8 +55,7 @@ impl<'s> ExtractionState<'s> { /// /// The file root is pushed onto `node_stack` as the first frame when /// extraction begins, so iterating the stack already yields the file - /// path as the leading segment. Prepending `self.file_path` here was - /// a leftover that duplicated the prefix (`::::Type::method`). + /// path as the leading segment. fn qualified_prefix(&self) -> String { self.node_stack .iter() diff --git a/crates/tracedecay-code-extraction/src/python_extractor.rs b/crates/tracedecay-code-extraction/src/python_extractor.rs index 2eb0a4cfd4..5afa90ba8d 100644 --- a/crates/tracedecay-code-extraction/src/python_extractor.rs +++ b/crates/tracedecay-code-extraction/src/python_extractor.rs @@ -55,8 +55,7 @@ impl<'s> ExtractionState<'s> { /// /// The file root is pushed onto `node_stack` as the first frame when /// extraction begins, so iterating the stack already yields the file - /// path as the leading segment. Prepending `self.file_path` here was - /// a leftover that duplicated the prefix (`::::Type::method`). + /// path as the leading segment. fn qualified_prefix(&self) -> String { self.node_stack .iter() diff --git a/crates/tracedecay-code-extraction/src/ruby_extractor.rs b/crates/tracedecay-code-extraction/src/ruby_extractor.rs index 7b8e53c7a0..3deceaed4e 100644 --- a/crates/tracedecay-code-extraction/src/ruby_extractor.rs +++ b/crates/tracedecay-code-extraction/src/ruby_extractor.rs @@ -51,8 +51,7 @@ impl<'s> ExtractionState<'s> { /// /// The file root is pushed onto `node_stack` as the first frame when /// extraction begins, so iterating the stack already yields the file - /// path as the leading segment. Prepending `self.file_path` here was - /// a leftover that duplicated the prefix (`::::Type::method`). + /// path as the leading segment. fn qualified_prefix(&self) -> String { self.node_stack .iter() diff --git a/crates/tracedecay-code-extraction/src/rust_extractor.rs b/crates/tracedecay-code-extraction/src/rust_extractor.rs index 973e7b59c4..6039667648 100644 --- a/crates/tracedecay-code-extraction/src/rust_extractor.rs +++ b/crates/tracedecay-code-extraction/src/rust_extractor.rs @@ -14,6 +14,7 @@ use crate::extraction_artifact::{ ExtractedImportEvidenceV1, ExtractionArtifactV1, ImportNamespaceV1, ImportReexportScopeV1, import_module_kind, }; +use crate::traversal::find_direct_child_by_kind; use crate::types::{ ComplexityAnalysisV1, Edge, EdgeKind, ExtractionResult, Node, NodeKind, SourceSpan, UnresolvedRef, Visibility, generate_node_id, @@ -165,8 +166,7 @@ impl<'s> ExtractionState<'s> { /// /// The file root is pushed onto `node_stack` as the first frame when /// extraction begins, so iterating the stack already yields the file - /// path as the leading segment. Prepending `self.file_path` here was - /// a leftover that duplicated the prefix (`::::Type::method`). + /// path as the leading segment. fn qualified_prefix(&self) -> String { self.node_stack .iter() @@ -2031,7 +2031,7 @@ impl RustExtractor { } } } - let Some(where_clause) = Self::child_of_kind(item, "where_clause") else { + let Some(where_clause) = find_direct_child_by_kind(item, "where_clause") else { return; }; let mut cursor = where_clause.walk(); @@ -2084,22 +2084,6 @@ impl RustExtractor { clause } - fn child_of_kind<'t>(node: TsNode<'t>, kind: &str) -> Option> { - let mut cursor = node.walk(); - if !cursor.goto_first_child() { - return None; - } - loop { - let child = cursor.node(); - if child.kind() == kind { - return Some(child); - } - if !cursor.goto_next_sibling() { - return None; - } - } - } - /// The type a `let` initialiser states in syntax: a `T { .. }` literal, /// optionally behind `?`. Method names are never return-type evidence. /// Abstain rather than fabricate a receiver type. diff --git a/crates/tracedecay-code-extraction/src/scala_extractor.rs b/crates/tracedecay-code-extraction/src/scala_extractor.rs index 827d6ad676..21e5852965 100644 --- a/crates/tracedecay-code-extraction/src/scala_extractor.rs +++ b/crates/tracedecay-code-extraction/src/scala_extractor.rs @@ -56,8 +56,7 @@ impl<'s> ExtractionState<'s> { /// /// The file root is pushed onto `node_stack` as the first frame when /// extraction begins, so iterating the stack already yields the file - /// path as the leading segment. Prepending `self.file_path` here was - /// a leftover that duplicated the prefix (`::::Type::method`). + /// path as the leading segment. fn qualified_prefix(&self) -> String { self.node_stack .iter() diff --git a/crates/tracedecay-code-extraction/src/swift_extractor.rs b/crates/tracedecay-code-extraction/src/swift_extractor.rs index b2f4a8d695..355c901589 100644 --- a/crates/tracedecay-code-extraction/src/swift_extractor.rs +++ b/crates/tracedecay-code-extraction/src/swift_extractor.rs @@ -51,8 +51,7 @@ impl<'s> ExtractionState<'s> { /// /// The file root is pushed onto `node_stack` as the first frame when /// extraction begins, so iterating the stack already yields the file - /// path as the leading segment. Prepending `self.file_path` here was - /// a leftover that duplicated the prefix (`::::Type::method`). + /// path as the leading segment. fn qualified_prefix(&self) -> String { self.node_stack .iter() diff --git a/crates/tracedecay-code-extraction/src/typescript_extractor.rs b/crates/tracedecay-code-extraction/src/typescript_extractor.rs index bf7c83df5d..4c5bb687c4 100644 --- a/crates/tracedecay-code-extraction/src/typescript_extractor.rs +++ b/crates/tracedecay-code-extraction/src/typescript_extractor.rs @@ -68,8 +68,7 @@ impl<'s> ExtractionState<'s> { /// /// The file root is pushed onto `node_stack` as the first frame when /// extraction begins, so iterating the stack already yields the file - /// path as the leading segment. Prepending `self.file_path` here was - /// a leftover that duplicated the prefix (`::::Type::method`). + /// path as the leading segment. fn qualified_prefix(&self) -> String { self.node_stack .iter() diff --git a/crates/tracedecay-code-extraction/src/vbnet_extractor.rs b/crates/tracedecay-code-extraction/src/vbnet_extractor.rs index ab15161e51..bc3f14a9ad 100644 --- a/crates/tracedecay-code-extraction/src/vbnet_extractor.rs +++ b/crates/tracedecay-code-extraction/src/vbnet_extractor.rs @@ -80,8 +80,7 @@ impl<'s> ExtractionState<'s> { /// /// The file root is pushed onto `node_stack` as the first frame when /// extraction begins, so iterating the stack already yields the file - /// path as the leading segment. Prepending `self.file_path` here was - /// a leftover that duplicated the prefix (`::::Type::method`). + /// path as the leading segment. fn qualified_prefix(&self) -> String { self.node_stack .iter() diff --git a/crates/tracedecay-code-extraction/src/zig_extractor.rs b/crates/tracedecay-code-extraction/src/zig_extractor.rs index 34a9469824..4d72f6c8a1 100644 --- a/crates/tracedecay-code-extraction/src/zig_extractor.rs +++ b/crates/tracedecay-code-extraction/src/zig_extractor.rs @@ -51,8 +51,7 @@ impl<'s> ExtractionState<'s> { /// /// The file root is pushed onto `node_stack` as the first frame when /// extraction begins, so iterating the stack already yields the file - /// path as the leading segment. Prepending `self.file_path` here was - /// a leftover that duplicated the prefix (`::::Type::method`). + /// path as the leading segment. fn qualified_prefix(&self) -> String { self.node_stack .iter() From 4e74272623a99884efa5e2d323639ae9fba81369 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:24:33 +0000 Subject: [PATCH 112/182] simplify(pass-1/5): share invocation delivery loop Co-authored-by: Zack Jackson --- .../src/daemon/connection_serving.rs | 657 +++++++----------- 1 file changed, 242 insertions(+), 415 deletions(-) diff --git a/crates/tracedecay/src/daemon/connection_serving.rs b/crates/tracedecay/src/daemon/connection_serving.rs index 4d3d087f86..c3cd4db906 100644 --- a/crates/tracedecay/src/daemon/connection_serving.rs +++ b/crates/tracedecay/src/daemon/connection_serving.rs @@ -7,7 +7,7 @@ use super::*; use tracedecay_daemon_protocol::DaemonInvocationPayload; use tracedecay_daemon_service::ProfileHostAdmissionBootstrapStatus; -use tracedecay_daemon_service::{DaemonInvocationService, Lease}; +use tracedecay_daemon_service::{DaemonInvocationService, DaemonLspSessionAccess, Lease}; use tracedecay_mcp::BrokerSelectedResponseLease; use tracedecay_runtime_core::logging::log_daemon_event; use tracedecay_session_memory::context::CancellationToken; @@ -803,227 +803,241 @@ async fn serve_broker_socket_client( serve_broker_socket_client_inner(stream, engine, auth_token, admission_class).await } -/// LSP sessions this connection owns are cleaned up exactly once, on every -/// exit path of the invocation loop. -#[cfg(unix)] +/// Drive one retained daemon-invocation connection: write each response, +/// settle its delivery ACK, and keep reading until the peer or the daemon +/// stops the connection. +/// +/// Unix and portable brokers differ only in how a request becomes a response. +/// The ACK state machine is one path so a dropped write, a missed deadline, +/// and a mismatched ACK id settle the same attempts on both. #[expect( clippy::too_many_lines, - reason = "LSP sessions this connection owns are cleaned up exactly once, on every exit path of the invocation loop." + reason = "Delivery ACK settlement is one state machine shared by the Unix and portable brokers." )] -async fn serve_retained_invocation_connection( +async fn drive_retained_invocation_responses<'a>( mut invocation: std::result::Result, - mut transport: BrokerStreamTransport, - engine: DaemonEngine, - handshake: DaemonHandshake, + transport: &mut BrokerStreamTransport, + service: &DaemonInvocationService, + handshake: &DaemonHandshake, + lifecycle: &DaemonLifecycle, + owned_lsp_sessions: &mut HashMap, + mut execute: impl FnMut( + DaemonInvocationRequest, + ) -> std::pin::Pin< + Box + Send + 'a>, + >, ) -> Result<()> { - let mut owned_lsp_sessions = HashMap::new(); let mut pending_line = None; - // Keep the retained invocation loop out of the broker connection - // future's inline state. With Hotpath enabled the surrounding - // transport wrapper is polled on Tokio's ordinary worker stack; - // embedding this loop there makes construction alone exceed that - // stack before the first request can be served. - let result = boxed_broker_connection_phase(async { - loop { - let delivery = invocation.as_ref().ok().and_then(|request| { - DaemonWorkDeliveryDescriptorV1::from_request(request, &handshake) - }); - let request_id = invocation - .as_ref() - .ok() - .map(|request| request.request_id.clone()); - let ack_deadline = invocation - .as_ref() - .ok() - .and_then(|request| request.delivery_ack_deadline()) - .cloned(); - let session_transition = invocation - .as_ref() - .ok() - .and_then(invocation_lsp_session_transition); - let response = match invocation { - Ok(request) => { - Box::pin(execute_daemon_invocation(&engine, &handshake, request)).await - } - Err(response) => response, - }; - update_connection_lsp_sessions( - &mut owned_lsp_sessions, - session_transition.as_ref(), - &response, - ); - let delivery = - delivery.filter(|delivery| delivery.is_successful_delivery(&response)); - // Resolve fan-out bindings before the socket response crosses - // the wire. The same immutable attempts are used for a - // Delivered or Dropped ACK; no mutable Work lookup occurs at - // terminal-ACK time. - let delivery_attempts = if let Some(delivery) = delivery { - Some( - delivery - .attempts( - &engine.invocation.service, - handshake.project_path.as_deref(), - &response, - ) - .await, - ) - } else { - None - }; - let write_result = - write_daemon_invocation_response(&mut transport, &response).await; - if let Err(error) = write_result { - let recorder = engine - .invocation - .service - .delivery_settlement_recorder(handshake.project_path.as_deref()) - .await; + loop { + let delivery = invocation + .as_ref() + .ok() + .and_then(|request| DaemonWorkDeliveryDescriptorV1::from_request(request, handshake)); + let request_id = invocation + .as_ref() + .ok() + .map(|request| request.request_id.clone()); + let ack_deadline = invocation + .as_ref() + .ok() + .and_then(|request| request.delivery_ack_deadline()) + .cloned(); + let session_transition = invocation + .as_ref() + .ok() + .and_then(invocation_lsp_session_transition); + let response = match invocation { + Ok(request) => execute(request).await, + Err(response) => response, + }; + update_connection_lsp_sessions( + owned_lsp_sessions, + session_transition.as_ref(), + &response, + ); + let delivery = delivery.filter(|delivery| delivery.is_successful_delivery(&response)); + // Resolve fan-out bindings before the socket response crosses + // the wire. The same immutable attempts are used for a + // Delivered or Dropped ACK; no mutable Work lookup occurs at + // terminal-ACK time. + let delivery_attempts = if let Some(delivery) = delivery { + Some( + delivery + .attempts(service, handshake.project_path.as_deref(), &response) + .await, + ) + } else { + None + }; + let write_result = write_daemon_invocation_response(transport, &response).await; + if let Err(error) = write_result { + let recorder = service + .delivery_settlement_recorder(handshake.project_path.as_deref()) + .await; + let _ = settle_daemon_work_delivery( + delivery_attempts.as_deref(), + recorder.as_ref(), + tracedecay_domain::DeliverySettlementOutcomeV1::Dropped, + Some(tracedecay_domain::DeliveryDropReasonV1::Disconnected), + ); + return Err(error); + } + if delivery_attempts.is_some() { + let recorder = service + .delivery_settlement_recorder(handshake.project_path.as_deref()) + .await; + let ack_timeout = ack_deadline + .as_ref() + .and_then(tracedecay_daemon_protocol::deadline_remaining); + let Some(ack_timeout) = ack_timeout else { + let _ = settle_daemon_work_delivery( + delivery_attempts.as_deref(), + recorder.as_ref(), + tracedecay_domain::DeliverySettlementOutcomeV1::Dropped, + Some(tracedecay_domain::DeliveryDropReasonV1::Deadline), + ); + return Ok(()); + }; + let delivery_cancellation = request_id + .as_deref() + .and_then(|request_id| service.request_cancellations().register(request_id)); + let cancellation = delivery_cancellation.as_ref().map(Lease::token); + let ack_line = match await_daemon_delivery_ack( + transport, + ack_timeout, + cancellation, + lifecycle.wait_for_draining(), + ) + .await + { + Ok(wait) => match classify_daemon_delivery_ack_wait(wait) { + Ok(line) => line, + Err(reason) => { let _ = settle_daemon_work_delivery( delivery_attempts.as_deref(), recorder.as_ref(), tracedecay_domain::DeliverySettlementOutcomeV1::Dropped, - Some(tracedecay_domain::DeliveryDropReasonV1::Disconnected), + Some(reason), ); - return Err(error); + return Ok(()); } - if delivery_attempts.is_some() { - let recorder = engine - .invocation - .service - .delivery_settlement_recorder(handshake.project_path.as_deref()) - .await; - let ack_timeout = ack_deadline - .as_ref() - .and_then(tracedecay_daemon_protocol::deadline_remaining); - let Some(ack_timeout) = ack_timeout else { - let _ = settle_daemon_work_delivery( - delivery_attempts.as_deref(), - recorder.as_ref(), - tracedecay_domain::DeliverySettlementOutcomeV1::Dropped, - Some(tracedecay_domain::DeliveryDropReasonV1::Deadline), - ); - return Ok(()); - }; - let delivery_cancellation = request_id.as_deref().and_then(|request_id| { - engine - .invocation - .service - .request_cancellations() - .register(request_id) - }); - let cancellation = delivery_cancellation - .as_ref() - .map(Lease::token); - let ack_line = match await_daemon_delivery_ack( - &mut transport, - ack_timeout, - cancellation, - engine.lifecycle.wait_for_draining(), - ) - .await - { - Ok(wait) => match classify_daemon_delivery_ack_wait(wait) { - Ok(line) => line, - Err(reason) => { - let _ = settle_daemon_work_delivery( - delivery_attempts.as_deref(), - recorder.as_ref(), - tracedecay_domain::DeliverySettlementOutcomeV1::Dropped, - Some(reason), - ); - return Ok(()); - } - }, - Err(error) => { - let _ = settle_daemon_work_delivery( - delivery_attempts.as_deref(), - recorder.as_ref(), - tracedecay_domain::DeliverySettlementOutcomeV1::Dropped, - Some(tracedecay_domain::DeliveryDropReasonV1::Disconnected), - ); - return Err(error); - } - }; - match ack_line { - Some(line) => { - let ack = tracedecay_daemon_protocol::parse_daemon_invocation_delivery_ack_request( - &line, - ); - if let Some(ack) = ack.filter(|ack| { - request_id - .as_deref() - .is_some_and(|request_id| ack.target_request_id() == request_id) - }) { - let target_request_id = ack.target_request_id().to_owned(); - let (outcome, drop_reason) = ack.outcome(); - let settlement_result = settle_daemon_work_delivery( - delivery_attempts.as_deref(), - recorder.as_ref(), - outcome, - drop_reason, - ); - let ack_response = match &settlement_result { - Ok(()) => { - tracedecay_daemon_protocol::DaemonInvocationDeliveryAckResponse::accepted( - target_request_id.clone(), - ) - } - Err(reason) => { - tracedecay_daemon_protocol::DaemonInvocationDeliveryAckResponse::rejected( - target_request_id.clone(), - *reason, - ) - } - }; - write_daemon_delivery_ack_response(&mut transport, &ack_response) - .await?; - if let Err(reason) = settlement_result { - return Err(TraceDecayError::Config { - message: format!( - "daemon could not durably record Work delivery ACK: {reason:?}" - ), - }); - } - } else { - let _ = settle_daemon_work_delivery( - delivery_attempts.as_deref(), - recorder.as_ref(), - tracedecay_domain::DeliverySettlementOutcomeV1::Dropped, - Some(tracedecay_domain::DeliveryDropReasonV1::Invalid), - ); - pending_line = Some(line); - } + }, + Err(error) => { + let _ = settle_daemon_work_delivery( + delivery_attempts.as_deref(), + recorder.as_ref(), + tracedecay_domain::DeliverySettlementOutcomeV1::Dropped, + Some(tracedecay_domain::DeliveryDropReasonV1::Disconnected), + ); + return Err(error); + } + }; + match ack_line { + Some(line) => { + let ack = + tracedecay_daemon_protocol::parse_daemon_invocation_delivery_ack_request( + &line, + ); + if let Some(ack) = ack.filter(|ack| { + request_id + .as_deref() + .is_some_and(|request_id| ack.target_request_id() == request_id) + }) { + let target_request_id = ack.target_request_id().to_owned(); + let (outcome, drop_reason) = ack.outcome(); + let settlement_result = settle_daemon_work_delivery( + delivery_attempts.as_deref(), + recorder.as_ref(), + outcome, + drop_reason, + ); + let ack_response = match &settlement_result { + Ok(()) => { + tracedecay_daemon_protocol::DaemonInvocationDeliveryAckResponse::accepted( + target_request_id.clone(), + ) } - None => { - let _ = settle_daemon_work_delivery( - delivery_attempts.as_deref(), - recorder.as_ref(), - tracedecay_domain::DeliverySettlementOutcomeV1::Dropped, - Some(tracedecay_domain::DeliveryDropReasonV1::Disconnected), - ); - return Ok(()) + Err(reason) => { + tracedecay_daemon_protocol::DaemonInvocationDeliveryAckResponse::rejected( + target_request_id.clone(), + *reason, + ) } + }; + write_daemon_delivery_ack_response(transport, &ack_response).await?; + if let Err(reason) = settlement_result { + return Err(TraceDecayError::Config { + message: format!( + "daemon could not durably record Work delivery ACK: {reason:?}" + ), + }); } - } - let next_line = if let Some(line) = pending_line.take() { - Some(line) } else { - tokio::select! { - result = read_line_handling_wire_oversized(&mut transport) => result?, - () = engine.lifecycle.wait_for_draining() => return Ok(()), - } - }; - let Some(next_line) = next_line else { - return Ok(()); - }; - let Some(next_invocation) = parse_daemon_invocation_request(&next_line) else { - return Ok(()); - }; - invocation = next_invocation; + let _ = settle_daemon_work_delivery( + delivery_attempts.as_deref(), + recorder.as_ref(), + tracedecay_domain::DeliverySettlementOutcomeV1::Dropped, + Some(tracedecay_domain::DeliveryDropReasonV1::Invalid), + ); + pending_line = Some(line); + } } - }) - .await; + None => { + let _ = settle_daemon_work_delivery( + delivery_attempts.as_deref(), + recorder.as_ref(), + tracedecay_domain::DeliverySettlementOutcomeV1::Dropped, + Some(tracedecay_domain::DeliveryDropReasonV1::Disconnected), + ); + return Ok(()); + } + } + } + let next_line = if let Some(line) = pending_line.take() { + Some(line) + } else { + tokio::select! { + result = read_line_handling_wire_oversized(transport) => result?, + () = lifecycle.wait_for_draining() => return Ok(()), + } + }; + let Some(next_line) = next_line else { + return Ok(()); + }; + let Some(next_invocation) = parse_daemon_invocation_request(&next_line) else { + return Ok(()); + }; + invocation = next_invocation; + } +} + +/// LSP sessions this connection owns are cleaned up exactly once, on every +/// exit path of the invocation loop. +#[cfg(unix)] +async fn serve_retained_invocation_connection( + invocation: std::result::Result, + mut transport: BrokerStreamTransport, + engine: DaemonEngine, + handshake: DaemonHandshake, +) -> Result<()> { + let mut owned_lsp_sessions = HashMap::new(); + let service = engine.invocation.service.clone(); + let lifecycle = engine.lifecycle.clone(); + // Keep the retained invocation loop out of the broker connection + // future's inline state. With Hotpath enabled the surrounding + // transport wrapper is polled on Tokio's ordinary worker stack; + // embedding this loop there makes construction alone exceed that + // stack before the first request can be served. + let result = boxed_broker_connection_phase(drive_retained_invocation_responses( + invocation, + &mut transport, + &service, + &handshake, + &lifecycle, + &mut owned_lsp_sessions, + |request| Box::pin(execute_daemon_invocation(&engine, &handshake, request)), + )) + .await; cleanup_connection_lsp_sessions(&engine.invocation, owned_lsp_sessions).await; result } @@ -1811,217 +1825,30 @@ pub(super) async fn serve_windows_broker_client_with_class_and_invocation( return Ok(()); } if let Some(invocation_request) = parse_daemon_invocation_request(first_request.raw()) { - let mut invocation_request = invocation_request; let mut owned_lsp_sessions = HashMap::new(); - let mut pending_line = None; - let result = async { - loop { - let delivery = invocation_request.as_ref().ok().and_then(|request| { - DaemonWorkDeliveryDescriptorV1::from_request(request, &handshake) - }); - let request_id = invocation_request - .as_ref() - .ok() - .map(|request| request.request_id.clone()); - let ack_deadline = invocation_request - .as_ref() - .ok() - .and_then(|request| request.delivery_ack_deadline()) - .cloned(); - let session_transition = invocation_request - .as_ref() - .ok() - .and_then(invocation_lsp_session_transition); - let response = match invocation_request { - Ok(request) => { - Box::pin(execute_portable_daemon_invocation( - lifecycle.clone(), - store_administration.clone(), - Arc::clone(&project_open_gates), - &handshake, - &invocation, - http_application_registry.clone(), - request, - #[cfg(test)] - project_open_attempts.clone(), - )) - .await - } - Err(response) => response, - }; - update_connection_lsp_sessions( - &mut owned_lsp_sessions, - session_transition.as_ref(), - &response, - ); - let delivery = - delivery.filter(|delivery| delivery.is_successful_delivery(&response)); - // Resolve fan-out bindings before the socket response crosses - // the wire. The same immutable attempts are used for a - // Delivered or Dropped ACK; no mutable Work lookup occurs at - // terminal-ACK time. - let delivery_attempts = if let Some(delivery) = delivery { - Some( - delivery - .attempts( - &invocation.service, - handshake.project_path.as_deref(), - &response, - ) - .await, - ) - } else { - None - }; - let write_result = - write_daemon_invocation_response(&mut transport, &response).await; - if let Err(error) = write_result { - let recorder = invocation - .service - .delivery_settlement_recorder(handshake.project_path.as_deref()) - .await; - let _ = settle_daemon_work_delivery( - delivery_attempts.as_deref(), - recorder.as_ref(), - tracedecay_domain::DeliverySettlementOutcomeV1::Dropped, - Some(tracedecay_domain::DeliveryDropReasonV1::Disconnected), - ); - return Err(error); - } - if delivery_attempts.is_some() { - let recorder = invocation - .service - .delivery_settlement_recorder(handshake.project_path.as_deref()) - .await; - let ack_timeout = ack_deadline - .as_ref() - .and_then(tracedecay_daemon_protocol::deadline_remaining); - let Some(ack_timeout) = ack_timeout else { - let _ = settle_daemon_work_delivery( - delivery_attempts.as_deref(), - recorder.as_ref(), - tracedecay_domain::DeliverySettlementOutcomeV1::Dropped, - Some(tracedecay_domain::DeliveryDropReasonV1::Deadline), - ); - return Ok(()); - }; - let delivery_cancellation = request_id.as_deref().and_then(|request_id| { - invocation - .service - .request_cancellations() - .register(request_id) - }); - let cancellation = delivery_cancellation - .as_ref() - .map(Lease::token); - let ack_line = match await_daemon_delivery_ack( - &mut transport, - ack_timeout, - cancellation, - lifecycle.wait_for_draining(), - ) - .await - { - Ok(wait) => match classify_daemon_delivery_ack_wait(wait) { - Ok(line) => line, - Err(reason) => { - let _ = settle_daemon_work_delivery( - delivery_attempts.as_deref(), - recorder.as_ref(), - tracedecay_domain::DeliverySettlementOutcomeV1::Dropped, - Some(reason), - ); - return Ok(()); - } - }, - Err(error) => { - let _ = settle_daemon_work_delivery( - delivery_attempts.as_deref(), - recorder.as_ref(), - tracedecay_domain::DeliverySettlementOutcomeV1::Dropped, - Some(tracedecay_domain::DeliveryDropReasonV1::Disconnected), - ); - return Err(error); - } - }; - match ack_line { - Some(line) => { - let ack = tracedecay_daemon_protocol::parse_daemon_invocation_delivery_ack_request( - &line, - ); - if let Some(ack) = ack.filter(|ack| { - request_id - .as_deref() - .is_some_and(|request_id| ack.target_request_id() == request_id) - }) { - let target_request_id = ack.target_request_id().to_owned(); - let (outcome, drop_reason) = ack.outcome(); - let settlement_result = settle_daemon_work_delivery( - delivery_attempts.as_deref(), - recorder.as_ref(), - outcome, - drop_reason, - ); - let ack_response = match &settlement_result { - Ok(()) => { - tracedecay_daemon_protocol::DaemonInvocationDeliveryAckResponse::accepted( - target_request_id.clone(), - ) - } - Err(reason) => { - tracedecay_daemon_protocol::DaemonInvocationDeliveryAckResponse::rejected( - target_request_id.clone(), - *reason, - ) - } - }; - write_daemon_delivery_ack_response(&mut transport, &ack_response) - .await?; - if let Err(reason) = settlement_result { - return Err(TraceDecayError::Config { - message: format!( - "daemon could not durably record Work delivery ACK: {reason:?}" - ), - }); - } - } else { - let _ = settle_daemon_work_delivery( - delivery_attempts.as_deref(), - recorder.as_ref(), - tracedecay_domain::DeliverySettlementOutcomeV1::Dropped, - Some(tracedecay_domain::DeliveryDropReasonV1::Invalid), - ); - pending_line = Some(line); - } - } - None => { - let _ = settle_daemon_work_delivery( - delivery_attempts.as_deref(), - recorder.as_ref(), - tracedecay_domain::DeliverySettlementOutcomeV1::Dropped, - Some(tracedecay_domain::DeliveryDropReasonV1::Disconnected), - ); - return Ok(()) - } - } - } - let next_line = if let Some(line) = pending_line.take() { - Some(line) - } else { - tokio::select! { - result = read_line_handling_wire_oversized(&mut transport) => result?, - () = lifecycle.wait_for_draining() => return Ok(()), - } - }; - let Some(next_line) = next_line else { - return Ok(()); - }; - let Some(next_invocation) = parse_daemon_invocation_request(&next_line) else { - return Ok(()); - }; - invocation_request = next_invocation; - } - } + let service = invocation.service.clone(); + let lifecycle = lifecycle.clone(); + let result = Box::pin(drive_retained_invocation_responses( + invocation_request, + &mut transport, + &service, + &handshake, + &lifecycle, + &mut owned_lsp_sessions, + |request| { + Box::pin(execute_portable_daemon_invocation( + lifecycle.clone(), + store_administration.clone(), + Arc::clone(&project_open_gates), + &handshake, + &invocation, + http_application_registry.clone(), + request, + #[cfg(test)] + project_open_attempts.clone(), + )) + }, + )) .await; cleanup_connection_lsp_sessions(&invocation, owned_lsp_sessions).await; return result; From ac649b36b5f25ac7bb7c9ea8315a701a5c56c928 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sun, 20 Sep 2026 19:28:04 +0000 Subject: [PATCH 113/182] fix(dist): match the test-API probe refusal by error code rustc 1.97 words the refusal "no associated function or constant named", and the probe step grepped for the older "no function or associated item named", so a correct refusal read as an unexpected failure with its stderr discarded. Match the error code and the probed name, and print the stderr when the match still fails. Co-Authored-By: Claude Fable 5.1 --- scripts/check-distribution-acceptance.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/check-distribution-acceptance.sh b/scripts/check-distribution-acceptance.sh index acfe010b6c..8962298f25 100755 --- a/scripts/check-distribution-acceptance.sh +++ b/scripts/check-distribution-acceptance.sh @@ -917,9 +917,15 @@ if CARGO_NET_OFFLINE=true cargo check \ 2>"$test_api_stderr"; then die "production package exposed test-transport APIs" fi -grep -Eq "no function or associated item named .*has_project_session_retrieval_service_for_test" \ - "$test_api_stderr" || +# rustc words this refusal differently across releases ("no function or +# associated item named" before 1.97, "no associated function or constant +# named" from 1.97), so match the error code and the probed name. +grep -Eq "error\[E0599\].*has_project_session_retrieval_service_for_test" \ + "$test_api_stderr" || { + echo "distribution acceptance: test API probe stderr follows" >&2 + tail -n 60 -- "$test_api_stderr" >&2 die "test API probe failed for an unexpected reason" +} binary=$(python3 "$repo/scripts/resolve-installed-binary.py" \ "$install_root" \ From 149dd838ca4fa5039fec20b6b970791a97b523ae Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:30:34 +0000 Subject: [PATCH 114/182] simplify(pass-2/5): share invocation cancellation leases Co-authored-by: Zack Jackson --- .../src/daemon/invocation_dispatch.rs | 123 +++++++++--------- 1 file changed, 63 insertions(+), 60 deletions(-) diff --git a/crates/tracedecay/src/daemon/invocation_dispatch.rs b/crates/tracedecay/src/daemon/invocation_dispatch.rs index cce0886799..812804b335 100644 --- a/crates/tracedecay/src/daemon/invocation_dispatch.rs +++ b/crates/tracedecay/src/daemon/invocation_dispatch.rs @@ -178,6 +178,43 @@ fn scope_set_cas_admission( Some((request, *observed_at, deadline, cancellation)) } +struct InvocationCancellation { + request_id: String, + scope_set_cas_lease: Option, + lsp_lease: Option, + lsp_cancellation: Option, + request_cancellation: Option, +} + +fn invocation_cancellation( + request: &DaemonInvocationRequest, + service: &DaemonInvocationService, +) -> Option { + let request_id = request.request_id.clone(); + let request_cancellations = service.request_cancellations(); + let scope_set_cas_lease = if scope_set_cas_admission(request).is_some() { + Some(request_cancellations.register(&request_id)?) + } else { + None + }; + let lsp_lease = if request.operation() == DaemonInvocationOperation::LspOpen { + Some(request_cancellations.register(&request_id)?) + } else { + None + }; + let lsp_cancellation = lsp_lease.as_ref().map(Lease::token); + let request_cancellation = lsp_cancellation + .clone() + .or_else(|| scope_set_cas_lease.as_ref().map(Lease::token)); + Some(InvocationCancellation { + request_id, + scope_set_cas_lease, + lsp_lease, + lsp_cancellation, + request_cancellation, + }) +} + fn selected_root_handshake(handshake: &DaemonHandshake, root: &Path) -> DaemonHandshake { DaemonHandshake { project_path: Some(root.to_path_buf()), @@ -312,38 +349,21 @@ pub(super) async fn execute_portable_daemon_invocation( if let Some(response) = invalid_multi_root_invocation_response(&request) { return response; } - let request_id = request.request_id.clone(); - let request_cancellations = invocation.service.request_cancellations(); - let scope_set_cas_cancellation_lease = if scope_set_cas_admission(&request).is_some() { - match request_cancellations.register(&request_id) { - Some(lease) => Some(lease), - None => { - return DaemonInvocationResponse::problem( - request_id, - DaemonInvocationProblem::InvalidRequest, - ); - } - } - } else { - None - }; - let lsp_cancellation_lease = if request.operation() == DaemonInvocationOperation::LspOpen { - match request_cancellations.register(&request_id) { - Some(lease) => Some(lease), - None => { - return DaemonInvocationResponse::problem( - request_id, - DaemonInvocationProblem::InvalidRequest, - ); - } + let InvocationCancellation { + request_id, + scope_set_cas_lease: _scope_set_cas_lease, + lsp_lease: _lsp_lease, + lsp_cancellation, + request_cancellation, + } = match invocation_cancellation(&request, &invocation.service) { + Some(cancellation) => cancellation, + None => { + return DaemonInvocationResponse::problem( + request.request_id.clone(), + DaemonInvocationProblem::InvalidRequest, + ); } - } else { - None }; - let lsp_cancellation = lsp_cancellation_lease.as_ref().map(Lease::token); - let request_cancellation = lsp_cancellation - .clone() - .or_else(|| scope_set_cas_cancellation_lease.as_ref().map(Lease::token)); let lsp_project_open_gates = Arc::clone(&project_open_gates); #[cfg(test)] let lsp_project_open_attempts = project_open_attempts.clone(); @@ -694,38 +714,21 @@ pub(super) async fn execute_daemon_invocation( if let Some(response) = invalid_multi_root_invocation_response(&request) { return response; } - let request_id = request.request_id.clone(); - let request_cancellations = engine.invocation.service.request_cancellations(); - let scope_set_cas_cancellation_lease = if scope_set_cas_admission(&request).is_some() { - match request_cancellations.register(&request_id) { - Some(lease) => Some(lease), - None => { - return DaemonInvocationResponse::problem( - request_id, - DaemonInvocationProblem::InvalidRequest, - ); - } - } - } else { - None - }; - let lsp_cancellation_lease = if request.operation() == DaemonInvocationOperation::LspOpen { - match request_cancellations.register(&request_id) { - Some(lease) => Some(lease), - None => { - return DaemonInvocationResponse::problem( - request_id, - DaemonInvocationProblem::InvalidRequest, - ); - } + let InvocationCancellation { + request_id, + scope_set_cas_lease: _scope_set_cas_lease, + lsp_lease: _lsp_lease, + lsp_cancellation, + request_cancellation, + } = match invocation_cancellation(&request, &engine.invocation.service) { + Some(cancellation) => cancellation, + None => { + return DaemonInvocationResponse::problem( + request.request_id.clone(), + DaemonInvocationProblem::InvalidRequest, + ); } - } else { - None }; - let lsp_cancellation = lsp_cancellation_lease.as_ref().map(Lease::token); - let request_cancellation = lsp_cancellation - .clone() - .or_else(|| scope_set_cas_cancellation_lease.as_ref().map(Lease::token)); let git_operation = invocation_is_git_operation(request.operation()); let workflow_application = request.is_workflow_application(); let mut project_path = None; From 031b8d2ddde58983b0fe621ec4b8de42d8d7b277 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:31:20 +0000 Subject: [PATCH 115/182] simplify(pass-3/5): share retirement shutdown receipt Co-authored-by: Zack Jackson --- .../daemon/branch_admin/project_retirement.rs | 58 +++++++++---------- 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/crates/tracedecay/src/daemon/branch_admin/project_retirement.rs b/crates/tracedecay/src/daemon/branch_admin/project_retirement.rs index bf33c8f44c..46fd8d9c5b 100644 --- a/crates/tracedecay/src/daemon/branch_admin/project_retirement.rs +++ b/crates/tracedecay/src/daemon/branch_admin/project_retirement.rs @@ -313,6 +313,30 @@ pub(super) async fn track_aborted_retirement_task( track_project_server_retirement(retirements, owner, task, true).await; } +async fn shutdown_receipt( + completions: Vec<( + String, + tokio::sync::watch::Receiver, + )>, + deadline: tokio::time::Instant, +) -> ShutdownTaskReceipt { + let mut receipt = ShutdownTaskReceipt::default(); + for (owner, completion) in completions { + let status = + match tokio::time::timeout_at(deadline, wait_for_project_server_retirement(completion)) + .await + { + Ok(ProjectServerRetirementStatus::Clean) => ShutdownTaskStatus::Clean, + Ok(ProjectServerRetirementStatus::Failed(error)) => { + ShutdownTaskStatus::Failed(error) + } + Ok(ProjectServerRetirementStatus::Pending) | Err(_) => ShutdownTaskStatus::TimedOut, + }; + receipt.outcomes.push(ShutdownTaskOutcome { owner, status }); + } + receipt +} + pub(super) async fn settle_project_retirements( retirements: &tokio::sync::Mutex>, profile_root: &std::path::Path, @@ -334,21 +358,7 @@ pub(super) async fn settle_project_retirements( ) }) .collect::>(); - let mut receipt = ShutdownTaskReceipt::default(); - for (owner, completion) in completions { - let status = - match tokio::time::timeout_at(deadline, wait_for_project_server_retirement(completion)) - .await - { - Ok(ProjectServerRetirementStatus::Clean) => ShutdownTaskStatus::Clean, - Ok(ProjectServerRetirementStatus::Failed(error)) => { - ShutdownTaskStatus::Failed(error) - } - Ok(ProjectServerRetirementStatus::Pending) => ShutdownTaskStatus::TimedOut, - Err(_) => ShutdownTaskStatus::TimedOut, - }; - receipt.outcomes.push(ShutdownTaskOutcome { owner, status }); - } + let receipt = shutdown_receipt(completions, deadline).await; retirements.lock().await.retain(|retirement| { !matches!( &*retirement.completion.borrow(), @@ -449,23 +459,7 @@ impl StoreAdministration { return ShutdownTaskReceipt::timed_out("project_server_retirement_registry"); } }; - let mut receipt = ShutdownTaskReceipt::default(); - for (owner, completion) in completions { - let status = match tokio::time::timeout_at( - deadline, - wait_for_project_server_retirement(completion), - ) - .await - { - Ok(ProjectServerRetirementStatus::Clean) => ShutdownTaskStatus::Clean, - Ok(ProjectServerRetirementStatus::Failed(error)) => { - ShutdownTaskStatus::Failed(error) - } - Ok(ProjectServerRetirementStatus::Pending) => ShutdownTaskStatus::TimedOut, - Err(_) => ShutdownTaskStatus::TimedOut, - }; - receipt.outcomes.push(ShutdownTaskOutcome { owner, status }); - } + let receipt = shutdown_receipt(completions, deadline).await; if let Ok(mut retirements) = tokio::time::timeout_at(deadline, self.project_server_retirements.lock()).await { From 384897758921b51bf0f30115b15b5ccb68aeb58f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:31:23 +0000 Subject: [PATCH 116/182] refactor(pass-1/5): drop session-evidence reexport The module only forwarded LcmGrepHit, LcmGrepSort, and LcmScope. Callers now import those types from tracedecay-lcm. Co-authored-by: Zack Jackson --- .../src/automation/effect_runtime/input.rs | 2 +- .../src/automation/runner/evidence.rs | 2 +- .../src/automation/runner/retrieval.rs | 2 +- .../automation/runner/session_reflector.rs | 2 +- .../src/automation/runner/tests.rs | 2 +- .../src/automation/skill_writer.rs | 2 +- .../src/ports.rs | 1 - .../src/ports/session_evidence.rs | 21 ------------------- .../session_reflector.rs | 2 +- 9 files changed, 7 insertions(+), 29 deletions(-) delete mode 100644 crates/tracedecay-automation-runtime/src/ports/session_evidence.rs diff --git a/crates/tracedecay-automation-runtime/src/automation/effect_runtime/input.rs b/crates/tracedecay-automation-runtime/src/automation/effect_runtime/input.rs index 07422d6f0a..bfdff1887f 100644 --- a/crates/tracedecay-automation-runtime/src/automation/effect_runtime/input.rs +++ b/crates/tracedecay-automation-runtime/src/automation/effect_runtime/input.rs @@ -7,7 +7,7 @@ use tracedecay_contracts::retained_surfaces::{ use tracedecay_domain::{RunId, UtcMicros}; use crate::automation::runner::{SessionReflectorAutomationOptions, SkillWriterAutomationOptions}; -use crate::ports::session_evidence::{LcmGrepSort, LcmScope}; +use tracedecay_lcm::{LcmGrepSort, LcmScope}; use super::contract::contract_error; use tracedecay_domain::errors::Result; diff --git a/crates/tracedecay-automation-runtime/src/automation/runner/evidence.rs b/crates/tracedecay-automation-runtime/src/automation/runner/evidence.rs index 6454ce9d45..a2cf737cd2 100644 --- a/crates/tracedecay-automation-runtime/src/automation/runner/evidence.rs +++ b/crates/tracedecay-automation-runtime/src/automation/runner/evidence.rs @@ -2,7 +2,7 @@ use serde::Serialize; use serde_json::{Value, json}; use tracedecay_domain::TemporalCoverageCountsV1; -use crate::ports::session_evidence::{LcmGrepHit, LcmGrepSort, LcmScope}; +use tracedecay_lcm::{LcmGrepHit, LcmGrepSort, LcmScope}; use crate::automation::artifacts::sha256_json; use crate::automation::managed_skills::list_managed_skills; diff --git a/crates/tracedecay-automation-runtime/src/automation/runner/retrieval.rs b/crates/tracedecay-automation-runtime/src/automation/runner/retrieval.rs index e00704d211..7734790d5f 100644 --- a/crates/tracedecay-automation-runtime/src/automation/runner/retrieval.rs +++ b/crates/tracedecay-automation-runtime/src/automation/runner/retrieval.rs @@ -26,10 +26,10 @@ use tracedecay_domain::{ use tracedecay_store::{StoreShardIdV1, StoreShardScopeV1}; use tracedecay_tool_catalog::{CapabilityId, UseCaseId}; -use crate::ports::session_evidence::LcmScope; use tracedecay_contracts::request_identity::{GlobalRequestSurface, mint_global_request_id}; use tracedecay_domain::errors::{Result, TraceDecayError}; use tracedecay_global_db::{RegisteredGlobalDb, RegisteredGlobalDbLeaseV1}; +use tracedecay_lcm::LcmScope; use tracedecay_session_memory::context::{ BranchId, CancellationToken, CapabilityDigest, ConfigurationDigest, PolicyDigest, ProfileId, RequestBudgets, ResolvedGitRoute, ResolvedSessionIdentity, SessionRootId, SessionStoreId, diff --git a/crates/tracedecay-automation-runtime/src/automation/runner/session_reflector.rs b/crates/tracedecay-automation-runtime/src/automation/runner/session_reflector.rs index a0ea312214..6e15f2f782 100644 --- a/crates/tracedecay-automation-runtime/src/automation/runner/session_reflector.rs +++ b/crates/tracedecay-automation-runtime/src/automation/runner/session_reflector.rs @@ -21,11 +21,11 @@ use crate::automation::lifecycle::{ use crate::automation::run_ledger::{AutomationRunLedgerRecord, AutomationTrigger}; use crate::automation::session_reflector::validate_fact_candidates; use crate::ports::project_runtime::AutomationProjectContext; -use crate::ports::session_evidence::{LcmGrepSort, LcmScope}; use tracedecay_domain::FactOwnerV1; use tracedecay_domain::configuration::ConfigurationRevisionId; use tracedecay_domain::errors::{Result, TraceDecayError}; use tracedecay_global_db::RegisteredGlobalDbLeaseV1; +use tracedecay_lcm::{LcmGrepSort, LcmScope}; use tracedecay_runtime_core::tracedecay::current_timestamp; use tracedecay_session_memory::fact_store::DatabaseFactStore; use tracedecay_session_memory::memory::MemoryApplication; diff --git a/crates/tracedecay-automation-runtime/src/automation/runner/tests.rs b/crates/tracedecay-automation-runtime/src/automation/runner/tests.rs index fb0846c901..1d4306f203 100644 --- a/crates/tracedecay-automation-runtime/src/automation/runner/tests.rs +++ b/crates/tracedecay-automation-runtime/src/automation/runner/tests.rs @@ -52,7 +52,7 @@ use super::{ combined_skill_writer_evidence_or_not_combined, split_skill_runtime_failure, validate_session_fact_candidates, }; -use crate::ports::session_evidence::{LcmGrepSort, LcmScope}; +use tracedecay_lcm::{LcmGrepSort, LcmScope}; use tracedecay_runtime_core::db::{Database, DatabaseAuthority, TestDatabaseRuntimeMode}; use tracedecay_session_memory::fact_store::DatabaseFactStore; diff --git a/crates/tracedecay-automation-runtime/src/automation/skill_writer.rs b/crates/tracedecay-automation-runtime/src/automation/skill_writer.rs index ca60764ef0..81a8a9eb5a 100644 --- a/crates/tracedecay-automation-runtime/src/automation/skill_writer.rs +++ b/crates/tracedecay-automation-runtime/src/automation/skill_writer.rs @@ -16,11 +16,11 @@ use super::skill_usage::{ SkillOverlapCandidate, SkillStaleRecommendation, SkillUsageSummary, skill_improvement_recommendations as usage_skill_improvement_recommendations, }; -use crate::ports::session_evidence::LcmGrepHit; use tracedecay_automation::analytics::ToolFamilySignal; use tracedecay_automation::managed_skills::validate_managed_skill_update; use tracedecay_automation::text::truncate_chars_for_prompt; use tracedecay_domain::errors::Result; +use tracedecay_lcm::LcmGrepHit; use super::config_error; diff --git a/crates/tracedecay-automation-runtime/src/ports.rs b/crates/tracedecay-automation-runtime/src/ports.rs index d48e7e2cdd..8f2a2d0c2d 100644 --- a/crates/tracedecay-automation-runtime/src/ports.rs +++ b/crates/tracedecay-automation-runtime/src/ports.rs @@ -10,5 +10,4 @@ pub mod codex_app_server; pub mod project_runtime; -pub mod session_evidence; pub mod session_store; diff --git a/crates/tracedecay-automation-runtime/src/ports/session_evidence.rs b/crates/tracedecay-automation-runtime/src/ports/session_evidence.rs deleted file mode 100644 index e28d0983c8..0000000000 --- a/crates/tracedecay-automation-runtime/src/ports/session_evidence.rs +++ /dev/null @@ -1,21 +0,0 @@ -//! The session-evidence contract automation states its queries in. -//! -//! `automation::runner` builds evidence for the memory curator, session -//! reflector, and skill writer by grepping stored session transcripts. The -//! LCM query engine that answers those greps lives in `tracedecay-lcm` -//! behind a runtime this crate must not open for itself, so the request -//! selectors and the hit shape are declared here and the execution arrives -//! through `runner::retrieval`'s `AutomationSessionRetrieval` port. -//! -//! These deliberately mirror `tracedecay_lcm`'s selectors rather than reusing -//! them: this crate states *what evidence automation wants*, and the session -//! runtime decides how to satisfy it. The serde representations match, so the -//! root adapter is a field-for-field conversion. -//! -//! Root wiring: the root converts between these and -//! `tracedecay_lcm::{LcmScope, LcmGrepSort, LcmGrepHit}` in the adapter it -//! registers as `AutomationSessionRetrieval`. - -/// Canonical definitions live in `tracedecay_lcm::types`; -/// re-exported here so this crate's port keeps its historical path. -pub use tracedecay_lcm::types::{LcmGrepHit, LcmGrepSort, LcmScope}; diff --git a/crates/tracedecay/tests/automation_runner_test/session_reflector.rs b/crates/tracedecay/tests/automation_runner_test/session_reflector.rs index 24179d4773..ec12495410 100644 --- a/crates/tracedecay/tests/automation_runner_test/session_reflector.rs +++ b/crates/tracedecay/tests/automation_runner_test/session_reflector.rs @@ -46,7 +46,7 @@ impl AutomationSessionRetrieval for StructuralBudgetRefusalRetrieval { #[cfg(feature = "test-transport")] use std::sync::atomic::Ordering; #[cfg(feature = "test-transport")] -use tracedecay_automation_runtime::ports::session_evidence::{LcmGrepSort, LcmScope}; +use tracedecay_lcm::{LcmGrepSort, LcmScope}; #[cfg(feature = "test-transport")] use tracedecay_store::{ProjectMemoryFactSearchKindV1, ProjectMemoryFactSearchQuery}; From 44bb3b27fdf9a69648acd74b66fd1f7a72e1b6ea Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:32:55 +0000 Subject: [PATCH 117/182] simplify(pass-1/5): share copied saturating micros stamps Co-authored-by: Zack Jackson --- commitlint.config.cjs | 1 + .../src/hooks/daemon_ports.rs | 9 ++---- .../src/graph_health_delta.rs | 11 +------- .../src/handlers/hook_runtime/envelope.rs | 9 +----- .../src/project/lifecycle/branches.rs | 8 +----- .../src/project/lifecycle/mod.rs | 16 ++--------- .../tracedecay-runtime-core/src/tracedecay.rs | 28 +++++++++++++------ .../wake.rs | 8 +----- .../src/daemon/hook_v2_replay_consumer.rs | 9 +----- 9 files changed, 30 insertions(+), 69 deletions(-) diff --git a/commitlint.config.cjs b/commitlint.config.cjs index 18b34ae514..f4494bb9eb 100644 --- a/commitlint.config.cjs +++ b/commitlint.config.cjs @@ -9,6 +9,7 @@ const allowedTypes = [ "refactor", "revert", "style", + "simplify", "test", ]; diff --git a/crates/tracedecay-agent-hosts/src/hooks/daemon_ports.rs b/crates/tracedecay-agent-hosts/src/hooks/daemon_ports.rs index 37a593654d..f6f95134a7 100644 --- a/crates/tracedecay-agent-hosts/src/hooks/daemon_ports.rs +++ b/crates/tracedecay-agent-hosts/src/hooks/daemon_ports.rs @@ -7,7 +7,7 @@ use std::path::Path; use std::sync::Mutex; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use std::time::Duration; use serde::Deserialize; use tracedecay_contracts::context_scout::{ContextScoutAddressV1, ContextScoutDeliveryReceiptV1}; @@ -123,12 +123,7 @@ struct DaemonAdmissionResponseWireV1 { } pub(crate) fn now_utc() -> UtcMicros { - let micros = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map_or(1, |duration| { - duration.as_micros().min(i64::MAX as u128) as i64 - }); - UtcMicros(micros.max(1)) + UtcMicros(tracedecay_runtime_core::tracedecay::saturating_utc_now().0.max(1)) } #[hotpath::measure(label = "agent_hosts.hook_ports.admission_decode")] diff --git a/crates/tracedecay-application/src/graph_health_delta.rs b/crates/tracedecay-application/src/graph_health_delta.rs index 9fbc1e8679..fd5879957d 100644 --- a/crates/tracedecay-application/src/graph_health_delta.rs +++ b/crates/tracedecay-application/src/graph_health_delta.rs @@ -33,15 +33,6 @@ enum PersistHealthDeltaError { Other(TraceDecayError), } -fn health_delta_now() -> UtcMicros { - let micros = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map_or(0, |duration| { - duration.as_micros().min(i64::MAX as u128) as i64 - }); - UtcMicros(micros) -} - fn health_score_ppm(value: f64) -> u64 { (value.clamp(0.0, 1.0) * 1_000_000.0).round() as u64 } @@ -418,7 +409,7 @@ pub async fn compute_verified_health_delta( label = "usecases.graph.health_delta.snapshot" ) .await?; - let observed_at = health_delta_now(); + let observed_at = tracedecay_contracts::now_micros(); let dimensions = health_delta_dimensions(&snapshot); let watermark = health_delta_watermark( &scope, diff --git a/crates/tracedecay-mcp/src/handlers/hook_runtime/envelope.rs b/crates/tracedecay-mcp/src/handlers/hook_runtime/envelope.rs index dfacf3af8b..8ff918f0b7 100644 --- a/crates/tracedecay-mcp/src/handlers/hook_runtime/envelope.rs +++ b/crates/tracedecay-mcp/src/handlers/hook_runtime/envelope.rs @@ -1,18 +1,11 @@ use serde_json::Value; use sha2::{Digest, Sha256}; -use std::time::{SystemTime, UNIX_EPOCH}; use tracedecay_automation_runtime::automation::config_error; use tracedecay_domain::errors::Result; use tracedecay_domain::{ObservationSourceRangeV1, SessionId, UtcMicros}; pub(super) fn hook_now() -> UtcMicros { - UtcMicros( - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map_or(1, |duration| { - duration.as_micros().min(i64::MAX as u128) as i64 - }), - ) + tracedecay_runtime_core::tracedecay::utc_now_or_one() } pub(super) fn hook_v2_envelope( diff --git a/crates/tracedecay-project/src/project/lifecycle/branches.rs b/crates/tracedecay-project/src/project/lifecycle/branches.rs index cb36d409fd..f8ad8e4a7e 100644 --- a/crates/tracedecay-project/src/project/lifecycle/branches.rs +++ b/crates/tracedecay-project/src/project/lifecycle/branches.rs @@ -260,13 +260,7 @@ impl TraceDecay { let _ = tracedecay_agent_hosts::agents::context_scout::owner::ProjectContextScoutOwnerV1::startup( graph.db.clone(), project_id, - tracedecay_domain::UtcMicros( - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map_or(1, |duration| { - duration.as_micros().min(i64::MAX as u128) as i64 - }), - ), + tracedecay_runtime_core::tracedecay::utc_now_or_one(), None, ) .await; diff --git a/crates/tracedecay-project/src/project/lifecycle/mod.rs b/crates/tracedecay-project/src/project/lifecycle/mod.rs index 5812e816f5..5632823191 100644 --- a/crates/tracedecay-project/src/project/lifecycle/mod.rs +++ b/crates/tracedecay-project/src/project/lifecycle/mod.rs @@ -433,13 +433,7 @@ impl TraceDecay { let _ = tracedecay_agent_hosts::agents::context_scout::owner::ProjectContextScoutOwnerV1::startup( ts.db.clone(), project_id, - tracedecay_domain::UtcMicros( - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map_or(1, |duration| { - duration.as_micros().min(i64::MAX as u128) as i64 - }), - ), + tracedecay_runtime_core::tracedecay::utc_now_or_one(), None, ) .await; @@ -660,13 +654,7 @@ impl TraceDecay { let _ = tracedecay_agent_hosts::agents::context_scout::owner::ProjectContextScoutOwnerV1::startup( ts.db.clone(), project_id, - tracedecay_domain::UtcMicros( - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map_or(1, |duration| { - duration.as_micros().min(i64::MAX as u128) as i64 - }), - ), + tracedecay_runtime_core::tracedecay::utc_now_or_one(), None, ) .await; diff --git a/crates/tracedecay-runtime-core/src/tracedecay.rs b/crates/tracedecay-runtime-core/src/tracedecay.rs index c73d2c735e..e3889ed09e 100644 --- a/crates/tracedecay-runtime-core/src/tracedecay.rs +++ b/crates/tracedecay-runtime-core/src/tracedecay.rs @@ -1,12 +1,12 @@ //! Kernel-owned slice of the root `tracedecay` orchestrator module. //! -//! Wall-clock stamps for this crate share [`wall_clock_since_epoch`]: a -//! pre-epoch clock saturates to a zero duration. Microsecond stamps then -//! saturate overflow to `i64::MAX`; second stamps keep the prior `as i64` -//! conversion. This crate cannot depend on `tracedecay_contracts::clock` -//! (that crate is the ports/contracts layer; taking it would pull policy, -//! tool-catalog, and schemars into a kernel that currently has no -//! application edge). +//! Wall-clock stamps for this crate share one read. A pre-epoch clock saturates +//! to a zero duration, except [`utc_now_or_one`], which reads as `1` so a failed +//! stamp stays distinct from an absent `UtcMicros(0)`. Microsecond overflow +//! saturates to `i64::MAX`; second stamps keep the prior `as i64` conversion. +//! This crate cannot depend on `tracedecay_contracts::clock` (that crate is the +//! ports/contracts layer; taking it would pull policy, tool-catalog, and +//! schemars into a kernel that currently has no application edge). use std::time::{Duration, SystemTime, UNIX_EPOCH}; @@ -30,5 +30,17 @@ pub fn current_timestamp() -> i64 { /// `i64::MAX`. This is the kernel-local equivalent of /// `tracedecay_contracts::clock::now_micros`. pub fn saturating_utc_now() -> UtcMicros { - UtcMicros(i64::try_from(wall_clock_since_epoch().as_micros()).unwrap_or(i64::MAX)) + UtcMicros(unix_micros_saturating(0)) +} + +/// Saturating microseconds since the epoch. A pre-epoch clock reads as `1`. +pub fn utc_now_or_one() -> UtcMicros { + UtcMicros(unix_micros_saturating(1)) +} + +fn unix_micros_saturating(pre_epoch: i64) -> i64 { + match SystemTime::now().duration_since(UNIX_EPOCH) { + Ok(duration) => i64::try_from(duration.as_micros()).unwrap_or(i64::MAX), + Err(_) => pre_epoch, + } } diff --git a/crates/tracedecay-session-runtime/src/session_temporal_refresh_scheduler/wake.rs b/crates/tracedecay-session-runtime/src/session_temporal_refresh_scheduler/wake.rs index fc7f3bab35..aa243d5e05 100644 --- a/crates/tracedecay-session-runtime/src/session_temporal_refresh_scheduler/wake.rs +++ b/crates/tracedecay-session-runtime/src/session_temporal_refresh_scheduler/wake.rs @@ -2,8 +2,6 @@ use std::collections::{HashMap, HashSet, VecDeque}; use std::sync::Arc; use std::sync::PoisonError; use std::sync::atomic::{AtomicBool, Ordering}; -use std::time::{SystemTime, UNIX_EPOCH}; - use tracedecay_domain::SessionId; use tracedecay_store::SessionRefreshBeginOrJoinRequestV1; use tracedecay_temporal_query::ports::ExecutionControl; @@ -542,11 +540,7 @@ impl SessionTemporalRefreshWakeState { telemetry.durable_backlog = durable_backlog; telemetry.last_pass_made_progress = made_progress; if made_progress { - let micros = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_micros() - .min(i64::MAX as u128) as i64; + let micros = tracedecay_runtime_core::tracedecay::saturating_utc_now().0; telemetry.last_progress_at_unix_micros = Some(micros); } if telemetry.depths_published { diff --git a/crates/tracedecay/src/daemon/hook_v2_replay_consumer.rs b/crates/tracedecay/src/daemon/hook_v2_replay_consumer.rs index 65ad57b5bd..a921594c90 100644 --- a/crates/tracedecay/src/daemon/hook_v2_replay_consumer.rs +++ b/crates/tracedecay/src/daemon/hook_v2_replay_consumer.rs @@ -246,14 +246,7 @@ async fn drain_admitted_host_spool( } fn hook_replay_now() -> UtcMicros { - UtcMicros( - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map_or(1, |duration| { - duration.as_micros().min(i64::MAX as u128) as i64 - }) - .max(1), - ) + UtcMicros(tracedecay_runtime_core::tracedecay::saturating_utc_now().0.max(1)) } struct RegisteredReplayConsumer { From 2d7d2f371046f74042481a740b546ba1dd1d8028 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:33:09 +0000 Subject: [PATCH 118/182] refactor(projection): share claude derive path Co-authored-by: Zack Jackson --- .../src/observation_projection/apply.rs | 84 +------------------ .../src/lib.rs | 1 + .../src/support.rs | 4 +- 3 files changed, 5 insertions(+), 84 deletions(-) diff --git a/crates/tracedecay-global-db/src/observation_projection/apply.rs b/crates/tracedecay-global-db/src/observation_projection/apply.rs index 5bcf7aabf8..f27ca237b5 100644 --- a/crates/tracedecay-global-db/src/observation_projection/apply.rs +++ b/crates/tracedecay-global-db/src/observation_projection/apply.rs @@ -10,14 +10,11 @@ use tracedecay_store::{ ObservationProjection, PROVIDER_USAGE_PROJECTOR_VERSION, ProjectionSkipReason, ProjectionStoreError, ProjectionStoreResult, SESSION_MESSAGE_PROJECTOR_VERSION, SessionMessageProjection, SessionMessageRecord, SessionRecord, WorkflowFactProjection, - WorkflowFactRecord, derive_canonical_projection, workflow_semantic_kind, + WorkflowFactRecord, workflow_semantic_kind, }; use tracedecay_lcm::contracts::LcmError; use tracedecay_runtime_core::db::engine::{Executor, QueryExecutor, params}; -use tracedecay_sessions::runtime::claude::{ - ClaudeRecordContext, ClaudeRecordDisposition, map_sanitized_claude_record, -}; use tracedecay_sessions::runtime::store_access::find_preceding_codex_goal_response; use super::state::{ @@ -39,84 +36,7 @@ fn decode_canonical_envelope( pub(in super::super) fn derive_projection( observation: &DurableObservationV1, ) -> ProjectionStoreResult { - match observation.source().provider().as_str() { - "claude" if decode_canonical_envelope(observation.payload()).is_ok() => { - derive_canonical_projection(observation) - } - "claude" => derive_claude_projection(observation), - _ => derive_canonical_projection(observation), - } -} - -fn derive_claude_projection( - observation: &DurableObservationV1, -) -> ProjectionStoreResult { - let session_id = observation.source().session_id().as_str(); - let payload = observation.payload(); - let durable_message_id = payload - .pointer("/message/id") - .and_then(serde_json::Value::as_str) - .or_else(|| payload.get("uuid").and_then(serde_json::Value::as_str)) - .filter(|id| !id.is_empty()); - let durable_tool_event_ids = payload - .pointer("/message/content") - .and_then(serde_json::Value::as_array) - .into_iter() - .flatten() - .filter_map(|item| { - item.get("id") - .or_else(|| item.get("tool_use_id")) - .and_then(serde_json::Value::as_str) - .filter(|id| !id.is_empty()) - .map(str::to_owned) - }) - .collect::>(); - let (project_key, project_path) = match observation.scope() { - ObservationScopeV1::Profile => ("user", "user"), - ObservationScopeV1::Project { project_id } => (project_id.as_str(), project_id.as_str()), - }; - let source_path = (observation.source().source_key() != observation.source().session_id()) - .then(|| observation.source().source_key().as_str()); - let context = ClaudeRecordContext { - session_id, - project_key, - project_path, - file_generation: observation.identity().generation().file_id(), - offset: observation.identity().position().start(), - session_cwd: None, - source_path, - raw_message_id: durable_message_id, - raw_tool_event_ids: &durable_tool_event_ids, - raw_hook_tool_use_id: None, - }; - - match map_sanitized_claude_record(payload, &context) { - ClaudeRecordDisposition::Message { draft, message } => { - let draft = *draft; - let message = *message; - let timestamp = message.timestamp; - let session = SessionRecord { - provider: "claude".to_string(), - session_id: draft.session_id, - project_key: draft.project_key, - project_path: draft.project_path, - title: draft.title, - started_at: timestamp, - ended_at: timestamp, - transcript_path: None, - metadata_json: draft.metadata_json, - parent_session_id: draft.parent_session_id, - is_subagent: draft.is_subagent, - agent_id: draft.agent_id, - parent_tool_use_id: draft.parent_tool_use_id, - }; - ObservationProjection::for_message(observation, session, message) - } - ClaudeRecordDisposition::NonConversational => ObservationProjection::for_skip( - observation, - ProjectionSkipReason::NonConversationalRecord, - ), - } + tracedecay_session_temporal_store::derive_projection(observation) } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/crates/tracedecay-session-temporal-store/src/lib.rs b/crates/tracedecay-session-temporal-store/src/lib.rs index 7da50f883f..2a85858386 100644 --- a/crates/tracedecay-session-temporal-store/src/lib.rs +++ b/crates/tracedecay-session-temporal-store/src/lib.rs @@ -11,6 +11,7 @@ mod handle; mod schema_constants; mod support; +pub use support::derive_projection; #[cfg(test)] mod test_registered_impls; #[cfg(test)] diff --git a/crates/tracedecay-session-temporal-store/src/support.rs b/crates/tracedecay-session-temporal-store/src/support.rs index 1025f2b120..d765ab9357 100644 --- a/crates/tracedecay-session-temporal-store/src/support.rs +++ b/crates/tracedecay-session-temporal-store/src/support.rs @@ -53,8 +53,8 @@ pub(crate) fn record_hydration_emitted_bytes(count: usize) { /// Same composition as the observation-projection derive path: canonical /// envelopes go through store authority; legacy Claude records use the public -/// sessions mapper. Kept here so this crate does not depend on global-db. -pub(crate) fn derive_projection( +/// sessions mapper. Global-db calls this so the two projections cannot drift. +pub fn derive_projection( observation: &DurableObservationV1, ) -> ProjectionStoreResult { match observation.source().provider().as_str() { From 77288a6bd3b4901a96d2d2cdcfd828e330902bed Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:33:27 +0000 Subject: [PATCH 119/182] refactor(pass-2/5): drop LCM render reexport The module only glob-re-exported hydration shaping from tracedecay-session-temporal-store, and nothing imported it. Co-authored-by: Zack Jackson --- .../tracedecay-session-memory/src/session/lcm/mod.rs | 12 ++++++------ .../src/session/lcm/render.rs | 3 --- 2 files changed, 6 insertions(+), 9 deletions(-) delete mode 100644 crates/tracedecay-session-memory/src/session/lcm/render.rs diff --git a/crates/tracedecay-session-memory/src/session/lcm/mod.rs b/crates/tracedecay-session-memory/src/session/lcm/mod.rs index 83193cbba4..ead7230a2d 100644 --- a/crates/tracedecay-session-memory/src/session/lcm/mod.rs +++ b/crates/tracedecay-session-memory/src/session/lcm/mod.rs @@ -1,14 +1,14 @@ -//! Application ownership of the DB-free LCM compatibility surface. +//! Application ownership of the LCM authority boundary. //! //! `contracts` holds the retrieval value types and their containment rules; //! `compression_policy` holds provider-neutral token, overflow, and atomic -//! chunk-selection rules; `render` holds the truncation and typed-omission -//! shaping applied after canonical hydration. The LCM engine crate owns the -//! contract and policy surfaces, and the registered temporal adapters depend -//! on them directly, so neither side has to reach through the other. +//! chunk-selection rules. Hydration shaping lives in +//! `tracedecay-session-temporal-store` and is not re-exported here. The LCM +//! engine crate owns the contract and policy surfaces, and the registered +//! temporal adapters depend on them directly, so neither side has to reach +//! through the other. pub mod authority; -pub mod render; pub use tracedecay_lcm::compression_policy; pub use tracedecay_lcm::contracts; diff --git a/crates/tracedecay-session-memory/src/session/lcm/render.rs b/crates/tracedecay-session-memory/src/session/lcm/render.rs deleted file mode 100644 index e05bcc7bc2..0000000000 --- a/crates/tracedecay-session-memory/src/session/lcm/render.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Compatibility re-exports for global-DB-owned LCM hydration shaping. - -pub use tracedecay_session_temporal_store::render::*; From 1c90823403efc1301659fe11cdb56ba5296a98aa Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:33:57 +0000 Subject: [PATCH 120/182] refactor(pass-3/5): drop temporal execution adapter The private ports module only forwarded execution types from tracedecay-session-temporal-store. Callers import that crate. Co-authored-by: Zack Jackson --- crates/tracedecay-session-memory/src/session/mod.rs | 9 ++++----- crates/tracedecay-session-memory/src/session/ports.rs | 11 ----------- .../src/session/retrieval.rs | 9 +++++---- 3 files changed, 9 insertions(+), 20 deletions(-) delete mode 100644 crates/tracedecay-session-memory/src/session/ports.rs diff --git a/crates/tracedecay-session-memory/src/session/mod.rs b/crates/tracedecay-session-memory/src/session/mod.rs index 48d9e9835d..3829067929 100644 --- a/crates/tracedecay-session-memory/src/session/mod.rs +++ b/crates/tracedecay-session-memory/src/session/mod.rs @@ -1,6 +1,5 @@ mod hotpath_observe; pub mod lcm; -mod ports; mod refresh; mod refresh_service; mod retrieval; @@ -8,10 +7,6 @@ mod retrieval; mod tests; mod types; -pub use ports::{ - AuthorizedTemporalExecutionRequest, SessionTemporalExecutionError, - SessionTemporalExecutionPort, SessionTemporalExecutionReport, TemporalExecutionFuture, -}; pub use refresh::{ SessionRefreshConfiguration, SessionRefreshDigest, SessionRefreshHandle, SessionRefreshOutcome, SessionRefreshRequestError, SessionRefreshSchedulerError, SessionRefreshService, @@ -31,6 +26,10 @@ pub use tracedecay_contracts::retrieval::{ SessionRetrievalBudgetAccountingV1, SessionRetrievalBudgetObservationV1, SessionRetrievalBudgetStageV1, }; +pub use tracedecay_session_temporal_store::execution::{ + AuthorizedTemporalExecutionRequest, SessionTemporalExecutionError, + SessionTemporalExecutionPort, SessionTemporalExecutionReport, TemporalExecutionFuture, +}; pub use types::{ AuthorizationGrantId, AuthorizedSessionScope, SessionAccess, SessionAuthorizationError, SessionAuthorizationGrant, SessionDataFreshness, SessionFreshnessPolicy, SessionRequestBinding, diff --git a/crates/tracedecay-session-memory/src/session/ports.rs b/crates/tracedecay-session-memory/src/session/ports.rs deleted file mode 100644 index d6430f8457..0000000000 --- a/crates/tracedecay-session-memory/src/session/ports.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! Authorized temporal execution contract. -//! -//! The registered global database owns both the only production executor and -//! this contract. Re-exporting that authority keeps retrieval callers and the -//! executor on one type identity instead of maintaining a second, structurally -//! identical port inside the use-case crate. - -pub use tracedecay_session_temporal_store::execution::{ - AuthorizedTemporalExecutionRequest, SessionTemporalExecutionError, - SessionTemporalExecutionPort, SessionTemporalExecutionReport, TemporalExecutionFuture, -}; diff --git a/crates/tracedecay-session-memory/src/session/retrieval.rs b/crates/tracedecay-session-memory/src/session/retrieval.rs index da4653155a..4dbafefc4f 100644 --- a/crates/tracedecay-session-memory/src/session/retrieval.rs +++ b/crates/tracedecay-session-memory/src/session/retrieval.rs @@ -27,14 +27,15 @@ use crate::context::{ application_observed_at, application_request_interruption, run_application_request_interruptible, }; -use crate::session::ports::{ - AuthorizedTemporalExecutionRequest, SessionTemporalExecutionError, SessionTemporalExecutionPort, -}; use crate::session::types::{ SessionAccess, SessionAuthorizationError, SessionDataFreshness, SessionFreshnessPolicy, SessionRequestBinding, SessionRetrievalOutcome, SessionRetrievalScope, SessionScopeAuthorizationRequest, SessionScopeAuthorizer, }; +use tracedecay_session_temporal_store::execution::{ + AuthorizedTemporalExecutionRequest, SessionTemporalExecutionError, + SessionTemporalExecutionPort, SessionTemporalExecutionReport, +}; mod task_session; pub use task_session::TaskSessionRetrievalOutcomeV1; @@ -425,7 +426,7 @@ fn temporal_authorized_root( } fn map_report( - report: crate::session::ports::SessionTemporalExecutionReport, + report: SessionTemporalExecutionReport, freshness_policy: SessionFreshnessPolicy, ) -> SessionRetrievalOutcome { let (result, freshness) = report.into_parts(); From 8d59fd1ea66135577bdc4ebee78ead72333634c1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:34:04 +0000 Subject: [PATCH 121/182] refactor(sessions): share message row decode Co-authored-by: Zack Jackson --- .../src/observation_projection/state.rs | 105 ++----------- .../src/runtime/store_access/mod.rs | 1 + .../src/runtime/store_access/sessions.rs | 143 +++++++----------- 3 files changed, 71 insertions(+), 178 deletions(-) diff --git a/crates/tracedecay-global-db/src/observation_projection/state.rs b/crates/tracedecay-global-db/src/observation_projection/state.rs index 04e38b4b13..9843cb0725 100644 --- a/crates/tracedecay-global-db/src/observation_projection/state.rs +++ b/crates/tracedecay-global-db/src/observation_projection/state.rs @@ -12,6 +12,9 @@ use tracedecay_lcm::LcmStorageKind; use tracedecay_lcm::retrieval_content::{derived_text_for_index, projected_content_hash}; use tracedecay_runtime_core::db::engine::{Executor, QueryExecutor, Row, params}; use tracedecay_sessions::runtime::shared::durable_project_path_key; +use tracedecay_sessions::runtime::store_access::{ + message_record_from_row, session_record_from_row, +}; use super::apply::{derive_projection_with_alias, verify_provenance}; @@ -277,31 +280,9 @@ pub(super) async fn read_session( else { return Ok(None); }; - macro_rules! cell { - ($index:literal) => { - row.get($index) - .map_err(|error| storage("decode projected session", error))? - }; - ($index:literal, $ty:ty) => { - row.get::<$ty>($index) - .map_err(|error| storage("decode projected session", error))? - }; - } - Ok(Some(SessionRecord { - provider: cell!(0), - session_id: cell!(1), - project_key: cell!(2), - project_path: cell!(3), - title: cell!(4), - started_at: cell!(5), - ended_at: cell!(6), - transcript_path: cell!(7), - metadata_json: cell!(8), - parent_session_id: cell!(9), - is_subagent: cell!(10, i64) != 0, - agent_id: cell!(11), - parent_tool_use_id: cell!(12), - })) + session_record_from_row(&row) + .map(Some) + .map_err(|error| storage("decode projected session", error.source)) } pub(super) async fn read_message( @@ -325,27 +306,9 @@ pub(super) async fn read_message( else { return Ok(None); }; - macro_rules! cell { - ($index:literal) => { - row.get($index) - .map_err(|error| storage("decode projected message", error))? - }; - } - Ok(Some(SessionMessageRecord { - provider: cell!(0), - message_id: cell!(1), - session_id: cell!(2), - role: cell!(3), - timestamp: cell!(4), - ordinal: cell!(5), - text: cell!(6), - kind: cell!(7), - model: cell!(8), - tool_names: cell!(9), - source_path: cell!(10), - source_offset: cell!(11), - metadata_json: cell!(12), - })) + message_record_from_row(&row, 0) + .map(Some) + .map_err(|error| storage("decode projected message", error.source)) } fn output_owner_lookup_sql(select_expr: &str, ordering: &str) -> String { @@ -893,27 +856,8 @@ pub(in super::super) async fn read_projection_rows_batch( .await .map_err(|error| storage("read projected messages", error))? { - macro_rules! cell { - ($index:literal) => { - row.get($index) - .map_err(|error| storage("decode projected messages", error))? - }; - } - let message = SessionMessageRecord { - provider: cell!(0), - message_id: cell!(1), - session_id: cell!(2), - role: cell!(3), - timestamp: cell!(4), - ordinal: cell!(5), - text: cell!(6), - kind: cell!(7), - model: cell!(8), - tool_names: cell!(9), - source_path: cell!(10), - source_offset: cell!(11), - metadata_json: cell!(12), - }; + let message = message_record_from_row(&row, 0) + .map_err(|error| storage("decode projected messages", error.source))?; messages.insert( (message.provider.clone(), message.message_id.clone()), message, @@ -1006,31 +950,8 @@ pub(in super::super) async fn read_projection_rows_batch( .await .map_err(|error| storage("read projected sessions", error))? { - macro_rules! cell { - ($index:literal) => { - row.get($index) - .map_err(|error| storage("decode projected sessions", error))? - }; - ($index:literal, $ty:ty) => { - row.get::<$ty>($index) - .map_err(|error| storage("decode projected sessions", error))? - }; - } - let session = SessionRecord { - provider: cell!(0), - session_id: cell!(1), - project_key: cell!(2), - project_path: cell!(3), - title: cell!(4), - started_at: cell!(5), - ended_at: cell!(6), - transcript_path: cell!(7), - metadata_json: cell!(8), - parent_session_id: cell!(9), - is_subagent: cell!(10, i64) != 0, - agent_id: cell!(11), - parent_tool_use_id: cell!(12), - }; + let session = session_record_from_row(&row) + .map_err(|error| storage("decode projected sessions", error.source))?; sessions.insert( (session.provider.clone(), session.session_id.clone()), session, diff --git a/crates/tracedecay-sessions/src/runtime/store_access/mod.rs b/crates/tracedecay-sessions/src/runtime/store_access/mod.rs index 299a0bf858..e107663a5f 100644 --- a/crates/tracedecay-sessions/src/runtime/store_access/mod.rs +++ b/crates/tracedecay-sessions/src/runtime/store_access/mod.rs @@ -20,6 +20,7 @@ pub use search::{ pub(crate) use sessions::EXISTING_SESSION_MESSAGE_IDS_SQL; pub(crate) use sessions::SESSION_MESSAGE_ID_LOOKUP_MAX; pub use sessions::SESSION_MESSAGES_AFTER_SQL; +pub use sessions::{SqlColumnError, message_record_from_row, session_record_from_row}; pub use transcript::{ TranscriptGitEvidence, get_parse_offset, require_expected_offset, set_parse_offset, }; diff --git a/crates/tracedecay-sessions/src/runtime/store_access/sessions.rs b/crates/tracedecay-sessions/src/runtime/store_access/sessions.rs index 6cfd4e1b77..0aed0f6e9c 100644 --- a/crates/tracedecay-sessions/src/runtime/store_access/sessions.rs +++ b/crates/tracedecay-sessions/src/runtime/store_access/sessions.rs @@ -5,7 +5,7 @@ use std::path::Path; use serde_json::Value as JsonValue; use tracedecay_domain::errors::TraceDecayError; -use tracedecay_runtime_core::db::engine::Value; +use tracedecay_runtime_core::db::engine::{Error as EngineError, FromValue, Row, Value}; use tracedecay_store::{SESSION_MESSAGE_PROJECTOR_VERSION, SessionMessageRecord, SessionRecord}; use crate::runtime::SessionMessageSearchResult; @@ -991,100 +991,71 @@ fn workflow_column_error(column: &str, error: &dyn std::fmt::Display) -> String format!("failed to decode workflow fact column '{column}': {error}") } -fn row_to_session( - row: &tracedecay_runtime_core::db::engine::Row, -) -> std::result::Result { +/// Column decode failure. Callers keep their own error text; the column name +/// is the sessions spelling and the source is the engine error. +pub struct SqlColumnError { + pub column: &'static str, + pub source: EngineError, +} + +fn column(row: &Row, index: i32, name: &'static str) -> Result { + row.get(index).map_err(|source| SqlColumnError { + column: name, + source, + }) +} + +/// `sessions` row in the column order both the session store and the +/// observation projection read. +pub fn session_record_from_row(row: &Row) -> Result { Ok(SessionRecord { - provider: row - .get(0) - .map_err(|error| session_column_error("provider", &error))?, - session_id: row - .get(1) - .map_err(|error| session_column_error("session_id", &error))?, - project_key: row - .get(2) - .map_err(|error| session_column_error("project_key", &error))?, - project_path: row - .get(3) - .map_err(|error| session_column_error("project_path", &error))?, - title: row - .get(4) - .map_err(|error| session_column_error("title", &error))?, - started_at: row - .get(5) - .map_err(|error| session_column_error("started_at", &error))?, - ended_at: row - .get(6) - .map_err(|error| session_column_error("ended_at", &error))?, - transcript_path: row - .get(7) - .map_err(|error| session_column_error("transcript_path", &error))?, - metadata_json: row - .get(8) - .map_err(|error| session_column_error("metadata_json", &error))?, - parent_session_id: row - .get(9) - .map_err(|error| session_column_error("parent_session_id", &error))?, - is_subagent: row - .get::(10) - .map_err(|error| session_column_error("is_subagent", &error))? - != 0, - agent_id: row - .get(11) - .map_err(|error| session_column_error("agent_id", &error))?, - parent_tool_use_id: row - .get(12) - .map_err(|error| session_column_error("parent_tool_use_id", &error))?, + provider: column(row, 0, "provider")?, + session_id: column(row, 1, "session_id")?, + project_key: column(row, 2, "project_key")?, + project_path: column(row, 3, "project_path")?, + title: column(row, 4, "title")?, + started_at: column(row, 5, "started_at")?, + ended_at: column(row, 6, "ended_at")?, + transcript_path: column(row, 7, "transcript_path")?, + metadata_json: column(row, 8, "metadata_json")?, + parent_session_id: column(row, 9, "parent_session_id")?, + is_subagent: column::(row, 10, "is_subagent")? != 0, + agent_id: column(row, 11, "agent_id")?, + parent_tool_use_id: column(row, 12, "parent_tool_use_id")?, }) } -fn row_to_message( - row: &tracedecay_runtime_core::db::engine::Row, +/// `session_messages` row, starting at `offset`, in the shared column order. +pub fn message_record_from_row( + row: &Row, offset: i32, -) -> std::result::Result { +) -> Result { Ok(SessionMessageRecord { - provider: row - .get(offset) - .map_err(|error| message_column_error("provider", &error))?, - message_id: row - .get(offset + 1) - .map_err(|error| message_column_error("message_id", &error))?, - session_id: row - .get(offset + 2) - .map_err(|error| message_column_error("session_id", &error))?, - role: row - .get(offset + 3) - .map_err(|error| message_column_error("role", &error))?, - timestamp: row - .get(offset + 4) - .map_err(|error| message_column_error("timestamp", &error))?, - ordinal: row - .get(offset + 5) - .map_err(|error| message_column_error("ordinal", &error))?, - text: row - .get(offset + 6) - .map_err(|error| message_column_error("text", &error))?, - kind: row - .get(offset + 7) - .map_err(|error| message_column_error("kind", &error))?, - model: row - .get(offset + 8) - .map_err(|error| message_column_error("model", &error))?, - tool_names: row - .get(offset + 9) - .map_err(|error| message_column_error("tool_names", &error))?, - source_path: row - .get(offset + 10) - .map_err(|error| message_column_error("source_path", &error))?, - source_offset: row - .get(offset + 11) - .map_err(|error| message_column_error("source_offset", &error))?, - metadata_json: row - .get(offset + 12) - .map_err(|error| message_column_error("metadata_json", &error))?, + provider: column(row, offset, "provider")?, + message_id: column(row, offset + 1, "message_id")?, + session_id: column(row, offset + 2, "session_id")?, + role: column(row, offset + 3, "role")?, + timestamp: column(row, offset + 4, "timestamp")?, + ordinal: column(row, offset + 5, "ordinal")?, + text: column(row, offset + 6, "text")?, + kind: column(row, offset + 7, "kind")?, + model: column(row, offset + 8, "model")?, + tool_names: column(row, offset + 9, "tool_names")?, + source_path: column(row, offset + 10, "source_path")?, + source_offset: column(row, offset + 11, "source_offset")?, + metadata_json: column(row, offset + 12, "metadata_json")?, }) } +fn row_to_session(row: &Row) -> std::result::Result { + session_record_from_row(row).map_err(|error| session_column_error(error.column, &error.source)) +} + +fn row_to_message(row: &Row, offset: i32) -> std::result::Result { + message_record_from_row(row, offset) + .map_err(|error| message_column_error(error.column, &error.source)) +} + fn row_to_workflow_message( row: &tracedecay_runtime_core::db::engine::Row, offset: i32, From 4c019c335ff69044ed376727997deb6d5b192851 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:34:09 +0000 Subject: [PATCH 122/182] refactor(graph-db): share stable graph identity Code-graph, git topology, and workflow topology minted the same NUL-separated SHA-256 id stem. One function keeps sealed ids identical. Co-authored-by: Zack Jackson --- .../symbol_graph_ignored_dependency_tests.rs | 10 +----- .../symbol_graph_implementation_tests.rs | 10 +----- .../src/work/workflow_topology.rs | 10 +----- .../src/git_projection.rs | 10 +----- .../src/graph_projection/schema.rs | 10 +----- crates/tracedecay-graph-db/src/lib.rs | 1 + crates/tracedecay-graph-db/src/schema.rs | 33 +++++++++++++++++++ 7 files changed, 39 insertions(+), 45 deletions(-) diff --git a/crates/tracedecay-application/src/primitives/symbol_graph_ignored_dependency_tests.rs b/crates/tracedecay-application/src/primitives/symbol_graph_ignored_dependency_tests.rs index bad2a214db..7c2e58014b 100644 --- a/crates/tracedecay-application/src/primitives/symbol_graph_ignored_dependency_tests.rs +++ b/crates/tracedecay-application/src/primitives/symbol_graph_ignored_dependency_tests.rs @@ -2,7 +2,6 @@ use std::collections::{BTreeMap, BTreeSet}; use std::sync::{Arc, Mutex}; use serde::Serialize; -use sha2::{Digest, Sha256}; use tracedecay_code_index::chunks::CodeIndexImportEvidenceV1; use tracedecay_code_index::graph_projection::{ CODE_GRAPH_PROJECTOR_REVISION, CodeGraphProjectionStore, CodeGraphSymbolBindingV1, @@ -26,6 +25,7 @@ use tracedecay_domain::{ SanitizedCodeFileV1, SanitizerRevision, SessionId, SnapshotFileDispositionV1, SourceSpan, SymbolIdentityDigest, SymbolOccurrenceId, TemporalModeV1, UtcMicros, WorktreeId, }; +use tracedecay_graph_db::graph_stable_identity as stable_identity; use tracedecay_graph_db::{ GraphEntity, GraphEntityId, GraphEntityRef, GraphGenerationManifest, GraphGenerationRelation, GraphLabel, GraphNamespace, GraphProjectorRevision, GraphProperty, GraphPropertyName, @@ -818,14 +818,6 @@ fn symbol_entity( .expect("symbol entity") } -fn stable_identity(kind: &str, value: &str) -> String { - let mut digest = Sha256::new(); - digest.update(kind.as_bytes()); - digest.update([0]); - digest.update(value.as_bytes()); - format!("{kind}:{}", hex::encode(digest.finalize())) -} - fn digest(byte: char) -> T where T: TryFrom, diff --git a/crates/tracedecay-application/src/primitives/symbol_graph_implementation_tests.rs b/crates/tracedecay-application/src/primitives/symbol_graph_implementation_tests.rs index 4cd4ad1f91..1644f172d4 100644 --- a/crates/tracedecay-application/src/primitives/symbol_graph_implementation_tests.rs +++ b/crates/tracedecay-application/src/primitives/symbol_graph_implementation_tests.rs @@ -2,7 +2,6 @@ use std::fmt::Debug; use std::sync::Arc; use serde::Serialize; -use sha2::{Digest, Sha256}; use tracedecay_code_index::graph_projection::{ CODE_GRAPH_PROJECTOR_REVISION, CodeGraphProjectionStore, CodeGraphSymbolBindingV1, build_code_graph_manifest, code_graph_projection_identity, @@ -17,6 +16,7 @@ use tracedecay_domain::{ PolicyRevisionId, RelationEdgeKindV1, SanitizerRevision, SensitivityDecision, SensitivityLevelV1, SourceSpan, SymbolIdentityDigest, SymbolOccurrenceId, }; +use tracedecay_graph_db::graph_stable_identity as stable_identity; use tracedecay_graph_db::{ GraphEntityId, GraphNamespace, GraphProjectorRevision, GraphProperty, GraphPropertyName, NeverCancelled, VerifiedGraphSnapshot, @@ -300,11 +300,3 @@ struct SymbolRecordFixture { binding: Option, metadata: Option, } - -fn stable_identity(kind: &str, value: &str) -> String { - let mut digest = Sha256::new(); - digest.update(kind.as_bytes()); - digest.update([0]); - digest.update(value.as_bytes()); - format!("{kind}:{}", hex::encode(digest.finalize())) -} diff --git a/crates/tracedecay-application/src/work/workflow_topology.rs b/crates/tracedecay-application/src/work/workflow_topology.rs index 962019f94f..46631fb67c 100644 --- a/crates/tracedecay-application/src/work/workflow_topology.rs +++ b/crates/tracedecay-application/src/work/workflow_topology.rs @@ -5,9 +5,9 @@ use std::fmt; use std::sync::Arc; use serde::Serialize; -use sha2::{Digest, Sha256}; use thiserror::Error; use tracedecay_domain::{WorkflowDefinition, WorkflowStep, WorkflowStepId, canonical_sha256}; +use tracedecay_graph_db::graph_stable_identity as stable_identity; use tracedecay_graph_db::{ GraphCancellation, GraphDbError, GraphEntity, GraphEntityId, GraphEntityRef, GraphGenerationId, GraphGenerationManifest, GraphGenerationRelation, GraphIdempotencyKey, GraphLabel, @@ -430,14 +430,6 @@ fn check_cancelled(cancellation: &dyn GraphCancellation) -> Result<(), WorkflowT } } -fn stable_identity(kind: &str, value: &str) -> String { - let mut digest = Sha256::new(); - digest.update(kind.as_bytes()); - digest.update([0]); - digest.update(value.as_bytes()); - format!("{kind}:{}", hex::encode(digest.finalize())) -} - fn serialize(value: &impl Serialize) -> Result, WorkflowTopologyError> { serde_json::to_vec(value).map_err(|error| WorkflowTopologyError::Contract(error.to_string())) } diff --git a/crates/tracedecay-code-index/src/git_projection.rs b/crates/tracedecay-code-index/src/git_projection.rs index 9981c3468a..529ef55965 100644 --- a/crates/tracedecay-code-index/src/git_projection.rs +++ b/crates/tracedecay-code-index/src/git_projection.rs @@ -7,12 +7,12 @@ use std::fmt; use std::sync::Arc; use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; use thiserror::Error; use tracedecay_domain::{ GitCommitMetadataV1, GitCoverageV1, GitHeadStateV1, GitHistoryV1, GitOidV1, ManifestDigest, RefId, RepositoryId, canonical_sha256, }; +use tracedecay_graph_db::graph_stable_identity as stable_identity; use tracedecay_graph_db::{ GraphCancellation, GraphDbError, GraphEntity, GraphEntityId, GraphEntityRef, GraphGenerationId, GraphGenerationManifest, GraphGenerationRelation, GraphIdempotencyKey, GraphLabel, @@ -751,14 +751,6 @@ fn metadata_entity_id() -> Result { GraphEntityId::new(stable_identity("metadata", GIT_PROJECTION)).map_err(Into::into) } -fn stable_identity(kind: &str, value: &str) -> String { - let mut digest = Sha256::new(); - digest.update(kind.as_bytes()); - digest.update([0]); - digest.update(value.as_bytes()); - format!("{kind}:{}", hex::encode(digest.finalize())) -} - fn serialize(value: &impl Serialize) -> Result, GitTopologyProjectionError> { serde_json::to_vec(value) .map_err(|error| GitTopologyProjectionError::Contract(error.to_string())) diff --git a/crates/tracedecay-code-index/src/graph_projection/schema.rs b/crates/tracedecay-code-index/src/graph_projection/schema.rs index a051f23adc..7cb4a1f74c 100644 --- a/crates/tracedecay-code-index/src/graph_projection/schema.rs +++ b/crates/tracedecay-code-index/src/graph_projection/schema.rs @@ -1,8 +1,8 @@ //! Durable labels, properties, and identities for the code-graph projection. use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; use tracedecay_domain::FileOccurrenceId; +pub(super) use tracedecay_graph_db::graph_stable_identity as stable_identity; use tracedecay_graph_db::{ GraphEntity, GraphEntityId, GraphProperty, GraphPropertyName, GraphRelationId, }; @@ -18,14 +18,6 @@ pub(super) const FILE_LABEL: &str = "CodeFile"; pub(super) const IMPORT_LABEL: &str = "CodeImport"; pub(super) const FILE_IMPORT_EDGE_KIND: &str = "CodeFileContainsImport"; -pub(super) fn stable_identity(kind: &str, value: &str) -> String { - let mut digest = Sha256::new(); - digest.update(kind.as_bytes()); - digest.update([0]); - digest.update(value.as_bytes()); - format!("{kind}:{}", hex::encode(digest.finalize())) -} - pub(super) fn file_entity_id( file: &FileOccurrenceId, ) -> Result { diff --git a/crates/tracedecay-graph-db/src/lib.rs b/crates/tracedecay-graph-db/src/lib.rs index baf03eab9d..83ebd4cacd 100644 --- a/crates/tracedecay-graph-db/src/lib.rs +++ b/crates/tracedecay-graph-db/src/lib.rs @@ -96,6 +96,7 @@ pub use registry::{ GraphPublicationPreparationV1, ProvenGraphPublicationV1, }; pub use runtime::{GraphDb, GraphDbRuntimeState, GraphSnapshot}; +pub use schema::graph_stable_identity; pub use sealed_store::{SealedStoreCensusV1, census_sealed_store}; /// What hydration decoded on **this thread** since the last take. diff --git a/crates/tracedecay-graph-db/src/schema.rs b/crates/tracedecay-graph-db/src/schema.rs index 66bd8fa2e4..87a645c61d 100644 --- a/crates/tracedecay-graph-db/src/schema.rs +++ b/crates/tracedecay-graph-db/src/schema.rs @@ -3,6 +3,7 @@ use std::collections::{BTreeMap, BTreeSet}; use grafeo_common::types::{EdgeId, NodeId, Value}; use grafeo_core::graph::GraphStore; use grafeo_core::graph::lpg::{Edge, Node}; +use sha2::{Digest, Sha256}; use crate::limits::{ MAX_GRAPH_ENTITY_LABEL_BYTES, MAX_GRAPH_ENTITY_LABELS, MAX_GRAPH_IDENTIFIER_BYTES, @@ -73,6 +74,21 @@ pub(crate) const INDEXED_PROPERTIES: [&str; 6] = [ QUARANTINE_KEY_PROPERTY, ]; +/// Durable `{kind}:{sha256}` stem used for graph entity and relation ids. +/// +/// Kind and value are separated by a NUL byte so a kind cannot be smuggled +/// in as a prefix of the value. Code-graph symbols, git topology, and +/// workflow topology all mint ids through this function; the byte layout is +/// already sealed in stored graphs. +#[must_use] +pub fn graph_stable_identity(kind: &str, value: &str) -> String { + let mut digest = Sha256::new(); + digest.update(kind.as_bytes()); + digest.update([0]); + digest.update(value.as_bytes()); + format!("{kind}:{}", hex::encode(digest.finalize())) +} + pub(crate) fn encoded_namespace_key(namespace: &GraphNamespace) -> String { hex::encode(namespace.as_str().as_bytes()) } @@ -814,6 +830,23 @@ fn decode_utf8(value: &str, description: &str) -> Result { }) } +#[cfg(test)] +mod graph_stable_identity_tests { + use super::graph_stable_identity; + + #[test] + fn kind_and_value_stay_separated_by_a_nul() { + assert_eq!( + graph_stable_identity("symbol", "occ"), + "symbol:199f069a8ccddbb90bd0626b5904f52fbb2d92879bdbbf2c5dc29c1ea4ab66fb" + ); + assert_ne!( + graph_stable_identity("symbol", "occ"), + graph_stable_identity("symbolo", "cc") + ); + } +} + fn persisted_validation_error(description: &str, error: GraphDbError) -> GraphDbError { GraphDbError::Corrupt { message: format!("invalid persisted {description}: {error}"), From fe8038dd502808604ad267c0996fe9e05548476b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:34:27 +0000 Subject: [PATCH 123/182] refactor(contracts): share lcm role wire labels LcmRoleV1 owns the serde snake_case labels. Session retrieval, automation role projection, and the LCM grep catalog use that map instead of private copies. Co-authored-by: Zack Jackson --- .../src/automation/effect_runtime/input.rs | 12 +--- .../src/retained_surfaces/sdk.rs | 62 +++++++++++++++++++ .../src/definitions/lcm.rs | 3 +- .../src/retained/lcm.rs | 12 +--- .../src/retained/lcm/retrieval.rs | 4 +- 5 files changed, 69 insertions(+), 24 deletions(-) diff --git a/crates/tracedecay-automation-runtime/src/automation/effect_runtime/input.rs b/crates/tracedecay-automation-runtime/src/automation/effect_runtime/input.rs index 07422d6f0a..f3f46ee64a 100644 --- a/crates/tracedecay-automation-runtime/src/automation/effect_runtime/input.rs +++ b/crates/tracedecay-automation-runtime/src/automation/effect_runtime/input.rs @@ -103,16 +103,8 @@ fn project_skill_writer_input( } fn project_role(role: &str) -> Result { - match role { - "system" => Ok(LcmRoleV1::System), - "user" => Ok(LcmRoleV1::User), - "assistant" => Ok(LcmRoleV1::Assistant), - "tool" => Ok(LcmRoleV1::Tool), - "unknown" => Ok(LcmRoleV1::Unknown), - _ => Err(contract_error(format!( - "session reflector role is not registered: {role}" - ))), - } + LcmRoleV1::parse(role) + .ok_or_else(|| contract_error(format!("session reflector role is not registered: {role}"))) } fn automation_run_request( diff --git a/crates/tracedecay-contracts/src/retained_surfaces/sdk.rs b/crates/tracedecay-contracts/src/retained_surfaces/sdk.rs index 547788760a..459145c32b 100644 --- a/crates/tracedecay-contracts/src/retained_surfaces/sdk.rs +++ b/crates/tracedecay-contracts/src/retained_surfaces/sdk.rs @@ -351,6 +351,68 @@ pub enum LcmRoleV1 { Unknown, } +impl LcmRoleV1 { + /// Advertised order for MCP schemas. Labels are [`Self::as_str`], which is + /// the serde `snake_case` name, so the catalog cannot drift from the wire. + pub const WIRE: [&'static str; 5] = [ + Self::System.as_str(), + Self::User.as_str(), + Self::Assistant.as_str(), + Self::Tool.as_str(), + Self::Unknown.as_str(), + ]; + + pub const fn as_str(self) -> &'static str { + match self { + Self::System => "system", + Self::User => "user", + Self::Assistant => "assistant", + Self::Tool => "tool", + Self::Unknown => "unknown", + } + } + + pub fn parse(value: &str) -> Option { + match value { + "system" => Some(Self::System), + "user" => Some(Self::User), + "assistant" => Some(Self::Assistant), + "tool" => Some(Self::Tool), + "unknown" => Some(Self::Unknown), + _ => None, + } + } +} + +#[cfg(test)] +mod lcm_role_wire_tests { + use serde_json::json; + + use super::LcmRoleV1; + + #[test] + fn role_labels_match_serde_and_reject_host_aliases() { + let roles = [ + LcmRoleV1::System, + LcmRoleV1::User, + LcmRoleV1::Assistant, + LcmRoleV1::Tool, + LcmRoleV1::Unknown, + ]; + let labels: Vec<&str> = roles.iter().copied().map(LcmRoleV1::as_str).collect(); + assert_eq!(labels.as_slice(), LcmRoleV1::WIRE); + for role in roles { + assert_eq!( + serde_json::to_value(role).expect("role serializes"), + json!(role.as_str()) + ); + assert_eq!(LcmRoleV1::parse(role.as_str()), Some(role)); + } + assert_eq!(LcmRoleV1::parse("developer"), None); + assert_eq!(LcmRoleV1::parse(" model"), None); + } +} + #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct LcmGrepRequestV1 { diff --git a/crates/tracedecay-mcp-catalog/src/definitions/lcm.rs b/crates/tracedecay-mcp-catalog/src/definitions/lcm.rs index fe46ce79fa..926916bda5 100644 --- a/crates/tracedecay-mcp-catalog/src/definitions/lcm.rs +++ b/crates/tracedecay-mcp-catalog/src/definitions/lcm.rs @@ -1,6 +1,7 @@ //! LCM session-store and session health-baseline tool definitions. use serde_json::json; +use tracedecay_contracts::retained_surfaces::LcmRoleV1; use super::{def, git_scope}; use crate::ToolDefinition; @@ -168,7 +169,7 @@ pub(super) fn def_lcm_grep() -> ToolDefinition { }, "role": { "type": "string", - "enum": ["system", "user", "assistant", "tool", "unknown"], + "enum": LcmRoleV1::WIRE, "description": "Optional raw-message role filter. When supplied, summary results are omitted." }, "start_time": { diff --git a/crates/tracedecay-session-runtime/src/retained/lcm.rs b/crates/tracedecay-session-runtime/src/retained/lcm.rs index 8a38681dd8..4a2e9490e1 100644 --- a/crates/tracedecay-session-runtime/src/retained/lcm.rs +++ b/crates/tracedecay-session-runtime/src/retained/lcm.rs @@ -8,7 +8,7 @@ use tracedecay_contracts::retained_surfaces::{ LcmDoctorHealthV1, LcmDoctorProjectionStateV1, LcmDoctorProjectionV1, LcmDoctorRequestV1, LcmDoctorResultV1, LcmExpandQueryRequestV1, LcmExpandRequestV1, LcmGrepRequestV1, LcmLifecycleStatusV1, LcmLoadSessionRequestV1, LcmPayloadCoverageStateV1, LcmPayloadCoverageV1, - LcmPayloadGcStatusV1, LcmPayloadStatusV1, LcmRedactionStatusV1, LcmRoleV1, LcmStatusRequestV1, + LcmPayloadGcStatusV1, LcmPayloadStatusV1, LcmRedactionStatusV1, LcmStatusRequestV1, LcmStatusResultV1, LcmStatusV1, LcmStoreStatusV1, LcmStoreTokenCoverageV1, LcmTemporalModeV1, MessageRelationshipScopeV1, MessageTypeFilterV1, RetainedOutcomeStatusV1, RetainedSurfaceOperation, RetainedSurfaceResultV1, RetainedTimeFilterV1, @@ -1013,16 +1013,6 @@ pub(super) fn message_type(value: Option) -> SessionMessage } } -pub(super) const fn role_name(value: LcmRoleV1) -> &'static str { - match value { - LcmRoleV1::System => "system", - LcmRoleV1::User => "user", - LcmRoleV1::Assistant => "assistant", - LcmRoleV1::Tool => "tool", - LcmRoleV1::Unknown => "unknown", - } -} - pub(super) fn time_filter( value: Option<&RetainedTimeFilterV1>, bound: SearchTimeBound, diff --git a/crates/tracedecay-session-runtime/src/retained/lcm/retrieval.rs b/crates/tracedecay-session-runtime/src/retained/lcm/retrieval.rs index a9c557535f..f01b170fc6 100644 --- a/crates/tracedecay-session-runtime/src/retained/lcm/retrieval.rs +++ b/crates/tracedecay-session-runtime/src/retained/lcm/retrieval.rs @@ -29,7 +29,7 @@ use tracedecay_temporal_query::ranking::DiversityLimits; use super::output; use super::{ cursor, message_type, optional_provider, optional_usize, relationship_scope, required, - role_name, session_id, specific_provider, temporal_mode, time_filter, trimmed, unsigned_i64, + session_id, specific_provider, temporal_mode, time_filter, trimmed, unsigned_i64, }; use crate::retained::session_retrieval_unavailable_detail; use crate::session_retrieval::{ @@ -186,7 +186,7 @@ pub(super) async fn execute_grep( let message_type = message_type(request.message_type); let roles = request .role - .map(|role| vec![role_name(role).to_owned()]) + .map(|role| vec![role.as_str().to_owned()]) .unwrap_or_default(); let start = request.start_time.as_ref().or(request.since.as_ref()); let end = request.end_time.as_ref().or(request.until.as_ref()); From 9cf1d2ce7ec24bab08f72810e5d061fc4038403d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:34:40 +0000 Subject: [PATCH 124/182] refactor(domain): share anchor owner column json Co-authored-by: Zack Jackson --- crates/tracedecay-domain/src/research/anchor.rs | 11 +++++++++++ .../src/git_topology_anchor.rs | 5 +++-- .../src/observation_adapter.rs | 2 +- .../observation_projection/source_transition.rs | 6 ++++-- .../src/operations/message_anchor.rs | 14 +++++++------- .../src/operations/publication.rs | 3 +-- .../src/operations/sources.rs | 3 ++- 7 files changed, 29 insertions(+), 15 deletions(-) diff --git a/crates/tracedecay-domain/src/research/anchor.rs b/crates/tracedecay-domain/src/research/anchor.rs index 0df14d5a4b..02b8c72a26 100644 --- a/crates/tracedecay-domain/src/research/anchor.rs +++ b/crates/tracedecay-domain/src/research/anchor.rs @@ -936,6 +936,17 @@ impl RetrievalAnchorRecordV2 { &self.owner } + /// JSON stored in `retrieval_anchors.owner_json`. Callers compare or insert + /// this text; they do not re-serialize the owner column themselves. + pub fn owner_column_json(&self) -> Result { + serde_json::to_string(self.owner()) + } + + /// Whether a stored `owner_json` cell is this record's owner column. + pub fn owner_column_matches(&self, stored: &str) -> bool { + self.owner_column_json().ok().as_deref() == Some(stored) + } + pub fn aliases(&self) -> &[NativeAliasV2] { &self.aliases } diff --git a/crates/tracedecay-global-db/src/git_topology_anchor.rs b/crates/tracedecay-global-db/src/git_topology_anchor.rs index 85ca263ddd..1cf5ef7b8b 100644 --- a/crates/tracedecay-global-db/src/git_topology_anchor.rs +++ b/crates/tracedecay-global-db/src/git_topology_anchor.rs @@ -58,7 +58,8 @@ impl RegisteredGitTopologyAnchorAuthorityV2 { } let anchor_json = serde_json::to_string(&candidate) .map_err(|_| GitTopologyAnchorAuthorityErrorV2::Conflict)?; - let owner_json = serde_json::to_string(candidate.owner()) + let owner_json = candidate + .owner_column_json() .map_err(|_| GitTopologyAnchorAuthorityErrorV2::Conflict)?; transaction .execute( @@ -209,7 +210,7 @@ fn decode_record( record .validate() .map_err(|_| GitTopologyAnchorAuthorityErrorV2::ResetRequired)?; - if serde_json::to_string(record.owner()).ok().as_deref() != Some(owner_json) + if !record.owner_column_matches(owner_json) || record.projection_generation().as_str() != projection_generation { return Err(GitTopologyAnchorAuthorityErrorV2::ResetRequired); diff --git a/crates/tracedecay-global-db/src/observation_adapter.rs b/crates/tracedecay-global-db/src/observation_adapter.rs index 965107ef73..180efb8f70 100644 --- a/crates/tracedecay-global-db/src/observation_adapter.rs +++ b/crates/tracedecay-global-db/src/observation_adapter.rs @@ -1303,7 +1303,7 @@ async fn read_stored_observations_from_snapshot( .map_err(|error| runtime_storage_error(operation, error))?; let expected_repository_owner = repository_anchor .as_ref() - .map(|anchor: &RetrievalAnchorRecordV2| serde_json::to_string(anchor.owner())) + .map(RetrievalAnchorRecordV2::owner_column_json) .transpose() .map_err(|error| runtime_storage_error(operation, error))?; if repository_owner != expected_repository_owner { diff --git a/crates/tracedecay-global-db/src/observation_projection/source_transition.rs b/crates/tracedecay-global-db/src/observation_projection/source_transition.rs index e3b37b83fb..4c6a3177ea 100644 --- a/crates/tracedecay-global-db/src/observation_projection/source_transition.rs +++ b/crates/tracedecay-global-db/src/observation_projection/source_transition.rs @@ -76,7 +76,8 @@ pub(crate) async fn verify_native_source_supersession( verify_workflow_effects(conn, effect.workflow_facts()).await?; } let (old_anchor, new_anchor) = read_transition_anchors(conn, predecessor, &successor).await?; - let owner = serde_json::to_string(old_anchor.owner()) + let owner = old_anchor + .owner_column_json() .map_err(|error| storage("encode native source owner", error))?; let mut disposition = conn .query( @@ -444,7 +445,8 @@ async fn promote_native_source_anchor( successor: &DurableObservationV1, ) -> ProjectionStoreResult<()> { let (old, new) = read_transition_anchors(conn, predecessor, successor).await?; - let owner = serde_json::to_string(old.owner()) + let owner = old + .owner_column_json() .map_err(|error| storage("encode native source anchor owner", error))?; let disposition = RetrievalAnchorDispositionRecordV1::new( format!( diff --git a/crates/tracedecay-session-temporal-store/src/operations/message_anchor.rs b/crates/tracedecay-session-temporal-store/src/operations/message_anchor.rs index e2698fb086..b155665056 100644 --- a/crates/tracedecay-session-temporal-store/src/operations/message_anchor.rs +++ b/crates/tracedecay-session-temporal-store/src/operations/message_anchor.rs @@ -322,7 +322,7 @@ fn require_session_owned_observation( || observation.source().session_id().as_str() != session_id || observation.scope() != expected_scope || anchor.owner() != observation.scope() - || serde_json::to_string(anchor.owner()).ok().as_deref() != Some(owner_json) + || !anchor.owner_column_matches(owner_json) || retained_receipt_id != observation.receipt().receipt().receipt_id().as_str() { return Err(LcmError::SummarySourceNotOwnedBySession); @@ -825,7 +825,7 @@ mod tests { &malformed.to_string(), &observation, &anchor, - &serde_json::to_string(anchor.owner()).expect("owner json"), + &anchor.owner_column_json().expect("owner json"), ) .await; @@ -862,7 +862,7 @@ mod tests { &malformed.to_string(), &observation, &anchor, - &serde_json::to_string(anchor.owner()).expect("owner json"), + &anchor.owner_column_json().expect("owner json"), ) .await; @@ -931,7 +931,7 @@ mod tests { &serde_json::to_string(&observation).expect("observation json"), &observation, &foreign_anchor, - &serde_json::to_string(foreign_anchor.owner()).expect("owner json"), + &foreign_anchor.owner_column_json().expect("owner json"), ) .await; @@ -1123,7 +1123,7 @@ mod tests { &serde_json::to_string(&observation).expect("observation json"), &observation, &anchor, - &serde_json::to_string(anchor.owner()).expect("owner json"), + &anchor.owner_column_json().expect("owner json"), index + 1, ) .await; @@ -1247,7 +1247,7 @@ mod tests { &serde_json::to_string(&canonical).expect("observation json"), &canonical, &canonical_anchor, - &serde_json::to_string(canonical_anchor.owner()).expect("owner json"), + &canonical_anchor.owner_column_json().expect("owner json"), 1, ) .await; @@ -1268,7 +1268,7 @@ mod tests { &serde_json::to_string(&observation).expect("observation json"), &observation, &anchor, - &serde_json::to_string(anchor.owner()).expect("owner json"), + &anchor.owner_column_json().expect("owner json"), sequence, ) .await; diff --git a/crates/tracedecay-session-temporal-store/src/operations/publication.rs b/crates/tracedecay-session-temporal-store/src/operations/publication.rs index e17c61426a..005844de28 100644 --- a/crates/tracedecay-session-temporal-store/src/operations/publication.rs +++ b/crates/tracedecay-session-temporal-store/src/operations/publication.rs @@ -418,8 +418,7 @@ async fn verify_summary_anchor( .ok() .is_some_and(|anchor| { anchor.anchor_id().as_str() == manifest.summary_anchor_id - && serde_json::to_string(anchor.owner()).ok().as_deref() - == Some(actual_owner_json.as_str()) + && anchor.owner_column_matches(actual_owner_json.as_str()) && matches!( anchor.target(), RetrievalAnchorTargetV2::Entity(entity) diff --git a/crates/tracedecay-session-temporal-store/src/operations/sources.rs b/crates/tracedecay-session-temporal-store/src/operations/sources.rs index 8d5ec33665..4ef9d132bd 100644 --- a/crates/tracedecay-session-temporal-store/src/operations/sources.rs +++ b/crates/tracedecay-session-temporal-store/src/operations/sources.rs @@ -634,7 +634,8 @@ pub(super) async fn insert_summary_anchor( typed_anchor: Option<&RetrievalAnchorRecord>, ) -> Result<(), LcmError> { let stored_owner_json = match typed_anchor { - Some(anchor) => serde_json::to_string(anchor.owner()) + Some(anchor) => anchor + .owner_column_json() .map_err(|error| LcmError::Db(format!("encode summary anchor owner: {error}")))?, None => owner_json.to_string(), }; From bb703b55c056873830b6453ffb1453e417d63215 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:34:48 +0000 Subject: [PATCH 125/182] refactor(graph-db): share store failure classification Git topology and workflow topology folded GraphDbError the same way except the conflict message. One classifier keeps those verdicts. Co-authored-by: Zack Jackson --- .../src/work/workflow_topology.rs | 32 +++----- .../src/git_projection.rs | 32 +++----- crates/tracedecay-graph-db/src/error.rs | 74 ++++++++++++++++++- crates/tracedecay-graph-db/src/lib.rs | 5 +- 4 files changed, 95 insertions(+), 48 deletions(-) diff --git a/crates/tracedecay-application/src/work/workflow_topology.rs b/crates/tracedecay-application/src/work/workflow_topology.rs index 46631fb67c..9f4f3d7f81 100644 --- a/crates/tracedecay-application/src/work/workflow_topology.rs +++ b/crates/tracedecay-application/src/work/workflow_topology.rs @@ -12,8 +12,9 @@ use tracedecay_graph_db::{ GraphCancellation, GraphDbError, GraphEntity, GraphEntityId, GraphEntityRef, GraphGenerationId, GraphGenerationManifest, GraphGenerationRelation, GraphIdempotencyKey, GraphLabel, GraphNamespace, GraphProjectionId, GraphProjectionIdentity, GraphProjectorRevision, - GraphProperty, GraphPropertyName, GraphRelationId, GraphRelationKind, GraphTraversalDirection, - GraphWatermark, SourceGeneration, TraversalRequest, VerifiedGraphSnapshot, + GraphProperty, GraphPropertyName, GraphRelationId, GraphRelationKind, GraphStoreFailureClass, + GraphTraversalDirection, GraphWatermark, SourceGeneration, TraversalRequest, + VerifiedGraphSnapshot, classify_graph_store_error, }; const WORKFLOW_PROJECTION: &str = "workflow-topology"; @@ -42,27 +43,12 @@ pub enum WorkflowTopologyError { impl From for WorkflowTopologyError { fn from(error: GraphDbError) -> Self { - match error { - GraphDbError::Cancelled => Self::Cancelled, - GraphDbError::BudgetExhausted { .. } | GraphDbError::DeadlineExceeded => { - Self::BudgetExhausted - } - GraphDbError::InvalidRequest { message } => Self::Contract(message), - GraphDbError::Corrupt { message } - | GraphDbError::ResetRequired { message } - | GraphDbError::DurabilityUncertain { message } - | GraphDbError::ProjectionMismatch { message, .. } - | GraphDbError::GenerationMismatch { message, .. } => Self::Corrupt(message), - GraphDbError::Conflict { .. } => { - Self::Unavailable("workflow topology publication conflict".to_owned()) - } - GraphDbError::Unavailable { message } - | GraphDbError::SealedStoreImmutable { message } => Self::Unavailable(message), - error @ (GraphDbError::SourceCommitmentsUnavailable { .. } - | GraphDbError::SealedRevisionIncompatible { .. }) => { - Self::Unavailable(error.to_string()) - } - GraphDbError::Closed => Self::Unavailable("graph store is closed".to_owned()), + match classify_graph_store_error(error, "workflow topology publication conflict") { + GraphStoreFailureClass::Cancelled => Self::Cancelled, + GraphStoreFailureClass::BudgetExhausted => Self::BudgetExhausted, + GraphStoreFailureClass::Contract(message) => Self::Contract(message), + GraphStoreFailureClass::Corrupt(message) => Self::Corrupt(message), + GraphStoreFailureClass::Unavailable(message) => Self::Unavailable(message), } } } diff --git a/crates/tracedecay-code-index/src/git_projection.rs b/crates/tracedecay-code-index/src/git_projection.rs index 529ef55965..72174ac835 100644 --- a/crates/tracedecay-code-index/src/git_projection.rs +++ b/crates/tracedecay-code-index/src/git_projection.rs @@ -17,8 +17,9 @@ use tracedecay_graph_db::{ GraphCancellation, GraphDbError, GraphEntity, GraphEntityId, GraphEntityRef, GraphGenerationId, GraphGenerationManifest, GraphGenerationRelation, GraphIdempotencyKey, GraphLabel, GraphNamespace, GraphProjectionId, GraphProjectionIdentity, GraphProjectorRevision, - GraphProperty, GraphPropertyName, GraphRelationId, GraphRelationKind, GraphTraversalDirection, - GraphWatermark, SourceGeneration, TraversalRequest, VerifiedGraphSnapshot, + GraphProperty, GraphPropertyName, GraphRelationId, GraphRelationKind, GraphStoreFailureClass, + GraphTraversalDirection, GraphWatermark, SourceGeneration, TraversalRequest, + VerifiedGraphSnapshot, classify_graph_store_error, }; use declared_topology::validate_declared_topology; @@ -151,27 +152,12 @@ pub enum GitTopologyProjectionError { impl From for GitTopologyProjectionError { fn from(error: GraphDbError) -> Self { - match error { - GraphDbError::Cancelled => Self::Cancelled, - GraphDbError::BudgetExhausted { .. } | GraphDbError::DeadlineExceeded => { - Self::BudgetExhausted - } - GraphDbError::InvalidRequest { message } => Self::Contract(message), - GraphDbError::Corrupt { message } - | GraphDbError::ResetRequired { message } - | GraphDbError::DurabilityUncertain { message } - | GraphDbError::ProjectionMismatch { message, .. } - | GraphDbError::GenerationMismatch { message, .. } => Self::Corrupt(message), - GraphDbError::Conflict { .. } => { - Self::Unavailable("Git topology publication conflict".to_owned()) - } - GraphDbError::Unavailable { message } - | GraphDbError::SealedStoreImmutable { message } => Self::Unavailable(message), - error @ (GraphDbError::SourceCommitmentsUnavailable { .. } - | GraphDbError::SealedRevisionIncompatible { .. }) => { - Self::Unavailable(error.to_string()) - } - GraphDbError::Closed => Self::Unavailable("graph store is closed".to_owned()), + match classify_graph_store_error(error, "Git topology publication conflict") { + GraphStoreFailureClass::Cancelled => Self::Cancelled, + GraphStoreFailureClass::BudgetExhausted => Self::BudgetExhausted, + GraphStoreFailureClass::Contract(message) => Self::Contract(message), + GraphStoreFailureClass::Corrupt(message) => Self::Corrupt(message), + GraphStoreFailureClass::Unavailable(message) => Self::Unavailable(message), } } } diff --git a/crates/tracedecay-graph-db/src/error.rs b/crates/tracedecay-graph-db/src/error.rs index 3911c4ef56..2dead63e82 100644 --- a/crates/tracedecay-graph-db/src/error.rs +++ b/crates/tracedecay-graph-db/src/error.rs @@ -145,6 +145,56 @@ pub enum GraphDbError { Closed, } +/// Store failure folded into a projection error that does not carry graph-db +/// context fields. Callers that keep richer variants map [`GraphDbError`] +/// themselves. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum GraphStoreFailureClass { + Cancelled, + BudgetExhausted, + Contract(String), + Corrupt(String), + Unavailable(String), +} + +/// Classify a store error the way git topology and workflow topology do. +/// +/// `conflict_unavailable` is the caller-specific conflict message. Every other +/// arm is identical for those projections, including the closed-store wording. +#[must_use] +pub fn classify_graph_store_error( + error: GraphDbError, + conflict_unavailable: &str, +) -> GraphStoreFailureClass { + match error { + GraphDbError::Cancelled => GraphStoreFailureClass::Cancelled, + GraphDbError::BudgetExhausted { .. } | GraphDbError::DeadlineExceeded => { + GraphStoreFailureClass::BudgetExhausted + } + GraphDbError::InvalidRequest { message } => GraphStoreFailureClass::Contract(message), + GraphDbError::Corrupt { message } + | GraphDbError::ResetRequired { message } + | GraphDbError::DurabilityUncertain { message } + | GraphDbError::ProjectionMismatch { message, .. } + | GraphDbError::GenerationMismatch { message, .. } => { + GraphStoreFailureClass::Corrupt(message) + } + GraphDbError::Conflict { .. } => { + GraphStoreFailureClass::Unavailable(conflict_unavailable.to_owned()) + } + GraphDbError::Unavailable { message } | GraphDbError::SealedStoreImmutable { message } => { + GraphStoreFailureClass::Unavailable(message) + } + error @ (GraphDbError::SourceCommitmentsUnavailable { .. } + | GraphDbError::SealedRevisionIncompatible { .. }) => { + GraphStoreFailureClass::Unavailable(error.to_string()) + } + GraphDbError::Closed => { + GraphStoreFailureClass::Unavailable("graph store is closed".to_owned()) + } + } +} + impl From for GraphDbError { fn from(error: GraphPublicationStoreErrorV1) -> Self { match error { @@ -232,7 +282,10 @@ pub(crate) fn rollback_failure( #[cfg(test)] mod tests { - use super::{GraphBudgetKind, GraphDbError, rollback_failure}; + use super::{ + GraphBudgetKind, GraphDbError, GraphStoreFailureClass, classify_graph_store_error, + rollback_failure, + }; #[test] fn budget_kind_from_name_round_trips_and_rejects_unnamed() { @@ -256,6 +309,25 @@ mod tests { assert_eq!(GraphBudgetKind::from_name("unnamed"), None); } + #[test] + fn store_failure_classification_keeps_shared_verdicts() { + assert_eq!( + classify_graph_store_error(GraphDbError::Cancelled, "unused"), + GraphStoreFailureClass::Cancelled + ); + assert_eq!( + classify_graph_store_error(GraphDbError::Closed, "unused"), + GraphStoreFailureClass::Unavailable("graph store is closed".to_owned()) + ); + assert_eq!( + classify_graph_store_error( + GraphDbError::conflict("site"), + "Git topology publication conflict", + ), + GraphStoreFailureClass::Unavailable("Git topology publication conflict".to_owned()) + ); + } + #[test] fn rollback_failure_preserves_both_errors_and_context() { assert_eq!( diff --git a/crates/tracedecay-graph-db/src/lib.rs b/crates/tracedecay-graph-db/src/lib.rs index 83ebd4cacd..5d5e36f9e6 100644 --- a/crates/tracedecay-graph-db/src/lib.rs +++ b/crates/tracedecay-graph-db/src/lib.rs @@ -33,7 +33,10 @@ pub use bundle::{ SealedReadBundleWriterV1, load_sealed_read_bundle_artifact, retire_sealed_read_bundle, sweep_aborted_sealed_read_bundle_temporaries, }; -pub use error::{GraphBudgetKind, GraphConflictContextV1, GraphDbError}; +pub use error::{ + GraphBudgetKind, GraphConflictContextV1, GraphDbError, GraphStoreFailureClass, + classify_graph_store_error, +}; pub use generation::{ GraphEntityRef, GraphGenerationDependency, GraphGenerationManifest, GraphGenerationManifestIdentity, GraphGenerationManifestProvider, GraphGenerationRelation, From f138068fbdfc4e5d55c9aa93eabad9d8205e134d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:35:02 +0000 Subject: [PATCH 126/182] refactor(domain): share exact term kind order The lexical row codec and the capability manifest listed the same kinds. Sealed ordinals now come from ExactTechnicalTermKindV1::ORDER. Co-authored-by: Zack Jackson --- .../tracedecay-code-index/src/capabilities.rs | 17 +++------------- .../src/code_intelligence/search.rs | 20 +++++++++++++++++++ .../lexical/projection/artifact/row_codec.rs | 19 ++++-------------- 3 files changed, 27 insertions(+), 29 deletions(-) diff --git a/crates/tracedecay-code-index/src/capabilities.rs b/crates/tracedecay-code-index/src/capabilities.rs index b30bb3f85c..609f1b47f4 100644 --- a/crates/tracedecay-code-index/src/capabilities.rs +++ b/crates/tracedecay-code-index/src/capabilities.rs @@ -43,20 +43,9 @@ pub const GENERATION_SEAL_SEPARATOR: &str = "tracedecay.code-generation-seal.v1" /// The chunk schema revision pinned by this implementation. pub const CHUNK_SCHEMA_REVISION_V1: &str = "code-search-chunk.v1"; -/// The exact-term kinds the query chunker emits. -pub const BASE_EXACT_TERM_KINDS: &[ExactTechnicalTermKindV1] = &[ - ExactTechnicalTermKindV1::WholeSymbol, - ExactTechnicalTermKindV1::QualifiedName, - ExactTechnicalTermKindV1::Path, - ExactTechnicalTermKindV1::CompilerErrorCode, - ExactTechnicalTermKindV1::CompilerErrorText, - ExactTechnicalTermKindV1::RuntimeErrorCode, - ExactTechnicalTermKindV1::RuntimeErrorText, - ExactTechnicalTermKindV1::CliFlag, - ExactTechnicalTermKindV1::ToolName, - ExactTechnicalTermKindV1::ConfigurationKey, - ExactTechnicalTermKindV1::CommitIdentifier, -]; +/// The exact-term kinds the query chunker emits, in codec ordinal order. +pub const BASE_EXACT_TERM_KINDS: &[ExactTechnicalTermKindV1] = + ExactTechnicalTermKindV1::ORDER.as_slice(); /// The edge-authority classes query tree-sitter extraction declares: edges /// derived purely from syntax are `SyntaxExact`; unresolved constructs are diff --git a/crates/tracedecay-domain/src/code_intelligence/search.rs b/crates/tracedecay-domain/src/code_intelligence/search.rs index 6a0d9b3fb0..d72e40ff1f 100644 --- a/crates/tracedecay-domain/src/code_intelligence/search.rs +++ b/crates/tracedecay-domain/src/code_intelligence/search.rs @@ -247,6 +247,26 @@ pub enum ExactTechnicalTermKindV1 { CommitIdentifier, } +impl ExactTechnicalTermKindV1 { + /// Lexical row-codec ordinal and the capability manifest's kind set. + /// + /// The codec stores the index in this array. Append new kinds; reordering + /// changes sealed artifact bytes. + pub const ORDER: [Self; 11] = [ + Self::WholeSymbol, + Self::QualifiedName, + Self::Path, + Self::CompilerErrorCode, + Self::CompilerErrorText, + Self::RuntimeErrorCode, + Self::RuntimeErrorText, + Self::CliFlag, + Self::ToolName, + Self::ConfigurationKey, + Self::CommitIdentifier, + ]; +} + /// One whole exact technical term extracted as evidence. Extraction /// evidence only; protected lexical policy is applied separately. /// diff --git a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/row_codec.rs b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/row_codec.rs index 258ae41391..d79adad4da 100644 --- a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/row_codec.rs +++ b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/row_codec.rs @@ -66,19 +66,8 @@ const GRAIN_ORDER: [CodeSearchChunkGrainV1; 5] = [ CodeSearchChunkGrainV1::FileWindow, ]; -const EXACT_TERM_KIND_ORDER: [ExactTechnicalTermKindV1; 11] = [ - ExactTechnicalTermKindV1::WholeSymbol, - ExactTechnicalTermKindV1::QualifiedName, - ExactTechnicalTermKindV1::Path, - ExactTechnicalTermKindV1::CompilerErrorCode, - ExactTechnicalTermKindV1::CompilerErrorText, - ExactTechnicalTermKindV1::RuntimeErrorCode, - ExactTechnicalTermKindV1::RuntimeErrorText, - ExactTechnicalTermKindV1::CliFlag, - ExactTechnicalTermKindV1::ToolName, - ExactTechnicalTermKindV1::ConfigurationKey, - ExactTechnicalTermKindV1::CommitIdentifier, -]; +const EXACT_TERM_KIND_ORDER: &[ExactTechnicalTermKindV1] = + ExactTechnicalTermKindV1::ORDER.as_slice(); /// Compact row payload: drop identities already stored as columns or /// generation metadata, and reconstruct ASCII-normalized text on read. @@ -525,7 +514,7 @@ fn encode_binary( put_varint(&mut out, length_u64(row.exact_terms.len())?); for term in &row.exact_terms { out.push(ordinal_of( - &EXACT_TERM_KIND_ORDER, + EXACT_TERM_KIND_ORDER, &term.kind(), "exact term kind", )?); @@ -674,7 +663,7 @@ fn decode_binary( } let mut exact_terms = Vec::with_capacity(term_count); for _ in 0..term_count { - let kind = *from_ordinal(&EXACT_TERM_KIND_ORDER, cursor.take_u8()?, "exact term kind")?; + let kind = *from_ordinal(EXACT_TERM_KIND_ORDER, cursor.take_u8()?, "exact term kind")?; let original_bytes = cursor.take_bytes()?.to_vec(); let span = SourceSpan { start_byte: cursor.take_varint()?, From 255c13d8197f5b594643b2f4d05cd7d866f519bf Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:35:06 +0000 Subject: [PATCH 127/182] simplify(pass-2/5): share saturating unix-seconds stamps Co-authored-by: Zack Jackson --- crates/tracedecay-daemon-identity/src/authority.rs | 7 +------ crates/tracedecay-daemon-service/src/doctor_kernel.rs | 9 +-------- crates/tracedecay-lsp/src/analyzer/client.rs | 10 +--------- crates/tracedecay-runtime-core/src/tracedecay.rs | 8 ++++++++ 4 files changed, 11 insertions(+), 23 deletions(-) diff --git a/crates/tracedecay-daemon-identity/src/authority.rs b/crates/tracedecay-daemon-identity/src/authority.rs index addb4b59db..fbd1cebe02 100644 --- a/crates/tracedecay-daemon-identity/src/authority.rs +++ b/crates/tracedecay-daemon-identity/src/authority.rs @@ -5,8 +5,6 @@ use std::fs::OpenOptions; use std::io::{Read, Seek, SeekFrom, Write}; use std::net::SocketAddr; use std::path::{Path, PathBuf}; -use std::time::{SystemTime, UNIX_EPOCH}; - use serde::{Deserialize, Deserializer, Serialize}; use tracedecay_domain::{BrainId, UserProfileId}; use tracedecay_runtime_core::path_safety::{ @@ -165,13 +163,10 @@ impl DaemonAuthority { let profile_identity = crate::profile_identity::load_or_create_pinned(&profile_root, pinned_identity)?; let prior_epoch = prior_record.as_ref().map_or(0, |record| record.epoch); - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default(); let record = DaemonAuthorityRecord { pid: std::process::id(), process_run_id: tracedecay_runtime_core::runtime_identity::process_run_id().to_string(), - started_at_unix_secs: i64::try_from(now.as_secs()).unwrap_or(i64::MAX), + started_at_unix_secs: tracedecay_runtime_core::tracedecay::saturating_unix_secs(), epoch: prior_epoch.saturating_add(1), version: version.to_string(), endpoint: canonical_endpoint(endpoint)?, diff --git a/crates/tracedecay-daemon-service/src/doctor_kernel.rs b/crates/tracedecay-daemon-service/src/doctor_kernel.rs index d0fe8bc9bf..b260033bd5 100644 --- a/crates/tracedecay-daemon-service/src/doctor_kernel.rs +++ b/crates/tracedecay-daemon-service/src/doctor_kernel.rs @@ -11,8 +11,6 @@ use std::collections::BTreeSet; use std::path::{Path, PathBuf}; use std::sync::Arc; -use std::time::{SystemTime, UNIX_EPOCH}; - use tracedecay_code_index_runtime::code_index_scheduler::CodeIndexSchedulerRegistryV1; use tracedecay_code_index_runtime::code_index_scheduler::identity::repository_id_for; use tracedecay_contracts::doctor::{ @@ -1188,12 +1186,7 @@ pub fn doctor_report_request_context( } fn now_secs() -> i64 { - i64::try_from( - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map_or(0, |duration| duration.as_secs()), - ) - .unwrap_or(i64::MAX) + tracedecay_runtime_core::tracedecay::saturating_unix_secs() } #[cfg(test)] diff --git a/crates/tracedecay-lsp/src/analyzer/client.rs b/crates/tracedecay-lsp/src/analyzer/client.rs index 568992246e..0e08f3b4fc 100644 --- a/crates/tracedecay-lsp/src/analyzer/client.rs +++ b/crates/tracedecay-lsp/src/analyzer/client.rs @@ -1411,18 +1411,10 @@ fn code_diagnostic( // resolved later via `DiagnosticBroker::resolve_enclosing_nodes`, // which has access to the indexed nodes for the file. enclosing_node: None, - updated_at: now_unix(), + updated_at: tracedecay_runtime_core::tracedecay::saturating_unix_secs(), } } -fn now_unix() -> i64 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map_or(0, |duration| { - i64::try_from(duration.as_secs()).unwrap_or(i64::MAX) - }) -} - fn code_to_string(value: NumberOrString) -> String { match value { NumberOrString::String(value) => value, diff --git a/crates/tracedecay-runtime-core/src/tracedecay.rs b/crates/tracedecay-runtime-core/src/tracedecay.rs index e3889ed09e..1ee70eb849 100644 --- a/crates/tracedecay-runtime-core/src/tracedecay.rs +++ b/crates/tracedecay-runtime-core/src/tracedecay.rs @@ -20,10 +20,18 @@ fn wall_clock_since_epoch() -> Duration { } /// Returns the current UNIX timestamp in seconds. +/// +/// Overflow keeps the historical wrapping `as i64` cast. Callers that must +/// saturate use [`saturating_unix_secs`]. pub fn current_timestamp() -> i64 { wall_clock_since_epoch().as_secs() as i64 } +/// Unix seconds as `i64`. A pre-epoch clock is `0`; overflow is `i64::MAX`. +pub fn saturating_unix_secs() -> i64 { + i64::try_from(wall_clock_since_epoch().as_secs()).unwrap_or(i64::MAX) +} + /// Shared saturating wall clock for shard, registry, and fact-runtime stamps. /// /// A pre-epoch clock reads as zero and an overflowing microsecond count as From e4b040308ab4e555d687821b18227bbc40456d7e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:35:10 +0000 Subject: [PATCH 128/182] simplify(pass-4/5): share daemon test git helper Co-authored-by: Zack Jackson --- crates/tracedecay/src/daemon/tests.rs | 11 ++++++++ .../src/daemon/tests/invocation_ownership.rs | 22 ++++------------ .../daemon/tests/multi_root_execute_mcp.rs | 23 ++++------------- .../src/daemon/tests/multi_root_journey.rs | 25 ++++++------------- 4 files changed, 28 insertions(+), 53 deletions(-) diff --git a/crates/tracedecay/src/daemon/tests.rs b/crates/tracedecay/src/daemon/tests.rs index e4a0ec963c..f9e8bb94ba 100644 --- a/crates/tracedecay/src/daemon/tests.rs +++ b/crates/tracedecay/src/daemon/tests.rs @@ -52,6 +52,17 @@ mod scheduler_config; mod scheduler_shutdown; mod socket; +#[cfg(unix)] +fn git(root: &std::path::Path, args: &[&str]) { + let status = Command::new("git") + .arg("-C") + .arg(root) + .args(args) + .status() + .expect("run Git fixture command"); + assert!(status.success(), "git {args:?}"); +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(super) enum ObservedMcpRoute { Rmcp, diff --git a/crates/tracedecay/src/daemon/tests/invocation_ownership.rs b/crates/tracedecay/src/daemon/tests/invocation_ownership.rs index bb453ae7d7..ac120348ea 100644 --- a/crates/tracedecay/src/daemon/tests/invocation_ownership.rs +++ b/crates/tracedecay/src/daemon/tests/invocation_ownership.rs @@ -1,9 +1,7 @@ #![cfg(unix)] use std::future::Future; -use std::path::Path; use std::pin::Pin; -use std::process::Command; use tempfile::TempDir; use tracedecay_contracts::retained_surfaces::{MemoryStatusRequestV1, RetainedSurfaceRequestV1}; @@ -30,16 +28,6 @@ use tracedecay_daemon_service::{ DaemonInvocationProblem, ProjectRuntimePublicationStateV1, RegisteredRetainedRuntime, }; -fn git(root: &Path, args: &[&str]) { - let status = Command::new("git") - .arg("-C") - .arg(root) - .args(args) - .status() - .expect("run Git fixture command"); - assert!(status.success(), "git {args:?}"); -} - async fn committed_fixture( label: &str, ) -> ( @@ -74,14 +62,14 @@ async fn unopened_committed_fixture( .expect("committed invocation source"); let client_identity = test_client_identity_for(profile_root.clone()); initialize_test_project(&project, &client_identity).await; - git(&project, &["init", "--quiet"]); - git(&project, &["config", "user.name", "TraceDecay Test"]); - git( + super::git(&project, &["init", "--quiet"]); + super::git(&project, &["config", "user.name", "TraceDecay Test"]); + super::git( &project, &["config", "user.email", "tracedecay@example.invalid"], ); - git(&project, &["add", "."]); - git(&project, &["commit", "--quiet", "-m", "base"]); + super::git(&project, &["add", "."]); + super::git(&project, &["commit", "--quiet", "-m", "base"]); let project_alias = temp.path().join("project-alias"); std::os::unix::fs::symlink(&project, &project_alias).expect("committed project alias"); let handshake = DaemonHandshake { diff --git a/crates/tracedecay/src/daemon/tests/multi_root_execute_mcp.rs b/crates/tracedecay/src/daemon/tests/multi_root_execute_mcp.rs index 2cafb1a661..e3f8734989 100644 --- a/crates/tracedecay/src/daemon/tests/multi_root_execute_mcp.rs +++ b/crates/tracedecay/src/daemon/tests/multi_root_execute_mcp.rs @@ -6,9 +6,6 @@ #![cfg(unix)] -use std::path::Path; -use std::process::Command; - use serde_json::{Value, json}; use tempfile::TempDir; use tracedecay_contracts::{ @@ -33,30 +30,20 @@ const SCOPE_SET_ID: &str = "scope-set.mcp-execute-proof"; const ALPHA_NAME: &str = "alpha_marker"; const BETA_NAME: &str = "beta_marker"; -fn git(root: &Path, args: &[&str]) { - let status = Command::new("git") - .arg("-C") - .arg(root) - .args(args) - .status() - .expect("run Git fixture command"); - assert!(status.success(), "git {args:?}"); -} - fn repository(source: &str) -> TempDir { let repository = TempDir::new().expect("repository"); - git(repository.path(), &["init", "--quiet"]); - git( + super::git(repository.path(), &["init", "--quiet"]); + super::git( repository.path(), &["config", "user.name", "TraceDecay Test"], ); - git( + super::git( repository.path(), &["config", "user.email", "tracedecay@example.com"], ); std::fs::write(repository.path().join("lib.rs"), source).expect("source"); - git(repository.path(), &["add", "."]); - git(repository.path(), &["commit", "--quiet", "-m", "base"]); + super::git(repository.path(), &["add", "."]); + super::git(repository.path(), &["commit", "--quiet", "-m", "base"]); repository } diff --git a/crates/tracedecay/src/daemon/tests/multi_root_journey.rs b/crates/tracedecay/src/daemon/tests/multi_root_journey.rs index d9d7f18a9a..372b664c71 100644 --- a/crates/tracedecay/src/daemon/tests/multi_root_journey.rs +++ b/crates/tracedecay/src/daemon/tests/multi_root_journey.rs @@ -1,7 +1,6 @@ #![cfg(unix)] use std::path::Path; -use std::process::Command; use std::sync::Arc; use std::sync::atomic::Ordering; use std::time::{SystemTime, UNIX_EPOCH}; @@ -27,24 +26,14 @@ use tracedecay_daemon_service::{ DaemonInvocationRequest, parse_daemon_invocation_request, }; -fn git(root: &Path, args: &[&str]) { - let status = Command::new("git") - .arg("-C") - .arg(root) - .args(args) - .status() - .expect("run Git fixture command"); - assert!(status.success(), "git {args:?}"); -} - fn repository() -> TempDir { let repository = TempDir::new().expect("repository"); - git(repository.path(), &["init", "--quiet"]); - git( + super::git(repository.path(), &["init", "--quiet"]); + super::git( repository.path(), &["config", "user.name", "TraceDecay Test"], ); - git( + super::git( repository.path(), &["config", "user.email", "tracedecay@example.com"], ); @@ -53,8 +42,8 @@ fn repository() -> TempDir { "pub fn value() -> u8 { 1 }\n", ) .expect("source"); - git(repository.path(), &["add", "."]); - git(repository.path(), &["commit", "--quiet", "-m", "base"]); + super::git(repository.path(), &["add", "."]); + super::git(repository.path(), &["commit", "--quiet", "-m", "base"]); repository } @@ -67,8 +56,8 @@ fn paginated_repository(prefix: &str) -> TempDir { .collect::>() .concat(); std::fs::write(repository.path().join("lib.rs"), source).expect("paged source"); - git(repository.path(), &["add", "."]); - git( + super::git(repository.path(), &["add", "."]); + super::git( repository.path(), &["commit", "--quiet", "-m", "paged source"], ); From 678f952e1f521def36e4e5f402971c783e82ea3a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:35:20 +0000 Subject: [PATCH 129/182] refactor(domain): share chunk grain order The row codec and capability emission each listed chunk grains. Both now start from CodeSearchChunkGrainV1::ORDER. Co-authored-by: Zack Jackson --- .../tracedecay-code-index/src/capabilities.rs | 17 ++++++----------- .../src/code_intelligence/search.rs | 12 ++++++++++++ .../lexical/projection/artifact/row_codec.rs | 12 +++--------- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/crates/tracedecay-code-index/src/capabilities.rs b/crates/tracedecay-code-index/src/capabilities.rs index 609f1b47f4..645cc07bec 100644 --- a/crates/tracedecay-code-index/src/capabilities.rs +++ b/crates/tracedecay-code-index/src/capabilities.rs @@ -230,18 +230,13 @@ impl BaseCapabilityEmitter { .iter() .map(|descriptor| descriptor.descriptor_revision.clone()) .collect(); - let mut available_grains = vec![ - CodeSearchChunkGrainV1::SymbolSignature, - CodeSearchChunkGrainV1::SymbolBody, - CodeSearchChunkGrainV1::FilePreamble, - CodeSearchChunkGrainV1::FileWindow, - ]; - if descriptors + let admit_members = descriptors .iter() - .any(|descriptor| descriptor.stable_member_spans) - { - available_grains.push(CodeSearchChunkGrainV1::SymbolMember); - } + .any(|descriptor| descriptor.stable_member_spans); + let mut available_grains: Vec = CodeSearchChunkGrainV1::ORDER + .into_iter() + .filter(|grain| *grain != CodeSearchChunkGrainV1::SymbolMember || admit_members) + .collect(); available_grains.sort(); available_grains.dedup(); diff --git a/crates/tracedecay-domain/src/code_intelligence/search.rs b/crates/tracedecay-domain/src/code_intelligence/search.rs index d72e40ff1f..53c2be7f07 100644 --- a/crates/tracedecay-domain/src/code_intelligence/search.rs +++ b/crates/tracedecay-domain/src/code_intelligence/search.rs @@ -181,6 +181,18 @@ pub enum CodeSearchChunkGrainV1 { FileWindow, } +impl CodeSearchChunkGrainV1 { + /// Row-codec grain ordinal. Append new grains; reordering changes sealed + /// artifact bytes. Capability emission sorts a filtered copy of this list. + pub const ORDER: [Self; 5] = [ + Self::SymbolSignature, + Self::SymbolBody, + Self::SymbolMember, + Self::FilePreamble, + Self::FileWindow, + ]; +} + /// Where one chunk lives inside one generation. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(deny_unknown_fields)] diff --git a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/row_codec.rs b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/row_codec.rs index d79adad4da..0883c9a173 100644 --- a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/row_codec.rs +++ b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/row_codec.rs @@ -58,13 +58,7 @@ const FIELD_LENGTH_ORDER: [LexicalFieldV1; 9] = [ LexicalFieldV1::Documentation, ]; -const GRAIN_ORDER: [CodeSearchChunkGrainV1; 5] = [ - CodeSearchChunkGrainV1::SymbolSignature, - CodeSearchChunkGrainV1::SymbolBody, - CodeSearchChunkGrainV1::SymbolMember, - CodeSearchChunkGrainV1::FilePreamble, - CodeSearchChunkGrainV1::FileWindow, -]; +const GRAIN_ORDER: &[CodeSearchChunkGrainV1] = CodeSearchChunkGrainV1::ORDER.as_slice(); const EXACT_TERM_KIND_ORDER: &[ExactTechnicalTermKindV1] = ExactTechnicalTermKindV1::ORDER.as_slice(); @@ -509,7 +503,7 @@ fn encode_binary( } put_varint(&mut out, row.anchor.source_span.start_byte); put_varint(&mut out, row.anchor.source_span.end_byte); - out.push(ordinal_of(&GRAIN_ORDER, &row.anchor.grain, "grain")?); + out.push(ordinal_of(GRAIN_ORDER, &row.anchor.grain, "grain")?); put_varint(&mut out, u64::from(row.anchor.ordinal)); put_varint(&mut out, length_u64(row.exact_terms.len())?); for term in &row.exact_terms { @@ -653,7 +647,7 @@ fn decode_binary( start_byte: cursor.take_varint()?, end_byte: cursor.take_varint()?, }; - let grain = *from_ordinal(&GRAIN_ORDER, cursor.take_u8()?, "grain")?; + let grain = *from_ordinal(GRAIN_ORDER, cursor.take_u8()?, "grain")?; let ordinal = u32::try_from(cursor.take_varint()?).map_err(corrupt)?; let term_count = usize::try_from(cursor.take_varint()?).map_err(corrupt)?; if term_count > cursor.bytes.len() { From b348a7b11c5e871d40176d9230ad365463815892 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:36:06 +0000 Subject: [PATCH 130/182] refactor(domain): share nonnegative sha256 prefix Lexical row ids and clone fingerprints both keep the high bit clear so the prefix fits a signed 64-bit id. One mask does that. Co-authored-by: Zack Jackson --- crates/tracedecay-code-index/src/clones.rs | 8 +--- .../tracedecay-domain/src/canonical_text.rs | 21 ++++++++++ crates/tracedecay-domain/src/lib.rs | 2 +- .../lexical/projection/artifact/schema.rs | 40 +++++++++---------- 4 files changed, 43 insertions(+), 28 deletions(-) diff --git a/crates/tracedecay-code-index/src/clones.rs b/crates/tracedecay-code-index/src/clones.rs index 065c2eb3bb..ba989e0a53 100644 --- a/crates/tracedecay-code-index/src/clones.rs +++ b/crates/tracedecay-code-index/src/clones.rs @@ -13,7 +13,7 @@ use tracedecay_code_extraction::{ }; use tracedecay_domain::{ CodeGenerationId, ManifestDigest, ProjectId, RepositoryId, SourceSpan, SymbolOccurrenceId, - WorktreeId, canonical_json_bytes, canonical_sha256, + WorktreeId, canonical_json_bytes, canonical_sha256, nonnegative_sha256_prefix, }; const BODY_DIGEST_DOMAIN: &str = "tracedecay.clone-body.v1"; @@ -616,11 +616,7 @@ fn winnow_clone_tokens( .map(|window| { let bytes = canonical_json_bytes(&(FINGERPRINT_DOMAIN, window)) .map_err(|error| error.to_string())?; - let digest = Sha256::digest(bytes); - let prefix: [u8; 8] = digest[..8] - .try_into() - .map_err(|error: std::array::TryFromSliceError| error.to_string())?; - Ok(u64::from_be_bytes(prefix) & i64::MAX as u64) + Ok(nonnegative_sha256_prefix(Sha256::digest(bytes).as_slice())) }) .collect::, String>>()?; select_rightmost_minima(&hashes) diff --git a/crates/tracedecay-domain/src/canonical_text.rs b/crates/tracedecay-domain/src/canonical_text.rs index ed5d6c87f4..9c35ac0b5e 100644 --- a/crates/tracedecay-domain/src/canonical_text.rs +++ b/crates/tracedecay-domain/src/canonical_text.rs @@ -150,6 +150,18 @@ pub fn sha256_hex_suffix(value: &str) -> Option<&str> { value.strip_prefix("sha256:") } +/// First eight bytes of a SHA-256 digest as a non-negative integer. +/// +/// Lexical artifact row ids and clone fingerprints both need a value that +/// fits in a signed 64-bit integer, so the high bit is cleared. `digest` +/// must be at least eight bytes; a full SHA-256 digest is 32. +#[must_use] +pub fn nonnegative_sha256_prefix(digest: &[u8]) -> u64 { + let mut prefix = [0_u8; 8]; + prefix.copy_from_slice(&digest[..8]); + u64::from_be_bytes(prefix) & i64::MAX as u64 +} + /// The hex body of a `sha256:`-tagged digest, without the algorithm tag. /// /// Identities that embed a digest under their own namespace all need the @@ -434,6 +446,15 @@ mod tests { ); } + #[test] + fn nonnegative_sha256_prefix_clears_the_high_bit() { + assert_eq!(nonnegative_sha256_prefix(&[0xff; 32]), i64::MAX as u64); + assert_eq!( + nonnegative_sha256_prefix(&[0x01, 0, 0, 0, 0, 0, 0, 2]), + 0x0100_0000_0000_0002 + ); + } + #[test] fn sha256_hex_suffix_strips_only_the_sha256_tag() { let hex = "a".repeat(64); diff --git a/crates/tracedecay-domain/src/lib.rs b/crates/tracedecay-domain/src/lib.rs index c4bcc4d8ba..5b20962a52 100644 --- a/crates/tracedecay-domain/src/lib.rs +++ b/crates/tracedecay-domain/src/lib.rs @@ -42,7 +42,7 @@ pub mod workflow_receipt; pub mod workflow_run; pub use automation::{SESSION_EVIDENCE_BUDGET_EXHAUSTED, SESSION_EVIDENCE_BUDGET_SUPPRESSED}; -pub use canonical_text::{encode_lowercase_hex, sha256_hex_suffix}; +pub use canonical_text::{encode_lowercase_hex, nonnegative_sha256_prefix, sha256_hex_suffix}; pub use code_intelligence::{ BoundedSanitizedText, CanonicalRelationEdgeV1, ChangedCodeChunkSetV1, ChangedCodeChunkV1, ChunkLogicalIdentityV1, ChunkerRevision, CodeChunkProjectionReceiptV1, CodeGenerationId, diff --git a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/schema.rs b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/schema.rs index 5abf876c08..e6409d8cfe 100644 --- a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/schema.rs +++ b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/schema.rs @@ -7,7 +7,7 @@ use super::super::LexicalFieldV1; use super::prepared::PreparedCodeLexicalArtifactPageV1; use super::{CodeLexicalArtifactErrorV1, checkpoint}; use tracedecay_code_index::production::CodeIndexExecutionControlV1; -use tracedecay_domain::ExactFieldV1; +use tracedecay_domain::{ExactFieldV1, nonnegative_sha256_prefix}; /// Revision 10 is the last TEXT-term posting layout. Revision 11 interns /// terms, stores integer field codes, drops redundant serving indexes, and @@ -351,23 +351,24 @@ pub(super) fn exact_field_code_from_encoded( /// batch order, so one-page and multi-page commits of the same source would /// disagree on `vocabulary` / `term_stats` section receipts. pub(super) fn stable_term_id(term: &str) -> i64 { - let mut hasher = Sha256::new(); - hasher.update(b"tracedecay.code-lexical-artifact.term-id.v11\0"); - hasher.update(term.as_bytes()); - let digest = hasher.finalize(); - let mut prefix = [0u8; 8]; - prefix.copy_from_slice(&digest[..8]); - (u64::from_be_bytes(prefix) & i64::MAX as u64) as i64 + stable_prefixed_id( + b"tracedecay.code-lexical-artifact.term-id.v11\0", + term.as_bytes(), + ) } pub(super) fn stable_exact_term_id(term: &[u8]) -> i64 { + stable_prefixed_id( + b"tracedecay.code-lexical-artifact.exact-term-id.v12\0", + term, + ) +} + +fn stable_prefixed_id(domain: &[u8], payload: &[u8]) -> i64 { let mut hasher = Sha256::new(); - hasher.update(b"tracedecay.code-lexical-artifact.exact-term-id.v12\0"); - hasher.update(term); - let digest = hasher.finalize(); - let mut prefix = [0u8; 8]; - prefix.copy_from_slice(&digest[..8]); - (u64::from_be_bytes(prefix) & i64::MAX as u64) as i64 + hasher.update(domain); + hasher.update(payload); + nonnegative_sha256_prefix(hasher.finalize().as_slice()) as i64 } /// Content-addressed `row_dictionary` key over the encoded entry. Pages are @@ -375,13 +376,10 @@ pub(super) fn stable_exact_term_id(term: &[u8]) -> i64 { /// boundaries; a digest of the entry does, and lets a reader verify each /// resolved entry against the id its row referenced. pub(super) fn stable_row_dictionary_id(entry: &[u8]) -> i64 { - let mut hasher = Sha256::new(); - hasher.update(b"tracedecay.code-lexical-artifact.row-dictionary-id.v14\0"); - hasher.update(entry); - let digest = hasher.finalize(); - let mut prefix = [0u8; 8]; - prefix.copy_from_slice(&digest[..8]); - (u64::from_be_bytes(prefix) & i64::MAX as u64) as i64 + stable_prefixed_id( + b"tracedecay.code-lexical-artifact.row-dictionary-id.v14\0", + entry, + ) } /// Stage every dictionary entry the batch references under its page ordinal. From 190145749746517aae387ba12e07aba0222c257f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:36:12 +0000 Subject: [PATCH 131/182] simplify(pass-5/5): share scheduler project task logs Co-authored-by: Zack Jackson --- crates/tracedecay/src/daemon/scheduler.rs | 76 +++++++++---------- .../src/daemon/scheduler/effect_admission.rs | 46 ++++------- 2 files changed, 49 insertions(+), 73 deletions(-) diff --git a/crates/tracedecay/src/daemon/scheduler.rs b/crates/tracedecay/src/daemon/scheduler.rs index cd94a210df..43b3308ff4 100644 --- a/crates/tracedecay/src/daemon/scheduler.rs +++ b/crates/tracedecay/src/daemon/scheduler.rs @@ -29,10 +29,9 @@ use effect_admission::{ }; use host_receipt_review::run_host_receipt_review; -pub(super) fn scheduler_task_log_fields( +fn scheduler_project_task_fields( project_path: &Path, - task: tracedecay_automation_runtime::automation::backend::AgentTaskKind, - outcome: &str, + task: AgentTaskKind, ) -> Vec<(&'static str, String)> { vec![ ("project", project_path.display().to_string()), @@ -40,10 +39,19 @@ pub(super) fn scheduler_task_log_fields( "task", tracedecay_automation_runtime::automation::backend::task_key(task).to_string(), ), - ("outcome", outcome.to_string()), ] } +pub(super) fn scheduler_task_log_fields( + project_path: &Path, + task: AgentTaskKind, + outcome: &str, +) -> Vec<(&'static str, String)> { + let mut fields = scheduler_project_task_fields(project_path, task); + fields.push(("outcome", outcome.to_string())); + fields +} + fn log_scheduler_task_start( project_path: &Path, task: tracedecay_automation_runtime::automation::backend::AgentTaskKind, @@ -56,17 +64,12 @@ fn log_scheduler_task_start( fn scheduler_task_error_log_fields( project_path: &Path, - task: tracedecay_automation_runtime::automation::backend::AgentTaskKind, + task: AgentTaskKind, error: &impl std::fmt::Display, ) -> Vec<(&'static str, String)> { - vec![ - ("project", project_path.display().to_string()), - ( - "task", - tracedecay_automation_runtime::automation::backend::task_key(task).to_string(), - ), - ("error", error.to_string()), - ] + let mut fields = scheduler_project_task_fields(project_path, task); + fields.push(("error", error.to_string())); + fields } fn log_scheduler_task_error( @@ -85,40 +88,28 @@ fn log_scheduler_automation_replay( task: tracedecay_automation_runtime::automation::backend::AgentTaskKind, terminal: &tracedecay_automation_runtime::automation::effect_runtime::AutomationSettledTerminal, ) { - log_daemon_event( - "scheduler_task_application_replay", - &[ - ("project", project_path.display().to_string()), - ( - "task", - tracedecay_automation_runtime::automation::backend::task_key(task).to_owned(), - ), - ( - "terminal", - if terminal.is_completed() { - "completed" - } else if terminal.problem().is_some() { - "problem" - } else { - "skipped" - } - .to_owned(), - ), - ], - ); + let mut fields = scheduler_project_task_fields(project_path, task); + fields.push(( + "terminal", + if terminal.is_completed() { + "completed" + } else if terminal.problem().is_some() { + "problem" + } else { + "skipped" + } + .to_owned(), + )); + log_daemon_event("scheduler_task_application_replay", &fields); } pub(super) fn scheduler_application_problem_log_fields( project_path: &Path, - task: tracedecay_automation_runtime::automation::backend::AgentTaskKind, + task: AgentTaskKind, problem: &tracedecay_automation_runtime::automation::effect_runtime::AutomationSettledProblem, ) -> Vec<(&'static str, String)> { - vec![ - ("project", project_path.display().to_string()), - ( - "task", - tracedecay_automation_runtime::automation::backend::task_key(task).to_owned(), - ), + let mut fields = scheduler_project_task_fields(project_path, task); + fields.extend([ ("request_id", problem.problem.request_id.as_str().to_owned()), ("run_id", problem.run_id.as_str().to_owned()), ( @@ -130,7 +121,8 @@ pub(super) fn scheduler_application_problem_log_fields( "committed_receipt_count", problem.committed_receipts.len().to_string(), ), - ] + ]); + fields } fn scheduler_run_observer( diff --git a/crates/tracedecay/src/daemon/scheduler/effect_admission.rs b/crates/tracedecay/src/daemon/scheduler/effect_admission.rs index 8b551ca686..7044e7226d 100644 --- a/crates/tracedecay/src/daemon/scheduler/effect_admission.rs +++ b/crates/tracedecay/src/daemon/scheduler/effect_admission.rs @@ -27,16 +27,12 @@ pub(super) fn log_scheduler_pre_admission_problem( task: tracedecay_automation_runtime::automation::backend::AgentTaskKind, problem: &tracedecay_contracts::ApplicationProblemEnvelope, ) { - let mut fields = vec![ - ("project", project_path.display().to_string()), - ( - "task", - tracedecay_automation_runtime::automation::backend::task_key(task).to_owned(), - ), + let mut fields = super::scheduler_project_task_fields(project_path, task); + fields.extend([ ("request_id", problem.request_id.as_str().to_owned()), ("problem_kind", format!("{:?}", problem.problem.kind())), ("problem_code", problem.problem.code.clone()), - ]; + ]); match serde_json::to_string(problem) { Ok(envelope) => fields.push(("application_problem", envelope)), Err(error) => fields.push(("observation_error", error.to_string())), @@ -48,18 +44,12 @@ pub(super) fn log_scheduler_admission_conflict( project_path: &Path, task: tracedecay_automation_runtime::automation::backend::AgentTaskKind, ) { - log_daemon_event( - "scheduler_task_automation_admission_conflict", - &[ - ("project", project_path.display().to_string()), - ( - "task", - tracedecay_automation_runtime::automation::backend::task_key(task).to_owned(), - ), - ("outcome", "skipped".to_owned()), - ("reason", "durable_admission_conflict".to_owned()), - ], - ); + let mut fields = super::scheduler_project_task_fields(project_path, task); + fields.extend([ + ("outcome", "skipped".to_owned()), + ("reason", "durable_admission_conflict".to_owned()), + ]); + log_daemon_event("scheduler_task_automation_admission_conflict", &fields); } fn log_scheduler_schedule_skip( @@ -70,18 +60,12 @@ fn log_scheduler_schedule_skip( // Not-due/disabled tasks never reach durable admission; without this // counter a silent schedule skip is indistinguishable from a lost tick. hotpath::gauge!("daemon.effect_admission.deferred_total").inc(1_u64); - log_daemon_event( - "scheduler_task", - &[ - ("project", project_path.display().to_string()), - ( - "task", - tracedecay_automation_runtime::automation::backend::task_key(task).to_owned(), - ), - ("outcome", "skipped".to_owned()), - ("reason", reason.as_str().to_owned()), - ], - ); + let mut fields = super::scheduler_project_task_fields(project_path, task); + fields.extend([ + ("outcome", "skipped".to_owned()), + ("reason", reason.as_str().to_owned()), + ]); + log_daemon_event("scheduler_task", &fields); } #[hotpath::measure(label = "daemon.scheduler.fixed_task_decision", future = true)] From cf990cdd86f463c4e3b08c670724b98cdbc125d4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:36:28 +0000 Subject: [PATCH 132/182] simplify(pass-1/5): share path scope matchers One domain matcher owns descendant-prefix checks. The trailing-slash spelling stays distinct from the literal repository prefix check. Co-authored-by: Zack Jackson --- .../src/primitives/grep_analysis.rs | 4 ++- .../production/extended_primitive.rs | 8 ++---- .../src/primitives/symbol_graph.rs | 2 +- .../src/code_index_scheduler/queries.rs | 2 +- .../src/code_intelligence/identity.rs | 27 +++++++++++++++++++ crates/tracedecay-domain/src/lib.rs | 4 +-- .../domain_suite/repository_scope_contract.rs | 26 +++++++++++++++++- crates/tracedecay-graph-query/src/queries.rs | 2 +- .../tracedecay-graph-query/src/test_risk.rs | 6 ++--- .../src/handlers/analysis/mod.rs | 2 +- .../src/handlers/analysis/unmounted_files.rs | 2 +- crates/tracedecay-mcp/src/handlers/grep.rs | 2 +- .../src/handlers/health/reports.rs | 2 +- .../tracedecay-mcp/src/handlers/info/body.rs | 2 +- .../tracedecay-mcp/src/handlers/info/files.rs | 7 +---- .../src/handlers/info/signature_search.rs | 2 +- .../tracedecay-mcp/src/handlers/info/todos.rs | 2 +- .../src/handlers/info/verified.rs | 7 +++-- crates/tracedecay-runtime-core/src/lib.rs | 1 - .../tracedecay-runtime-core/src/path_scope.rs | 23 ---------------- 20 files changed, 76 insertions(+), 57 deletions(-) delete mode 100644 crates/tracedecay-runtime-core/src/path_scope.rs diff --git a/crates/tracedecay-application/src/primitives/grep_analysis.rs b/crates/tracedecay-application/src/primitives/grep_analysis.rs index 64d18ea4c4..05a14aabda 100644 --- a/crates/tracedecay-application/src/primitives/grep_analysis.rs +++ b/crates/tracedecay-application/src/primitives/grep_analysis.rs @@ -329,7 +329,9 @@ fn effective_scoped_path( match (requested, authorized) { (None, None) => Ok(None), (Some(path), None) | (None, Some(path)) => Ok(Some(path.to_owned())), - (Some(path), Some(scope)) if path == scope || path.starts_with(&format!("{scope}/")) => { + (Some(path), Some(scope)) + if tracedecay_domain::repository_path_matches_scope(path, Some(scope)) => + { Ok(Some(path.to_owned())) } (Some(_), Some(_)) => Err(GrepAnalysisProblemV1::Denied), diff --git a/crates/tracedecay-application/src/primitives/production/extended_primitive.rs b/crates/tracedecay-application/src/primitives/production/extended_primitive.rs index 34fe39e64e..64c3559db3 100644 --- a/crates/tracedecay-application/src/primitives/production/extended_primitive.rs +++ b/crates/tracedecay-application/src/primitives/production/extended_primitive.rs @@ -43,11 +43,6 @@ pub(super) fn public_module_symbols( nodes: Vec, path: &str, ) -> Result, ()> { - let prefix = if path.ends_with('/') { - path.to_owned() - } else { - format!("{path}/") - }; let mut pub_nodes: Vec = nodes .into_iter() .filter(|node| { @@ -61,7 +56,8 @@ pub(super) fn public_module_symbols( else { return false; }; - metadata.visibility == "public" && (file_path == path || file_path.starts_with(&prefix)) + metadata.visibility == "public" + && tracedecay_domain::path_matches_scope(file_path, Some(path)) }) .collect(); pub_nodes.sort_by(|left, right| { diff --git a/crates/tracedecay-application/src/primitives/symbol_graph.rs b/crates/tracedecay-application/src/primitives/symbol_graph.rs index 8f147d2041..99fc7a0ac4 100644 --- a/crates/tracedecay-application/src/primitives/symbol_graph.rs +++ b/crates/tracedecay-application/src/primitives/symbol_graph.rs @@ -1095,7 +1095,7 @@ fn in_scope_parts(binding: Option<&CodeGraphSymbolBindingV1>, scope: &SymbolGrap return false; }; scope.path_prefix.as_deref().is_none_or(|path_prefix| { - tracedecay_runtime_core::path_scope::path_matches_scope(file, Some(path_prefix)) + tracedecay_domain::path_matches_scope(file, Some(path_prefix)) }) } diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/queries.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/queries.rs index b1f2a2b3a7..4629023a5d 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/queries.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/queries.rs @@ -818,7 +818,7 @@ fn bounded_result( } fn path_is_in_code_query_scope(path: &str, scope: &tracedecay_contracts::CodeQueryScope) -> bool { - tracedecay_runtime_core::path_scope::path_matches_scope(path, scope.path_prefix.as_deref()) + tracedecay_domain::path_matches_scope(path, scope.path_prefix.as_deref()) } fn relation_edge_kind_name(kind: RelationEdgeKindV1) -> &'static str { diff --git a/crates/tracedecay-domain/src/code_intelligence/identity.rs b/crates/tracedecay-domain/src/code_intelligence/identity.rs index 6d28992667..ed050cae24 100644 --- a/crates/tracedecay-domain/src/code_intelligence/identity.rs +++ b/crates/tracedecay-domain/src/code_intelligence/identity.rs @@ -18,6 +18,12 @@ use crate::research::id::digest_id; /// Whether a canonical repository-relative path is exactly the requested /// scope or one of its descendants. +/// +/// A trailing slash is a literal prefix character. `src/` matches `src/` and +/// `src//lib.rs`, not `src/lib.rs`. An empty prefix matches only the empty +/// path and paths that start with `/`. [`path_matches_scope`] is the other +/// spelling: a prefix that already ends in `/` matches that directory's +/// children without requiring a second slash. pub fn repository_path_matches_scope(path: &str, scope_prefix: Option<&str>) -> bool { scope_prefix.is_none_or(|prefix| { path == prefix @@ -27,6 +33,27 @@ pub fn repository_path_matches_scope(path: &str, scope_prefix: Option<&str>) -> }) } +/// Whether `path` is exactly `prefix`, or a descendant of it. +/// +/// A prefix that does not end in `/` matches that path and `prefix/...`. +/// A prefix that ends in `/` matches names that start with that exact +/// spelling (`src/` matches `src/lib.rs` and `src/`, not `src`). An empty +/// prefix matches the empty path and any path that starts with `/`. `None` +/// matches every path. +/// +/// Unlike [`repository_path_matches_scope`], a single trailing slash is the +/// directory separator, not a character that must be followed by another `/`. +pub fn path_matches_scope(path: &str, scope_prefix: Option<&str>) -> bool { + scope_prefix.is_none_or(|prefix| { + let with_slash = if prefix.ends_with('/') { + prefix.to_string() + } else { + format!("{prefix}/") + }; + path.starts_with(&with_slash) || path == prefix + }) +} + /// Reject code identities that are empty, untrimmed, over 512 bytes, or carry /// control characters. use crate::canonical_text::validate_canonical_identity as validate_code_identity; diff --git a/crates/tracedecay-domain/src/lib.rs b/crates/tracedecay-domain/src/lib.rs index c4bcc4d8ba..abf49b5671 100644 --- a/crates/tracedecay-domain/src/lib.rs +++ b/crates/tracedecay-domain/src/lib.rs @@ -67,8 +67,8 @@ pub use code_intelligence::{ is_cli_flag_token, is_commit_hash, is_commit_identifier_token, is_compiler_error_code_token, is_configuration_key_token, is_identifier_token, is_path_shape, is_path_token, is_qualified_name_token, is_runtime_error_code_token, is_technical_token_char, - is_tool_name_token, projection_batch_publication_digest, repository_path_matches_scope, - split_subtokens, technical_tokens, validate_code_logical_path, + is_tool_name_token, path_matches_scope, projection_batch_publication_digest, + repository_path_matches_scope, split_subtokens, technical_tokens, validate_code_logical_path, }; pub use configuration::{ ACCESS_RULES_SETTING_KEY, ANALYZER_SETTINGS_SETTING_KEY, AUTOMATION_SETTINGS_SETTING_KEY, diff --git a/crates/tracedecay-domain/tests/domain_suite/repository_scope_contract.rs b/crates/tracedecay-domain/tests/domain_suite/repository_scope_contract.rs index 6a9320cd96..9717659341 100644 --- a/crates/tracedecay-domain/tests/domain_suite/repository_scope_contract.rs +++ b/crates/tracedecay-domain/tests/domain_suite/repository_scope_contract.rs @@ -1,4 +1,4 @@ -use tracedecay_domain::repository_path_matches_scope; +use tracedecay_domain::{path_matches_scope, repository_path_matches_scope}; #[test] fn scope_matches_itself_and_descendants_only() { @@ -19,3 +19,27 @@ fn scope_matches_itself_and_descendants_only() { Some("src") )); } + +#[test] +fn trailing_slash_is_literal_for_repository_scope_and_a_separator_for_path_scope() { + assert!(repository_path_matches_scope("src/", Some("src/"))); + assert!(repository_path_matches_scope("src//lib.rs", Some("src/"))); + assert!(!repository_path_matches_scope("src/lib.rs", Some("src/"))); + assert!(repository_path_matches_scope("", Some(""))); + assert!(repository_path_matches_scope("/src", Some(""))); + assert!(!repository_path_matches_scope("src", Some(""))); + assert!(!repository_path_matches_scope("/src", Some("/"))); + + assert!(path_matches_scope("src/lib.rs", Some("src"))); + assert!(path_matches_scope("src", Some("src"))); + assert!(!path_matches_scope("src2/lib.rs", Some("src"))); + assert!(path_matches_scope("src/lib.rs", None)); + assert!(path_matches_scope("src/lib.rs", Some("src/"))); + assert!(path_matches_scope("src/", Some("src/"))); + assert!(!path_matches_scope("src", Some("src/"))); + assert!(path_matches_scope("", Some(""))); + assert!(path_matches_scope("/a", Some(""))); + assert!(!path_matches_scope("a", Some(""))); + assert!(path_matches_scope("/src", Some("/"))); + assert!(!path_matches_scope("src", Some("/"))); +} diff --git a/crates/tracedecay-graph-query/src/queries.rs b/crates/tracedecay-graph-query/src/queries.rs index 4addc3d0f3..91dc77d704 100644 --- a/crates/tracedecay-graph-query/src/queries.rs +++ b/crates/tracedecay-graph-query/src/queries.rs @@ -257,7 +257,7 @@ impl<'a> GraphQueryManager<'a> { .as_ref() .and_then(|binding| binding.logical_path.as_deref()) .is_some_and(|path| { - tracedecay_runtime_core::path_scope::path_matches_scope(path, path_prefix) + tracedecay_domain::path_matches_scope(path, path_prefix) }) && (kind_filter.is_empty() || kind_filter.contains(metadata.kind.as_str())) && (include_public || metadata.visibility != "public") diff --git a/crates/tracedecay-graph-query/src/test_risk.rs b/crates/tracedecay-graph-query/src/test_risk.rs index bc06c54dc8..9992510eee 100644 --- a/crates/tracedecay-graph-query/src/test_risk.rs +++ b/crates/tracedecay-graph-query/src/test_risk.rs @@ -159,7 +159,7 @@ pub async fn analyze_test_risk( && !n.skip_test_coverage && !n.qualified_name.contains("::tests::") }) - .filter(|n| tracedecay_runtime_core::path_scope::path_matches_scope(&n.file, path_prefix)) + .filter(|n| tracedecay_domain::path_matches_scope(&n.file, path_prefix)) .collect(); let excluded_count = eligible_fns @@ -208,7 +208,7 @@ pub async fn analyze_test_risk( && n.skip_test_coverage && !is_test_file(&n.file) && is_source_file(&n.file) - && tracedecay_runtime_core::path_scope::path_matches_scope(&n.file, path_prefix) + && tracedecay_domain::path_matches_scope(&n.file, path_prefix) && !n.qualified_name.contains("::tests::") }) .count(); @@ -342,7 +342,7 @@ pub fn verified_test_evidence( .into_iter() .map(|file| file.logical_path) .filter(|path| { - tracedecay_runtime_core::path_scope::path_matches_scope(path, Some(prefix)) + tracedecay_domain::path_matches_scope(path, Some(prefix)) }) .collect::>() }) diff --git a/crates/tracedecay-mcp/src/handlers/analysis/mod.rs b/crates/tracedecay-mcp/src/handlers/analysis/mod.rs index ea03ff9330..b35c5c5644 100644 --- a/crates/tracedecay-mcp/src/handlers/analysis/mod.rs +++ b/crates/tracedecay-mcp/src/handlers/analysis/mod.rs @@ -54,7 +54,7 @@ fn path_is_rust(path: &str) -> bool { } fn path_matches_optional_scope(path: &str, scope_prefix: Option<&str>) -> bool { - tracedecay_runtime_core::path_scope::path_matches_scope(path, scope_prefix) + tracedecay_domain::path_matches_scope(path, scope_prefix) } const ANALYSIS_SYMBOL_BUDGET: usize = 500_000; diff --git a/crates/tracedecay-mcp/src/handlers/analysis/unmounted_files.rs b/crates/tracedecay-mcp/src/handlers/analysis/unmounted_files.rs index e0e797d46a..a50d0ec4ae 100644 --- a/crates/tracedecay-mcp/src/handlers/analysis/unmounted_files.rs +++ b/crates/tracedecay-mcp/src/handlers/analysis/unmounted_files.rs @@ -75,7 +75,7 @@ pub async fn handle_unmounted_files( .map(move |entry| (ecosystem.ecosystem, entry)) }) .filter(|(_, entry)| { - tracedecay_runtime_core::path_scope::path_matches_scope( + tracedecay_domain::path_matches_scope( &entry.file, path_filter.as_deref(), ) diff --git a/crates/tracedecay-mcp/src/handlers/grep.rs b/crates/tracedecay-mcp/src/handlers/grep.rs index 6890ca89be..b4635f8a13 100644 --- a/crates/tracedecay-mcp/src/handlers/grep.rs +++ b/crates/tracedecay-mcp/src/handlers/grep.rs @@ -138,7 +138,7 @@ pub async fn handle_grep( .into_iter() .map(GrepHit::from) .filter(|hit| { - tracedecay_runtime_core::path_scope::path_matches_scope(hit.file.as_str(), scope_prefix) + tracedecay_domain::path_matches_scope(hit.file.as_str(), scope_prefix) }) .collect::>(); let truncated = scan.truncated || hits.len() > max_results; diff --git a/crates/tracedecay-mcp/src/handlers/health/reports.rs b/crates/tracedecay-mcp/src/handlers/health/reports.rs index 2a187709df..2c6faee0b9 100644 --- a/crates/tracedecay-mcp/src/handlers/health/reports.rs +++ b/crates/tracedecay-mcp/src/handlers/health/reports.rs @@ -119,7 +119,7 @@ fn verified_gini_values( "verified Gini symbol is missing lineage metadata", ) })?; - if tracedecay_runtime_core::path_scope::path_matches_scope(path, path_prefix) { + if tracedecay_domain::path_matches_scope(path, path_prefix) { symbols.push((symbol.occurrence, path.clone(), metadata.clone())); } } diff --git a/crates/tracedecay-mcp/src/handlers/info/body.rs b/crates/tracedecay-mcp/src/handlers/info/body.rs index d31e3f97ab..ecbfe737e2 100644 --- a/crates/tracedecay-mcp/src/handlers/info/body.rs +++ b/crates/tracedecay-mcp/src/handlers/info/body.rs @@ -174,7 +174,7 @@ fn body_candidates( let path = required_file_path(&candidate)?; let metadata = required_metadata(&candidate)?; if scope_prefix.is_none_or(|scope| { - tracedecay_runtime_core::path_scope::path_matches_scope(path, Some(scope)) + tracedecay_domain::path_matches_scope(path, Some(scope)) }) { let preference = NodeKind::from_str(&metadata.kind) .map_or(u8::MAX, |kind| body_kind_preference(&kind)); diff --git a/crates/tracedecay-mcp/src/handlers/info/files.rs b/crates/tracedecay-mcp/src/handlers/info/files.rs index f4615e320e..07392a243c 100644 --- a/crates/tracedecay-mcp/src/handlers/info/files.rs +++ b/crates/tracedecay-mcp/src/handlers/info/files.rs @@ -25,12 +25,7 @@ pub async fn handle_files( .await?; if let Some(dir) = effective_path(&args, scope_prefix) { - let prefix = if dir.ends_with('/') { - dir.to_string() - } else { - format!("{dir}/") - }; - files.retain(|f| f.path.starts_with(&prefix) || f.path == dir); + files.retain(|f| tracedecay_domain::path_matches_scope(&f.path, Some(dir))); } if let Some(pat) = args.get("pattern").and_then(|v| v.as_str()) { diff --git a/crates/tracedecay-mcp/src/handlers/info/signature_search.rs b/crates/tracedecay-mcp/src/handlers/info/signature_search.rs index 2ca96ba7a9..09a275d327 100644 --- a/crates/tracedecay-mcp/src/handlers/info/signature_search.rs +++ b/crates/tracedecay-mcp/src/handlers/info/signature_search.rs @@ -52,7 +52,7 @@ pub async fn handle_signature_search( continue; } if let Some(prefix) = path_filter - && !tracedecay_runtime_core::path_scope::path_matches_scope(file_path, Some(prefix)) + && !tracedecay_domain::path_matches_scope(file_path, Some(prefix)) { continue; } diff --git a/crates/tracedecay-mcp/src/handlers/info/todos.rs b/crates/tracedecay-mcp/src/handlers/info/todos.rs index 3eb85fea94..c59d0a3ab1 100644 --- a/crates/tracedecay-mcp/src/handlers/info/todos.rs +++ b/crates/tracedecay-mcp/src/handlers/info/todos.rs @@ -106,7 +106,7 @@ pub async fn handle_todos( 'outer: for file in &files { if let Some(prefix) = path.as_deref() - && !tracedecay_runtime_core::path_scope::path_matches_scope(file, Some(prefix)) + && !tracedecay_domain::path_matches_scope(file, Some(prefix)) { continue; } diff --git a/crates/tracedecay-mcp/src/handlers/info/verified.rs b/crates/tracedecay-mcp/src/handlers/info/verified.rs index ae411effd9..9f5453bc07 100644 --- a/crates/tracedecay-mcp/src/handlers/info/verified.rs +++ b/crates/tracedecay-mcp/src/handlers/info/verified.rs @@ -83,14 +83,13 @@ pub(super) fn symbols_in_dir( directory: &str, kinds: &[NodeKind], ) -> Result> { + // Trim every trailing slash first. `repository_path_matches_scope` treats + // a leftover `/` as a literal character, so `src/` would miss `src/lib.rs`. let prefix = directory.trim_end_matches('/'); let mut selected = Vec::new(); for symbol in all_symbols(graph)? { let (metadata, path) = required_symbol_parts(&symbol)?; - let path_matches = path == prefix - || path - .strip_prefix(prefix) - .is_some_and(|rest| rest.starts_with('/')); + let path_matches = tracedecay_domain::repository_path_matches_scope(path, Some(prefix)); let kind_matches = NodeKind::from_str(&metadata.kind).is_some_and(|kind| kinds.contains(&kind)); if path_matches && kind_matches { diff --git a/crates/tracedecay-runtime-core/src/lib.rs b/crates/tracedecay-runtime-core/src/lib.rs index d4dd1a2c35..20a6329b01 100644 --- a/crates/tracedecay-runtime-core/src/lib.rs +++ b/crates/tracedecay-runtime-core/src/lib.rs @@ -108,7 +108,6 @@ pub mod logging; pub mod operation_task_owner; pub mod os_str_bytes; pub mod path_safety; -pub mod path_scope; mod profiled_lock; pub mod resident_memory; pub mod runtime_identity; diff --git a/crates/tracedecay-runtime-core/src/path_scope.rs b/crates/tracedecay-runtime-core/src/path_scope.rs deleted file mode 100644 index bea9effcf1..0000000000 --- a/crates/tracedecay-runtime-core/src/path_scope.rs +++ /dev/null @@ -1,23 +0,0 @@ -pub fn path_matches_scope(path: &str, scope_prefix: Option<&str>) -> bool { - scope_prefix.is_none_or(|prefix| { - let with_slash = if prefix.ends_with('/') { - prefix.to_string() - } else { - format!("{prefix}/") - }; - path.starts_with(&with_slash) || path == prefix - }) -} - -#[cfg(test)] -mod tests { - use super::path_matches_scope; - - #[test] - fn scope_prefix_matches_exact_file_or_descendant() { - assert!(path_matches_scope("src/lib.rs", Some("src"))); - assert!(path_matches_scope("src", Some("src"))); - assert!(!path_matches_scope("src2/lib.rs", Some("src"))); - assert!(path_matches_scope("src/lib.rs", None)); - } -} From c0d675b0453c734db42fb0c9dd1fa25ba0397743 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:36:35 +0000 Subject: [PATCH 133/182] refactor(workflow): share digest-bound journal decode Co-authored-by: Zack Jackson --- crates/tracedecay-domain/src/lib.rs | 5 ++-- .../src/research/canonical.rs | 15 ++++++++++ .../src/research/canonical_tests.rs | 13 ++++++++ .../src/workflow.rs | 30 +++++-------------- .../src/workflow/census.rs | 25 +++++----------- .../src/workflow/run_journal.rs | 13 +++----- 6 files changed, 50 insertions(+), 51 deletions(-) diff --git a/crates/tracedecay-domain/src/lib.rs b/crates/tracedecay-domain/src/lib.rs index c4bcc4d8ba..319e766114 100644 --- a/crates/tracedecay-domain/src/lib.rs +++ b/crates/tracedecay-domain/src/lib.rs @@ -334,8 +334,9 @@ pub use research::{ WorkflowOutputName, WorkflowStepId, WorktreeCaptureAnchorRefV1, WorktreeId, WorktreeInventoryEpoch, WorktreeInventorySnapshotId, canonical_json_bytes, canonical_json_bytes_and_sha256, canonical_json_value, canonical_sha256, - derive_exact_observation_anchor_id, derive_exact_source_occurrence_anchor_id, - derive_git_topology_anchor_id, validate_anchor_lineage_v3, zero_digest, + decode_with_canonical_digest, derive_exact_observation_anchor_id, + derive_exact_source_occurrence_anchor_id, derive_git_topology_anchor_id, + validate_anchor_lineage_v3, zero_digest, }; pub use resource_policy::host_cpu_target; pub use retrieval::{ diff --git a/crates/tracedecay-domain/src/research/canonical.rs b/crates/tracedecay-domain/src/research/canonical.rs index 914e0493a3..5b668d67cd 100644 --- a/crates/tracedecay-domain/src/research/canonical.rs +++ b/crates/tracedecay-domain/src/research/canonical.rs @@ -1,6 +1,7 @@ use std::cell::RefCell; use serde::Serialize; +use serde::de::DeserializeOwned; use serde_json::Value; use sha2::{Digest, Sha256}; @@ -118,6 +119,20 @@ pub fn canonical_json_bytes_and_sha256( Ok((bytes, digest)) } +/// Decode a stored JSON payload and require its canonical digest to match the +/// column that was persisted with it. Journal rows use this so a rewritten +/// payload cannot reuse another row's digest. +pub fn decode_with_canonical_digest(payload: &str, stored_digest: &str) -> Result +where + T: Serialize + DeserializeOwned, +{ + let value: T = serde_json::from_str(payload).map_err(|_| ())?; + match canonical_sha256(&value) { + Ok(digest) if digest.as_str() == stored_digest => Ok(value), + _ => Err(()), + } +} + #[cfg(test)] #[path = "canonical_tests.rs"] mod tests; diff --git a/crates/tracedecay-domain/src/research/canonical_tests.rs b/crates/tracedecay-domain/src/research/canonical_tests.rs index 1c17f2bbb5..4d8a626a64 100644 --- a/crates/tracedecay-domain/src/research/canonical_tests.rs +++ b/crates/tracedecay-domain/src/research/canonical_tests.rs @@ -8,10 +8,23 @@ use super::super::canonical_sink::{ use super::super::canonical_value::{keys_are_canonically_ordered, write_canonical}; use super::{ canonical_json_bytes, canonical_json_bytes_and_sha256, canonical_json_value, canonical_sha256, + decode_with_canonical_digest, }; use serde_json::json; +#[test] +fn decode_with_canonical_digest_accepts_matching_payload_and_rejects_tamper() { + let value = json!({"b": 1, "a": 2}); + let payload = serde_json::to_string(&value).unwrap(); + let digest = canonical_sha256(&value).unwrap(); + let decoded: Value = + decode_with_canonical_digest(&payload, digest.as_str()).expect("matching digest"); + assert_eq!(decoded, value); + assert!(decode_with_canonical_digest::(&payload, "sha256:dead").is_err()); + assert!(decode_with_canonical_digest::("{", digest.as_str()).is_err()); +} + #[test] fn canonical_outputs_match_for_nested_ordering_and_scalars() { let value = json!({ diff --git a/crates/tracedecay-rusqlite-runtime/src/workflow.rs b/crates/tracedecay-rusqlite-runtime/src/workflow.rs index 74f49186c0..8fbee458a4 100644 --- a/crates/tracedecay-rusqlite-runtime/src/workflow.rs +++ b/crates/tracedecay-rusqlite-runtime/src/workflow.rs @@ -96,12 +96,9 @@ impl WorkflowSqliteAuthority { let Some(ExactSqlValue::Text(stored_digest)) = row.values.get(1) else { return Err(WorkflowSqliteAuthorityBuildError::ResetRequired); }; - let definition: WorkflowDefinition = serde_json::from_str(payload) + let definition = tracedecay_domain::decode_with_canonical_digest(payload, stored_digest) .map_err(|_| WorkflowSqliteAuthorityBuildError::ResetRequired)?; - let digest = canonical_sha256(&definition) - .map_err(|_| WorkflowSqliteAuthorityBuildError::ResetRequired)?; - if digest.as_str() != stored_digest - || definition.definition_id() != definition_id + if definition.definition_id() != definition_id || definition.definition_version() != definition_version { return Err(WorkflowSqliteAuthorityBuildError::ResetRequired); @@ -277,13 +274,8 @@ fn decode_definition_source_row( let Some(ExactSqlValue::Text(stored_digest)) = row.values.get(1) else { return Err(definition_authority_unavailable()); }; - let definition: WorkflowDefinition = - serde_json::from_str(payload).map_err(|_| definition_authority_unavailable())?; - let digest = canonical_sha256(&definition).map_err(|_| definition_authority_unavailable())?; - if digest.as_str() != stored_digest { - return Err(definition_authority_unavailable()); - } - Ok(definition) + tracedecay_domain::decode_with_canonical_digest(payload, stored_digest) + .map_err(|_| definition_authority_unavailable()) } fn definition_authority_unavailable() -> WorkflowDefinitionAuthorityError { @@ -435,17 +427,11 @@ fn statement(sql: &str, params: Vec) -> Result Option<&str> { - match values.get(index)? { - ExactSqlValue::Text(value) => Some(value), - _ => None, - } + crate::exact_sql::text_column(values, index) } fn sql_integer(values: &[ExactSqlValue], index: usize) -> Option { - match values.get(index)? { - ExactSqlValue::Integer(value) => Some(*value), - _ => None, - } + crate::exact_sql::integer_column(values, index) } fn version_i64(version: u64) -> Result { @@ -461,11 +447,11 @@ fn encode_definition(definition: &WorkflowDefinition) -> Result { } fn encode_json(value: &T) -> Result { - serde_json::to_string(value).map_err(|_| ()) + crate::exact_sql::encode_json(value, |_| ()) } fn decode_json(payload: &str) -> Result { - serde_json::from_str(payload).map_err(|_| ()) + crate::exact_sql::decode_json(payload, |_| ()) } fn query_tx( diff --git a/crates/tracedecay-rusqlite-runtime/src/workflow/census.rs b/crates/tracedecay-rusqlite-runtime/src/workflow/census.rs index 13701106fc..1b566e3172 100644 --- a/crates/tracedecay-rusqlite-runtime/src/workflow/census.rs +++ b/crates/tracedecay-rusqlite-runtime/src/workflow/census.rs @@ -5,13 +5,13 @@ use tracedecay_contracts::{ WorkflowFanOutCensusStoragePort, }; use tracedecay_domain::{ - ObservabilityTerminalResultV1, RunId, WorkAuthority, WorkflowFanOutCensusV1, WorkflowRunEvent, + ObservabilityTerminalResultV1, RunId, WorkAuthority, WorkflowFanOutCensusV1, WorkflowRunProjection, WorkflowRunStatus, canonical_sha256, }; use super::{ - ExactSqlTransaction, ExactSqlValue, WorkflowSqliteAuthority, decode_json, encode_json, - execute_tx, execute_tx_changed, query_tx, sql_text, + ExactSqlTransaction, ExactSqlValue, WorkflowSqliteAuthority, encode_json, execute_tx, + execute_tx_changed, query_tx, sql_text, }; fn unavailable(_: E) -> WorkflowFanOutCensusError { @@ -22,16 +22,11 @@ fn decode_census( payload: &str, stored_digest: &str, ) -> Result { - let census: WorkflowFanOutCensusV1 = - decode_json(payload).map_err(|_| WorkflowFanOutCensusError::InvalidHistory)?; + let census = tracedecay_domain::decode_with_canonical_digest(payload, stored_digest) + .map_err(|_| WorkflowFanOutCensusError::InvalidHistory)?; census .validate() .map_err(|_| WorkflowFanOutCensusError::InvalidHistory)?; - let digest = - canonical_sha256(&census).map_err(|_| WorkflowFanOutCensusError::InvalidHistory)?; - if digest.as_str() != stored_digest { - return Err(WorkflowFanOutCensusError::InvalidHistory); - } Ok(census) } @@ -117,14 +112,8 @@ fn projection_through_tx( sql_text(&row.values, 0).ok_or(WorkflowFanOutCensusError::InvalidHistory)?; let stored_digest = sql_text(&row.values, 1).ok_or(WorkflowFanOutCensusError::InvalidHistory)?; - let event: WorkflowRunEvent = - decode_json(payload).map_err(|_| WorkflowFanOutCensusError::InvalidHistory)?; - let digest = - canonical_sha256(&event).map_err(|_| WorkflowFanOutCensusError::InvalidHistory)?; - if digest.as_str() != stored_digest { - return Err(WorkflowFanOutCensusError::InvalidHistory); - } - Ok(event) + tracedecay_domain::decode_with_canonical_digest(payload, stored_digest) + .map_err(|_| WorkflowFanOutCensusError::InvalidHistory) }) .collect::, _>>()?; let projection = WorkflowRunProjection::rebuild(&history) diff --git a/crates/tracedecay-rusqlite-runtime/src/workflow/run_journal.rs b/crates/tracedecay-rusqlite-runtime/src/workflow/run_journal.rs index e74819366a..8430a906ec 100644 --- a/crates/tracedecay-rusqlite-runtime/src/workflow/run_journal.rs +++ b/crates/tracedecay-rusqlite-runtime/src/workflow/run_journal.rs @@ -18,8 +18,8 @@ use tracedecay_domain::{ }; use super::{ - ExactSqlTransaction, ExactSqlValue, WorkflowSqliteAuthority, decode_json, encode_json, - execute_tx, query_tx, sql_text, + ExactSqlTransaction, ExactSqlValue, WorkflowSqliteAuthority, encode_json, execute_tx, query_tx, + sql_text, }; fn run_journal_unavailable(_: E) -> WorkflowRunStorageError { @@ -30,13 +30,8 @@ fn decode_event( payload: &str, stored_digest: &str, ) -> Result { - let event: WorkflowRunEvent = - decode_json(payload).map_err(|_| WorkflowRunStorageError::InvalidHistory)?; - let digest = canonical_sha256(&event).map_err(|_| WorkflowRunStorageError::InvalidHistory)?; - if digest.as_str() != stored_digest { - return Err(WorkflowRunStorageError::InvalidHistory); - } - Ok(event) + tracedecay_domain::decode_with_canonical_digest(payload, stored_digest) + .map_err(|_| WorkflowRunStorageError::InvalidHistory) } fn history_tx( From 948808556a200934559f8b7e97d522475f694cd0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:36:43 +0000 Subject: [PATCH 134/182] refactor(pass-4/5): drop LCM contract reexports session::lcm forwarded tracedecay-lcm contracts and compression policy with no callers. Authority stays; those crates stay the import path. Co-authored-by: Zack Jackson --- .../src/session/lcm/mod.rs | 22 ++++--------------- 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/crates/tracedecay-session-memory/src/session/lcm/mod.rs b/crates/tracedecay-session-memory/src/session/lcm/mod.rs index ead7230a2d..bf732b13f8 100644 --- a/crates/tracedecay-session-memory/src/session/lcm/mod.rs +++ b/crates/tracedecay-session-memory/src/session/lcm/mod.rs @@ -1,16 +1,10 @@ -//! Application ownership of the LCM authority boundary. +//! Application-owned LCM authority boundary. //! -//! `contracts` holds the retrieval value types and their containment rules; -//! `compression_policy` holds provider-neutral token, overflow, and atomic -//! chunk-selection rules. Hydration shaping lives in -//! `tracedecay-session-temporal-store` and is not re-exported here. The LCM -//! engine crate owns the contract and policy surfaces, and the registered -//! temporal adapters depend on them directly, so neither side has to reach -//! through the other. +//! Retrieval contracts and compression policy live in `tracedecay-lcm`. +//! Hydration shaping lives in `tracedecay-session-temporal-store`. Neither +//! surface is re-exported here; callers already depend on those crates. pub mod authority; -pub use tracedecay_lcm::compression_policy; -pub use tracedecay_lcm::contracts; pub use authority::{ LcmAuthorityFuture, LcmAuthorityInvocation, LcmAuthorityOperation, LcmAuthorityOutcome, @@ -19,11 +13,3 @@ pub use authority::{ LcmCompressionEvidence, LcmDoctorQuery, LcmHostProtocol, LcmStatusQuery, LcmTranscriptIngestCommand, lcm_authority_operation_identity, }; -pub use contracts::{ - LcmContentRange, LcmContentSlice, LcmDescribeExternalPayload, LcmDescribeRequest, - LcmDescribeResponse, LcmDescribeSourceOverview, LcmDescribeSummaryNode, LcmDescribeTarget, - LcmError, LcmExpandRequest, LcmExpandResponse, LcmExpandSourcePagination, LcmExpandTarget, - LcmExpandedSummarySource, LcmPayloadExpansion, LcmPayloadRef, LcmRawMessage, - LcmRawMessageOverview, LcmSourceRef, LcmStorageKind, LcmSummaryNode, LcmSummaryNodeOverview, - validate_payload_ref, -}; From 8345c7b31d685400dd32c901fd6af877620d5d17 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:37:38 +0000 Subject: [PATCH 135/182] refactor(domain): share canonical message role parse Known role labels parse in one place. Host aliases stay at the host boundary, and the identical unknown-fallback tables are gone. Co-authored-by: Zack Jackson --- .../src/claude/canonical.rs | 17 ++--- crates/tracedecay-capture/src/codex.rs | 12 +--- crates/tracedecay-capture/src/content.rs | 50 +++++++++++++++ crates/tracedecay-capture/src/cursor.rs | 22 +++---- crates/tracedecay-capture/src/kimi.rs | 9 +-- crates/tracedecay-capture/src/opencode.rs | 12 +--- crates/tracedecay-domain/src/observation.rs | 63 +++++++++++++++++++ .../src/runtime/hosts/hermes/observation.rs | 9 +-- .../observation/snapshot_observation.rs | 12 ++-- 9 files changed, 138 insertions(+), 68 deletions(-) diff --git a/crates/tracedecay-capture/src/claude/canonical.rs b/crates/tracedecay-capture/src/claude/canonical.rs index 84cb8a1c95..aa2e43c59d 100644 --- a/crates/tracedecay-capture/src/claude/canonical.rs +++ b/crates/tracedecay-capture/src/claude/canonical.rs @@ -571,17 +571,12 @@ fn system_hook_has_signal(native: &Value) -> bool { } fn canonical_role(message: &Value, record_kind: &str) -> CanonicalMessageRoleV1 { - match message - .get("role") - .and_then(Value::as_str) - .unwrap_or(record_kind) - { - "user" => CanonicalMessageRoleV1::User, - "assistant" => CanonicalMessageRoleV1::Assistant, - "system" => CanonicalMessageRoleV1::System, - "tool" => CanonicalMessageRoleV1::Tool, - _ => CanonicalMessageRoleV1::Unknown, - } + CanonicalMessageRoleV1::from_wire_label( + message + .get("role") + .and_then(Value::as_str) + .unwrap_or(record_kind), + ) } fn optional_id(native: &Value, keys: &[&str]) -> Option { diff --git a/crates/tracedecay-capture/src/codex.rs b/crates/tracedecay-capture/src/codex.rs index d2cf0b6a25..01063b2dd7 100644 --- a/crates/tracedecay-capture/src/codex.rs +++ b/crates/tracedecay-capture/src/codex.rs @@ -744,7 +744,7 @@ fn append_codex_response_item_facts( return; } facts.push(CanonicalObservationFactV1::Message { - role: canonical_message_role(role), + role: crate::content::canonical_message_role(role), content, model: payload .get("model") @@ -895,16 +895,6 @@ fn timestamp_from_record(record: &Value) -> Option { .and_then(parse_rfc3339_timestamp) } -fn canonical_message_role(role: Option<&str>) -> CanonicalMessageRoleV1 { - match role { - Some("user") => CanonicalMessageRoleV1::User, - Some("assistant") => CanonicalMessageRoleV1::Assistant, - Some("system" | "developer") => CanonicalMessageRoleV1::System, - Some("tool") => CanonicalMessageRoleV1::Tool, - _ => CanonicalMessageRoleV1::Unknown, - } -} - fn canonical_native_observation_id( native_id: Option<&str>, fallback: &ObservationId, diff --git a/crates/tracedecay-capture/src/content.rs b/crates/tracedecay-capture/src/content.rs index 804e26703b..1abcf7c6c1 100644 --- a/crates/tracedecay-capture/src/content.rs +++ b/crates/tracedecay-capture/src/content.rs @@ -1,4 +1,5 @@ use serde_json::Value; +use tracedecay_domain::CanonicalMessageRoleV1; /// Whether a native message `content` value carries nothing renderable: /// null, blank text, or an empty collection. Numbers and booleans count as @@ -12,3 +13,52 @@ pub fn content_is_empty(content: &Value) -> bool { Value::Bool(_) | Value::Number(_) => false, } } + +/// Cursor and Codex share this map. `developer` is their system alias; every +/// other label, including a missing role, uses the canonical wire parser. +pub(crate) fn canonical_message_role(role: Option<&str>) -> CanonicalMessageRoleV1 { + match role { + Some("developer") => CanonicalMessageRoleV1::System, + Some(label) => CanonicalMessageRoleV1::from_wire_label(label), + None => CanonicalMessageRoleV1::Unknown, + } +} + +#[cfg(test)] +mod tests { + use tracedecay_domain::CanonicalMessageRoleV1; + + use super::canonical_message_role; + + #[test] + fn developer_is_system_and_other_labels_use_the_canonical_map() { + assert_eq!( + canonical_message_role(Some("developer")), + CanonicalMessageRoleV1::System + ); + assert_eq!( + canonical_message_role(Some("user")), + CanonicalMessageRoleV1::User + ); + assert_eq!( + canonical_message_role(Some("assistant")), + CanonicalMessageRoleV1::Assistant + ); + assert_eq!( + canonical_message_role(Some("system")), + CanonicalMessageRoleV1::System + ); + assert_eq!( + canonical_message_role(Some("tool")), + CanonicalMessageRoleV1::Tool + ); + assert_eq!( + canonical_message_role(Some("model")), + CanonicalMessageRoleV1::Unknown + ); + assert_eq!( + canonical_message_role(None), + CanonicalMessageRoleV1::Unknown + ); + } +} diff --git a/crates/tracedecay-capture/src/cursor.rs b/crates/tracedecay-capture/src/cursor.rs index ecf22f8468..7e886cd0da 100644 --- a/crates/tracedecay-capture/src/cursor.rs +++ b/crates/tracedecay-capture/src/cursor.rs @@ -1,10 +1,10 @@ use serde_json::Value; use sha2::{Digest, Sha256}; use tracedecay_domain::{ - CanonicalGitEvidenceKindV1, CanonicalMessageRoleV1, CanonicalObservationEnvelopeV1, - CanonicalObservationEvidenceV1, CanonicalObservationFactV1, CanonicalObservationRelationsV1, - CanonicalReasoningVisibilityV1, CanonicalUnknownStateV1, CanonicalWorkflowEvidenceKindV1, - ObservationId, ObservationOrderingDomainV1, ObservationPositionalOccurrenceV1, ProviderId, + CanonicalGitEvidenceKindV1, CanonicalObservationEnvelopeV1, CanonicalObservationEvidenceV1, + CanonicalObservationFactV1, CanonicalObservationRelationsV1, CanonicalReasoningVisibilityV1, + CanonicalUnknownStateV1, CanonicalWorkflowEvidenceKindV1, ObservationId, + ObservationOrderingDomainV1, ObservationPositionalOccurrenceV1, ProviderId, ProviderUsageCounterSemanticsV1, ProviderUsageCountersV1, ProviderUsageModelV1, ProviderUsageScopeV1, SessionId, }; @@ -137,7 +137,9 @@ fn normalize_cursor_record( if let Some(content) = content { if let Some(message_content) = canonical_cursor_message_content(content) { facts.push(CanonicalObservationFactV1::Message { - role: canonical_message_role(native.get("role").and_then(Value::as_str)), + role: crate::content::canonical_message_role( + native.get("role").and_then(Value::as_str), + ), content: message_content, model: cursor_record_message_model(native, message.unwrap_or(native)).or_else( || { @@ -567,16 +569,6 @@ pub fn cursor_projected_message_id( ObservationId::new(message_id).map_err(|_| ObservationRecordParseErrorV1::NormalizationFailed) } -fn canonical_message_role(role: Option<&str>) -> CanonicalMessageRoleV1 { - match role { - Some("user") => CanonicalMessageRoleV1::User, - Some("assistant") => CanonicalMessageRoleV1::Assistant, - Some("system" | "developer") => CanonicalMessageRoleV1::System, - Some("tool") => CanonicalMessageRoleV1::Tool, - _ => CanonicalMessageRoleV1::Unknown, - } -} - fn canonical_native_observation_id( native_id: Option<&str>, fallback: &ObservationId, diff --git a/crates/tracedecay-capture/src/kimi.rs b/crates/tracedecay-capture/src/kimi.rs index ad9e90b837..8797e1f809 100644 --- a/crates/tracedecay-capture/src/kimi.rs +++ b/crates/tracedecay-capture/src/kimi.rs @@ -359,13 +359,10 @@ fn append_tool_result( } fn canonical_role(role: &str) -> Result { - match role { - "user" => Ok(CanonicalMessageRoleV1::User), - "assistant" => Ok(CanonicalMessageRoleV1::Assistant), - "system" | "_system_prompt" => Ok(CanonicalMessageRoleV1::System), - "tool" => Ok(CanonicalMessageRoleV1::Tool), - _ => Err(invalid()), + if role == "_system_prompt" { + return Ok(CanonicalMessageRoleV1::System); } + CanonicalMessageRoleV1::from_known_label(role).ok_or_else(invalid) } fn content_text(content: &Value) -> Option<&str> { diff --git a/crates/tracedecay-capture/src/opencode.rs b/crates/tracedecay-capture/src/opencode.rs index 4a34f50cb0..4728b06a9e 100644 --- a/crates/tracedecay-capture/src/opencode.rs +++ b/crates/tracedecay-capture/src/opencode.rs @@ -56,7 +56,7 @@ fn normalize_opencode_record( let mut facts = Vec::new(); if let Some(content) = message_content(parts) { facts.push(CanonicalObservationFactV1::Message { - role: canonical_role(role), + role: CanonicalMessageRoleV1::from_wire_label(role), content, model: message .pointer("/model/modelID") @@ -290,16 +290,6 @@ fn message_content(parts: &[Value]) -> Option { (!content.is_empty()).then_some(Value::Array(content)) } -fn canonical_role(role: &str) -> CanonicalMessageRoleV1 { - match role { - "user" => CanonicalMessageRoleV1::User, - "assistant" => CanonicalMessageRoleV1::Assistant, - "system" => CanonicalMessageRoleV1::System, - "tool" => CanonicalMessageRoleV1::Tool, - _ => CanonicalMessageRoleV1::Unknown, - } -} - /// OpenCode's `time.created` is strictly numeric; string forms stay /// unsupported, so only the millis/seconds normalization is shared. fn timestamp_secs(value: Option<&Value>) -> Option { diff --git a/crates/tracedecay-domain/src/observation.rs b/crates/tracedecay-domain/src/observation.rs index 954855587e..5473ffff62 100644 --- a/crates/tracedecay-domain/src/observation.rs +++ b/crates/tracedecay-domain/src/observation.rs @@ -1737,6 +1737,69 @@ pub enum CanonicalReasoningVisibilityV1 { NotApplicable, } +impl CanonicalMessageRoleV1 { + /// Exact canonical role label. Host aliases (`developer`, `model`, + /// `_system_prompt`) stay at the host boundary so a foreign alias cannot + /// become a role for every provider. + pub fn from_known_label(value: &str) -> Option { + match value { + "user" => Some(Self::User), + "assistant" => Some(Self::Assistant), + "system" => Some(Self::System), + "tool" => Some(Self::Tool), + _ => None, + } + } + + /// `from_known_label`, with every other label recorded as [`Self::Unknown`] + /// instead of refused. + pub fn from_wire_label(value: &str) -> Self { + match Self::from_known_label(value) { + Some(role) => role, + None => Self::Unknown, + } + } +} + +#[cfg(test)] +mod canonical_message_role_label_tests { + use super::CanonicalMessageRoleV1; + + #[test] + fn known_labels_parse_and_foreign_aliases_stay_unknown() { + assert_eq!( + CanonicalMessageRoleV1::from_known_label("user"), + Some(CanonicalMessageRoleV1::User) + ); + assert_eq!( + CanonicalMessageRoleV1::from_known_label("assistant"), + Some(CanonicalMessageRoleV1::Assistant) + ); + assert_eq!( + CanonicalMessageRoleV1::from_known_label("system"), + Some(CanonicalMessageRoleV1::System) + ); + assert_eq!( + CanonicalMessageRoleV1::from_known_label("tool"), + Some(CanonicalMessageRoleV1::Tool) + ); + assert_eq!(CanonicalMessageRoleV1::from_known_label("unknown"), None); + assert_eq!(CanonicalMessageRoleV1::from_known_label("developer"), None); + assert_eq!( + CanonicalMessageRoleV1::from_wire_label("user"), + CanonicalMessageRoleV1::User + ); + assert_eq!( + CanonicalMessageRoleV1::from_wire_label("developer"), + CanonicalMessageRoleV1::Unknown + ); + assert_eq!( + CanonicalMessageRoleV1::from_wire_label(""), + CanonicalMessageRoleV1::Unknown + ); + } +} + #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum CanonicalGitEvidenceKindV1 { diff --git a/crates/tracedecay-sessions/src/runtime/hosts/hermes/observation.rs b/crates/tracedecay-sessions/src/runtime/hosts/hermes/observation.rs index 227d3c366e..f0d99b111c 100644 --- a/crates/tracedecay-sessions/src/runtime/hosts/hermes/observation.rs +++ b/crates/tracedecay-sessions/src/runtime/hosts/hermes/observation.rs @@ -345,13 +345,8 @@ pub fn normalize_native_observation( fn canonical_message_role( role: &str, ) -> Result { - match role { - "user" => Ok(CanonicalMessageRoleV1::User), - "assistant" => Ok(CanonicalMessageRoleV1::Assistant), - "system" => Ok(CanonicalMessageRoleV1::System), - "tool" => Ok(CanonicalMessageRoleV1::Tool), - _ => Err(ObservationRecordParseErrorV1::InvalidCanonicalEnvelope), - } + CanonicalMessageRoleV1::from_known_label(role) + .ok_or(ObservationRecordParseErrorV1::InvalidCanonicalEnvelope) } fn append_tool_invocations( diff --git a/crates/tracedecay-sessions/src/runtime/observation/snapshot_observation.rs b/crates/tracedecay-sessions/src/runtime/observation/snapshot_observation.rs index 1b80ac451d..69eab54194 100644 --- a/crates/tracedecay-sessions/src/runtime/observation/snapshot_observation.rs +++ b/crates/tracedecay-sessions/src/runtime/observation/snapshot_observation.rs @@ -220,13 +220,11 @@ pub fn canonical_snapshot_envelope( range: ObservationSourceRangeV1, ) -> Result { let invalid = || ObservationRecordParseErrorV1::NormalizationFailed; - let role = match native.get("role").and_then(Value::as_str) { - Some("user") => CanonicalMessageRoleV1::User, - Some("assistant") => CanonicalMessageRoleV1::Assistant, - Some("system") => CanonicalMessageRoleV1::System, - Some("tool") => CanonicalMessageRoleV1::Tool, - _ => CanonicalMessageRoleV1::Unknown, - }; + let role = native + .get("role") + .and_then(Value::as_str) + .map(CanonicalMessageRoleV1::from_wire_label) + .unwrap_or(CanonicalMessageRoleV1::Unknown); let timestamp = native.get("timestamp").and_then(Value::as_i64); let mut facts = Vec::new(); if let Some(text) = native.get("text").cloned() { From b047c040063d19af90ff08a9cf3a71fe08289302 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:37:49 +0000 Subject: [PATCH 136/182] refactor(lcm): share relationship scope mapping MessageRelationshipScopeV1 owns the wire labels. Session search, the temporal filter, MCP schemas, and the CLI parser use that one map. Co-authored-by: Zack Jackson --- crates/tracedecay-cli/src/cli.rs | 7 +- .../src/retained_surfaces/sdk.rs | 60 +++++++++++++++++ crates/tracedecay-lcm/src/lib.rs | 64 +++++++++++++++---- .../src/definitions/lcm.rs | 4 +- .../src/definitions/session.rs | 3 +- .../src/retained/lcm.rs | 6 +- .../src/retained/session.rs | 14 +--- .../src/session_retrieval.rs | 1 + .../src/session_retrieval/contract.rs | 37 +++++++++-- 9 files changed, 160 insertions(+), 36 deletions(-) diff --git a/crates/tracedecay-cli/src/cli.rs b/crates/tracedecay-cli/src/cli.rs index fc3fe9f1a7..f13259e537 100644 --- a/crates/tracedecay-cli/src/cli.rs +++ b/crates/tracedecay-cli/src/cli.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; use clap::{Args, Parser, Subcommand, ValueEnum, builder::PossibleValuesParser}; +use tracedecay_contracts::retained_surfaces::MessageRelationshipScopeV1; mod automation; pub mod dispatch; @@ -1104,7 +1105,11 @@ pub(crate) struct SessionsSearchArgs { #[arg(long)] pub(crate) provider: Option, /// Relationship scope: all, parents_only, or subagents_only - #[arg(long, default_value = "all", value_parser = ["all", "parents_only", "subagents_only"])] + #[arg( + long, + default_value = "all", + value_parser = PossibleValuesParser::new(MessageRelationshipScopeV1::WIRE) + )] pub(crate) scope: String, /// Semantic message type: all, direct_user, or tool_result #[arg(long, default_value = "all", value_parser = ["all", "direct_user", "tool_result"])] diff --git a/crates/tracedecay-contracts/src/retained_surfaces/sdk.rs b/crates/tracedecay-contracts/src/retained_surfaces/sdk.rs index 459145c32b..3d5b008578 100644 --- a/crates/tracedecay-contracts/src/retained_surfaces/sdk.rs +++ b/crates/tracedecay-contracts/src/retained_surfaces/sdk.rs @@ -178,6 +178,66 @@ pub enum MessageRelationshipScopeV1 { SubagentsOnly, } +impl MessageRelationshipScopeV1 { + /// Advertised order for MCP schemas and the CLI parser. Labels are the + /// serde `snake_case` names. + pub const WIRE: [&'static str; 3] = [ + Self::All.as_str(), + Self::ParentsOnly.as_str(), + Self::SubagentsOnly.as_str(), + ]; + + pub const fn as_str(self) -> &'static str { + match self { + Self::All => "all", + Self::ParentsOnly => "parents_only", + Self::SubagentsOnly => "subagents_only", + } + } + + pub fn parse(value: &str) -> Option { + match value { + "all" => Some(Self::All), + "parents_only" => Some(Self::ParentsOnly), + "subagents_only" => Some(Self::SubagentsOnly), + _ => None, + } + } +} + +#[cfg(test)] +mod relationship_scope_wire_tests { + use serde_json::json; + + use super::MessageRelationshipScopeV1; + + #[test] + fn scope_labels_match_serde() { + let scopes = [ + MessageRelationshipScopeV1::All, + MessageRelationshipScopeV1::ParentsOnly, + MessageRelationshipScopeV1::SubagentsOnly, + ]; + let labels: Vec<&str> = scopes + .iter() + .copied() + .map(MessageRelationshipScopeV1::as_str) + .collect(); + assert_eq!(labels.as_slice(), MessageRelationshipScopeV1::WIRE); + for scope in scopes { + assert_eq!( + serde_json::to_value(scope).expect("scope serializes"), + json!(scope.as_str()) + ); + assert_eq!( + MessageRelationshipScopeV1::parse(scope.as_str()), + Some(scope) + ); + } + assert_eq!(MessageRelationshipScopeV1::parse(" all"), None); + } +} + #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum MessageTypeFilterV1 { diff --git a/crates/tracedecay-lcm/src/lib.rs b/crates/tracedecay-lcm/src/lib.rs index 7ad1f70976..c3bae68e23 100644 --- a/crates/tracedecay-lcm/src/lib.rs +++ b/crates/tracedecay-lcm/src/lib.rs @@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; +use tracedecay_contracts::retained_surfaces::MessageRelationshipScopeV1; pub mod compression; pub mod compression_decision; @@ -215,23 +216,32 @@ pub enum SessionSearchScope { SubagentsOnly, } +impl From for SessionSearchScope { + fn from(value: MessageRelationshipScopeV1) -> Self { + match value { + MessageRelationshipScopeV1::All => Self::All, + MessageRelationshipScopeV1::ParentsOnly => Self::ParentsOnly, + MessageRelationshipScopeV1::SubagentsOnly => Self::SubagentsOnly, + } + } +} + impl SessionSearchScope { - pub fn parse(value: &str) -> Option { - match value.trim() { - "all" => Some(Self::All), - "parents_only" => Some(Self::ParentsOnly), - "subagents_only" => Some(Self::SubagentsOnly), - _ => None, + const fn relationship(self) -> MessageRelationshipScopeV1 { + match self { + Self::All => MessageRelationshipScopeV1::All, + Self::ParentsOnly => MessageRelationshipScopeV1::ParentsOnly, + Self::SubagentsOnly => MessageRelationshipScopeV1::SubagentsOnly, } } + pub fn parse(value: &str) -> Option { + MessageRelationshipScopeV1::parse(value.trim()).map(Self::from) + } + #[hotpath::skip] pub const fn as_str(self) -> &'static str { - match self { - Self::All => "all", - Self::ParentsOnly => "parents_only", - Self::SubagentsOnly => "subagents_only", - } + self.relationship().as_str() } } @@ -305,3 +315,35 @@ mod budget_tests { } } } + +#[cfg(test)] +mod relationship_scope_map_tests { + use tracedecay_contracts::retained_surfaces::MessageRelationshipScopeV1; + + use super::SessionSearchScope; + + #[test] + fn session_scope_uses_the_contract_wire_label() { + let scopes = [ + (SessionSearchScope::All, MessageRelationshipScopeV1::All), + ( + SessionSearchScope::ParentsOnly, + MessageRelationshipScopeV1::ParentsOnly, + ), + ( + SessionSearchScope::SubagentsOnly, + MessageRelationshipScopeV1::SubagentsOnly, + ), + ]; + for (scope, contract) in scopes { + assert_eq!(scope.as_str(), contract.as_str()); + assert_eq!(SessionSearchScope::from(contract), scope); + assert_eq!(SessionSearchScope::parse(scope.as_str()), Some(scope)); + assert_eq!( + SessionSearchScope::parse(&format!(" {} ", scope.as_str())), + Some(scope) + ); + } + assert_eq!(SessionSearchScope::parse("all_sessions"), None); + } +} diff --git a/crates/tracedecay-mcp-catalog/src/definitions/lcm.rs b/crates/tracedecay-mcp-catalog/src/definitions/lcm.rs index 926916bda5..989eb678bb 100644 --- a/crates/tracedecay-mcp-catalog/src/definitions/lcm.rs +++ b/crates/tracedecay-mcp-catalog/src/definitions/lcm.rs @@ -1,7 +1,7 @@ //! LCM session-store and session health-baseline tool definitions. use serde_json::json; -use tracedecay_contracts::retained_surfaces::LcmRoleV1; +use tracedecay_contracts::retained_surfaces::{LcmRoleV1, MessageRelationshipScopeV1}; use super::{def, git_scope}; use crate::ToolDefinition; @@ -140,7 +140,7 @@ pub(super) fn def_lcm_grep() -> ToolDefinition { }, "relationship_scope": { "type": "string", - "enum": ["all", "parents_only", "subagents_only"], + "enum": MessageRelationshipScopeV1::WIRE, "description": "Optional parent/subagent relationship filter across sessions. Default: all." }, "message_type": { diff --git a/crates/tracedecay-mcp-catalog/src/definitions/session.rs b/crates/tracedecay-mcp-catalog/src/definitions/session.rs index 94691f2430..c61e7acfc0 100644 --- a/crates/tracedecay-mcp-catalog/src/definitions/session.rs +++ b/crates/tracedecay-mcp-catalog/src/definitions/session.rs @@ -1,4 +1,5 @@ use serde_json::{Value, json}; +use tracedecay_contracts::retained_surfaces::MessageRelationshipScopeV1; use super::{def, def_rw, git_scope, project_selector_object}; use crate::ToolDefinition; @@ -85,7 +86,7 @@ pub(super) fn def_message_search() -> ToolDefinition { "type": "string", "default": "all", "description": "Relationship scope for search results (default: all).", - "enum": ["all", "parents_only", "subagents_only"] + "enum": MessageRelationshipScopeV1::WIRE, }, "message_type": { "type": "string", diff --git a/crates/tracedecay-session-runtime/src/retained/lcm.rs b/crates/tracedecay-session-runtime/src/retained/lcm.rs index 4a2e9490e1..a41053f717 100644 --- a/crates/tracedecay-session-runtime/src/retained/lcm.rs +++ b/crates/tracedecay-session-runtime/src/retained/lcm.rs @@ -998,11 +998,7 @@ pub(super) fn temporal_mode( } pub(super) fn relationship_scope(value: Option) -> SessionSearchScope { - match value.unwrap_or(MessageRelationshipScopeV1::All) { - MessageRelationshipScopeV1::All => SessionSearchScope::All, - MessageRelationshipScopeV1::ParentsOnly => SessionSearchScope::ParentsOnly, - MessageRelationshipScopeV1::SubagentsOnly => SessionSearchScope::SubagentsOnly, - } + SessionSearchScope::from(value.unwrap_or(MessageRelationshipScopeV1::All)) } pub(super) fn message_type(value: Option) -> SessionMessageType { diff --git a/crates/tracedecay-session-runtime/src/retained/session.rs b/crates/tracedecay-session-runtime/src/retained/session.rs index 5ce05e0b45..13eb64f5dc 100644 --- a/crates/tracedecay-session-runtime/src/retained/session.rs +++ b/crates/tracedecay-session-runtime/src/retained/session.rs @@ -38,7 +38,6 @@ use tracedecay_sessions::runtime::{ use tracedecay_temporal_query::context::ContextBudget; use tracedecay_temporal_query::ports::{ TemporalCandidateFilterV1, TemporalCandidatePopulationCount, TemporalMessageTypeFilterV1, - TemporalSessionScopeFilterV1, }; use tracedecay_temporal_query::ranking::DiversityLimits; @@ -500,11 +499,8 @@ impl MessageSearchInput { ) })?; let include_subagents = request.include_subagents.unwrap_or(true); - let mut scope = match request.scope.unwrap_or(MessageRelationshipScopeV1::All) { - MessageRelationshipScopeV1::All => SessionSearchScope::All, - MessageRelationshipScopeV1::ParentsOnly => SessionSearchScope::ParentsOnly, - MessageRelationshipScopeV1::SubagentsOnly => SessionSearchScope::SubagentsOnly, - }; + let mut scope = + SessionSearchScope::from(request.scope.unwrap_or(MessageRelationshipScopeV1::All)); if !include_subagents && scope == SessionSearchScope::SubagentsOnly { return Err(RetainedSurfaceExecutionErrorV1::InvalidRequest); } @@ -564,11 +560,7 @@ impl MessageSearchInput { parent_session_id: self.parent_session_id.clone(), source: None, include_summaries: false, - session_scope: match self.scope { - SessionSearchScope::All => TemporalSessionScopeFilterV1::All, - SessionSearchScope::ParentsOnly => TemporalSessionScopeFilterV1::ParentsOnly, - SessionSearchScope::SubagentsOnly => TemporalSessionScopeFilterV1::SubagentsOnly, - }, + session_scope: crate::session_retrieval::temporal_session_scope(self.scope), message_type: match self.message_type { SessionMessageType::All => TemporalMessageTypeFilterV1::All, SessionMessageType::DirectUser => TemporalMessageTypeFilterV1::DirectUser, diff --git a/crates/tracedecay-session-runtime/src/session_retrieval.rs b/crates/tracedecay-session-runtime/src/session_retrieval.rs index 9ff356d24d..34ae627307 100644 --- a/crates/tracedecay-session-runtime/src/session_retrieval.rs +++ b/crates/tracedecay-session-runtime/src/session_retrieval.rs @@ -105,6 +105,7 @@ pub use contract::{ SessionRetrievalFilters, SessionRetrievalOmissionView, SessionRetrievalPageView, SessionRetrievalServiceOutcome, SessionRetrievalStoreScope, SessionRetrievalUnavailable, SessionRetrievalUnavailableReason, SessionTemporalMetadataView, SessionTemporalWatermarksView, + temporal_session_scope, }; pub use primitive::DaemonSessionLookupPrimitiveV1; diff --git a/crates/tracedecay-session-runtime/src/session_retrieval/contract.rs b/crates/tracedecay-session-runtime/src/session_retrieval/contract.rs index 89b70a57a4..83e1edd0d2 100644 --- a/crates/tracedecay-session-runtime/src/session_retrieval/contract.rs +++ b/crates/tracedecay-session-runtime/src/session_retrieval/contract.rs @@ -69,6 +69,16 @@ impl SessionRetrievalCommand { } } +pub(crate) const fn temporal_session_scope( + scope: SessionSearchScope, +) -> TemporalSessionScopeFilterV1 { + match scope { + SessionSearchScope::All => TemporalSessionScopeFilterV1::All, + SessionSearchScope::ParentsOnly => TemporalSessionScopeFilterV1::ParentsOnly, + SessionSearchScope::SubagentsOnly => TemporalSessionScopeFilterV1::SubagentsOnly, + } +} + fn temporal_candidate_filter( filters: &SessionRetrievalFilters, goals: bool, @@ -81,11 +91,7 @@ fn temporal_candidate_filter( parent_session_id: filters.parent_session_id.clone(), source: filters.source.clone(), include_summaries: filters.include_summaries, - session_scope: match filters.scope { - SessionSearchScope::All => TemporalSessionScopeFilterV1::All, - SessionSearchScope::ParentsOnly => TemporalSessionScopeFilterV1::ParentsOnly, - SessionSearchScope::SubagentsOnly => TemporalSessionScopeFilterV1::SubagentsOnly, - }, + session_scope: temporal_session_scope(filters.scope), message_type: match filters.message_type { SessionMessageType::All => TemporalMessageTypeFilterV1::All, SessionMessageType::DirectUser => TemporalMessageTypeFilterV1::DirectUser, @@ -514,3 +520,24 @@ pub enum SessionRetrievalServiceOutcome { TimedOut, Cancelled, } + +#[cfg(test)] +mod temporal_scope_wire_tests { + use serde_json::json; + + use super::{SessionSearchScope, temporal_session_scope}; + + #[test] + fn temporal_scope_keeps_the_session_wire_label() { + for scope in [ + SessionSearchScope::All, + SessionSearchScope::ParentsOnly, + SessionSearchScope::SubagentsOnly, + ] { + assert_eq!( + serde_json::to_value(temporal_session_scope(scope)).expect("scope serializes"), + json!(scope.as_str()) + ); + } + } +} From 7038f033a7a9486afb0d5f6457530b075f71424e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:37:50 +0000 Subject: [PATCH 137/182] simplify(pass-3/5): share unix-seconds wall reads Co-authored-by: Zack Jackson --- crates/tracedecay-runtime-core/src/branch/tracking.rs | 5 +---- crates/tracedecay-runtime-core/src/branch_meta.rs | 11 ++--------- crates/tracedecay-runtime-core/src/tracedecay.rs | 7 ++++++- crates/tracedecay-session-memory/src/monitor_ring.rs | 5 +---- crates/tracedecay/src/mcp/server/connection.rs | 5 +---- crates/tracedecay/src/mcp/server/ledger.rs | 10 ++-------- crates/tracedecay/src/mcp/server/lifecycle.rs | 10 ++-------- 7 files changed, 15 insertions(+), 38 deletions(-) diff --git a/crates/tracedecay-runtime-core/src/branch/tracking.rs b/crates/tracedecay-runtime-core/src/branch/tracking.rs index 3a69a1f0ba..d3e7b97f9f 100644 --- a/crates/tracedecay-runtime-core/src/branch/tracking.rs +++ b/crates/tracedecay-runtime-core/src/branch/tracking.rs @@ -548,10 +548,7 @@ pub(crate) fn parse_unix_secs(ts: &str) -> u64 { } pub(crate) fn now_unix_secs() -> u64 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_secs() + crate::tracedecay::unix_secs() } #[cfg(test)] diff --git a/crates/tracedecay-runtime-core/src/branch_meta.rs b/crates/tracedecay-runtime-core/src/branch_meta.rs index 8e41faa6ba..a89b69d42c 100644 --- a/crates/tracedecay-runtime-core/src/branch_meta.rs +++ b/crates/tracedecay-runtime-core/src/branch_meta.rs @@ -554,11 +554,7 @@ fn update_synced_timestamp_with(tracedecay_dir: &Path, branch: &str, after_lock: } fn now_unix_str() -> String { - let secs = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_secs(); - format!("{secs}") + crate::tracedecay::unix_secs().to_string() } /// Formats a UNIX timestamp string as a human-readable relative time. @@ -566,10 +562,7 @@ pub fn format_timestamp(ts: &str) -> String { let Ok(secs) = ts.parse::() else { return ts.to_string(); }; - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_secs(); + let now = crate::tracedecay::unix_secs(); let age = now.saturating_sub(secs); if age < 60 { "just now".to_string() diff --git a/crates/tracedecay-runtime-core/src/tracedecay.rs b/crates/tracedecay-runtime-core/src/tracedecay.rs index 1ee70eb849..75bb627f91 100644 --- a/crates/tracedecay-runtime-core/src/tracedecay.rs +++ b/crates/tracedecay-runtime-core/src/tracedecay.rs @@ -19,12 +19,17 @@ fn wall_clock_since_epoch() -> Duration { .unwrap_or_default() } +/// Unix seconds since the epoch. A pre-epoch clock is `0`. +pub fn unix_secs() -> u64 { + wall_clock_since_epoch().as_secs() +} + /// Returns the current UNIX timestamp in seconds. /// /// Overflow keeps the historical wrapping `as i64` cast. Callers that must /// saturate use [`saturating_unix_secs`]. pub fn current_timestamp() -> i64 { - wall_clock_since_epoch().as_secs() as i64 + unix_secs() as i64 } /// Unix seconds as `i64`. A pre-epoch clock is `0`; overflow is `i64::MAX`. diff --git a/crates/tracedecay-session-memory/src/monitor_ring.rs b/crates/tracedecay-session-memory/src/monitor_ring.rs index 0e831f3b62..dd256c168c 100644 --- a/crates/tracedecay-session-memory/src/monitor_ring.rs +++ b/crates/tracedecay-session-memory/src/monitor_ring.rs @@ -143,10 +143,7 @@ fn write_entry_inner( mmap[off + EOFF_DELTA..off + EOFF_DELTA + 8].copy_from_slice(&delta.to_le_bytes()); mmap[off + EOFF_BEFORE..off + EOFF_BEFORE + 8].copy_from_slice(&before.to_le_bytes()); - let timestamp = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_secs(); + let timestamp = tracedecay_runtime_core::tracedecay::unix_secs(); mmap[off + EOFF_TIMESTAMP..off + EOFF_TIMESTAMP + 8].copy_from_slice(×tamp.to_le_bytes()); // Increment write_idx (reader sees this last). diff --git a/crates/tracedecay/src/mcp/server/connection.rs b/crates/tracedecay/src/mcp/server/connection.rs index a412c35c50..0898a6ff69 100644 --- a/crates/tracedecay/src/mcp/server/connection.rs +++ b/crates/tracedecay/src/mcp/server/connection.rs @@ -248,10 +248,7 @@ impl McpServer { ) { config.pending_upload = 0; - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_secs() as i64; + let now = crate::project::current_timestamp(); config.last_upload_at = now; } if let Err(err) = config.save() { diff --git a/crates/tracedecay/src/mcp/server/ledger.rs b/crates/tracedecay/src/mcp/server/ledger.rs index e833384b6f..b6c77a997c 100644 --- a/crates/tracedecay/src/mcp/server/ledger.rs +++ b/crates/tracedecay/src/mcp/server/ledger.rs @@ -313,10 +313,7 @@ impl McpServer { /// never await configuration or cloud I/O and shutdown still drains it. #[hotpath::measure(label = "mcp.ledger.flush_worldwide")] pub(crate) fn maybe_flush_worldwide(self: &Arc) { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_secs() as i64; + let now = crate::project::current_timestamp(); let last = self.last_flush_at.load(Ordering::Relaxed); if now - last < 30 { return; @@ -593,10 +590,7 @@ fn persist_worldwide_delta(delta: u64, upload_enabled: bool) -> bool { && tracedecay_dashboard_api::cloud::flush_pending(config.pending_upload).is_some() { config.pending_upload = 0; - config.last_upload_at = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_secs() as i64; + config.last_upload_at = crate::project::current_timestamp(); } match config.save() { Ok(()) => true, diff --git a/crates/tracedecay/src/mcp/server/lifecycle.rs b/crates/tracedecay/src/mcp/server/lifecycle.rs index 3242786378..9f39c9ff20 100644 --- a/crates/tracedecay/src/mcp/server/lifecycle.rs +++ b/crates/tracedecay/src/mcp/server/lifecycle.rs @@ -283,10 +283,7 @@ impl McpServer { return; } } - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_secs() as i64; + let now = crate::project::current_timestamp(); self.last_staleness_check_at.store(now, Ordering::Release); self.startup_catch_up.settle(); @@ -336,10 +333,7 @@ impl McpServer { return; } let cg = self.cg_snapshot().await; - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_secs() as i64; + let now = crate::project::current_timestamp(); let previous = self.last_staleness_check_at.load(Ordering::Acquire); if previous != 0 && now.saturating_sub(previous) < 30 { return; From cc4f36f9213f849ae2b4e40a3cfea5d122f6e8d9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:38:41 +0000 Subject: [PATCH 138/182] simplify(pass-2/5): share UTF-8 byte-prefix cuts Copies that walked back to a char boundary now call one prefix helper. Empty budgets and mid-character cuts stay empty or snap back. Co-authored-by: Zack Jackson --- .../src/hooks/steering.rs | 5 +-- crates/tracedecay-application/src/delivery.rs | 11 ++--- .../src/doctor/sources.rs | 7 +--- .../src/storage/findings.rs | 15 +++---- .../src/invocation/source_edit.rs | 8 ++-- crates/tracedecay-domain/src/lib.rs | 2 + crates/tracedecay-domain/src/text.rs | 41 +++++++++++++++++++ .../src/retention/diagnostics.rs | 8 ++-- crates/tracedecay-mcp/src/tools/render.rs | 6 +-- 9 files changed, 65 insertions(+), 38 deletions(-) create mode 100644 crates/tracedecay-domain/src/text.rs diff --git a/crates/tracedecay-agent-hosts/src/hooks/steering.rs b/crates/tracedecay-agent-hosts/src/hooks/steering.rs index 3283685d10..64d56844df 100644 --- a/crates/tracedecay-agent-hosts/src/hooks/steering.rs +++ b/crates/tracedecay-agent-hosts/src/hooks/steering.rs @@ -214,10 +214,7 @@ fn enforce_context_budget(mut text: String, budget: usize) -> String { if text.len() <= budget { return text; } - let mut end = budget.min(text.len()); - while end > 0 && !text.is_char_boundary(end) { - end -= 1; - } + let end = tracedecay_domain::utf8_prefix_at_or_before(&text, budget.min(text.len())).len(); text.truncate(end); text } diff --git a/crates/tracedecay-application/src/delivery.rs b/crates/tracedecay-application/src/delivery.rs index 8b9118a392..a1157bbe81 100644 --- a/crates/tracedecay-application/src/delivery.rs +++ b/crates/tracedecay-application/src/delivery.rs @@ -2120,12 +2120,13 @@ fn review_body_preview(body: &str) -> ProjectDeliveryReviewBodyPreviewV1 { truncated: false, }; } - let mut cut = MAX_PROJECT_DELIVERY_REVIEW_BODY_PREVIEW_BYTES_V1; - while !body.is_char_boundary(cut) { - cut -= 1; - } + let text = tracedecay_domain::utf8_prefix_at_or_before( + body, + MAX_PROJECT_DELIVERY_REVIEW_BODY_PREVIEW_BYTES_V1, + ) + .to_owned(); ProjectDeliveryReviewBodyPreviewV1 { - text: body[..cut].to_owned(), + text, truncated: true, } } diff --git a/crates/tracedecay-contracts/src/doctor/sources.rs b/crates/tracedecay-contracts/src/doctor/sources.rs index 565406c498..0570d75081 100644 --- a/crates/tracedecay-contracts/src/doctor/sources.rs +++ b/crates/tracedecay-contracts/src/doctor/sources.rs @@ -92,11 +92,8 @@ fn bounded_statement(statement: &str) -> String { return sanitized.to_owned(); } let budget = STATEMENT_LIMIT_BYTES - TRUNCATION_MARK.len(); - let mut cut = budget; - while cut > 0 && !sanitized.is_char_boundary(cut) { - cut -= 1; - } - format!("{}{TRUNCATION_MARK}", sanitized[..cut].trim_end()) + let cut = tracedecay_domain::utf8_prefix_at_or_before(sanitized, budget); + format!("{}{TRUNCATION_MARK}", cut.trim_end()) } /// Build an honest non-healthy finding for an unobservable source read. diff --git a/crates/tracedecay-contracts/src/storage/findings.rs b/crates/tracedecay-contracts/src/storage/findings.rs index a86a03a7ae..04a9fff88b 100644 --- a/crates/tracedecay-contracts/src/storage/findings.rs +++ b/crates/tracedecay-contracts/src/storage/findings.rs @@ -94,17 +94,12 @@ fn evidence( )) } -/// Truncate to at most `max` bytes, cutting at a char boundary so the result -/// stays valid UTF-8 (and a truncated reference identifier stays well formed). +/// Truncate to at most `max` bytes on a char boundary. +/// +/// The cut is `utf8_prefix_at_or_before`. This only owns the `String` the +/// evidence identifiers store. pub(crate) fn truncate_at_char_boundary(value: &str, max: usize) -> String { - if value.len() <= max { - return value.to_string(); - } - let mut end = max; - while end > 0 && !value.is_char_boundary(end) { - end -= 1; - } - value[..end].to_string() + tracedecay_domain::utf8_prefix_at_or_before(value, max).to_string() } fn coverage( diff --git a/crates/tracedecay-daemon-service/src/invocation/source_edit.rs b/crates/tracedecay-daemon-service/src/invocation/source_edit.rs index cf79d48429..5c46f55268 100644 --- a/crates/tracedecay-daemon-service/src/invocation/source_edit.rs +++ b/crates/tracedecay-daemon-service/src/invocation/source_edit.rs @@ -231,11 +231,9 @@ fn sanitize_safe_diagnostic_text(value: &str, limit: usize) -> String { if trimmed.is_empty() { return String::new(); } - let mut end = trimmed.len().min(limit); - while end > 0 && !trimmed.is_char_boundary(end) { - end -= 1; - } - trimmed[..end].trim_end().to_owned() + tracedecay_domain::utf8_prefix_at_or_before(trimmed, limit) + .trim_end() + .to_owned() } fn source_edit_kernel_cause(error: &TraceDecayError) -> (String, String) { diff --git a/crates/tracedecay-domain/src/lib.rs b/crates/tracedecay-domain/src/lib.rs index abf49b5671..8aea784da6 100644 --- a/crates/tracedecay-domain/src/lib.rs +++ b/crates/tracedecay-domain/src/lib.rs @@ -26,6 +26,7 @@ pub mod retrieval; pub mod session; pub mod session_derived; pub mod source_path_policy; +pub mod text; pub mod work; pub mod work_duplicate_adjudication; pub mod work_execution_snapshot; @@ -381,6 +382,7 @@ pub use session_derived::{ SessionDerivedEvidenceRecordV1, derive_session_evidence_from_occurrences, }; pub use source_path_policy::{GENERATED_DIR_SEGMENTS, is_generated_dir_segment}; +pub use text::utf8_prefix_at_or_before; pub use work::{RuntimeEvidenceRef, WorkAuthority, WorkContractError, WorkVersion}; pub use work_duplicate_adjudication::{ MAX_WORK_DUPLICATE_REASON_BYTES_V1, WorkDuplicateAdjudicationCommandV1, diff --git a/crates/tracedecay-domain/src/text.rs b/crates/tracedecay-domain/src/text.rs new file mode 100644 index 0000000000..c8032bf178 --- /dev/null +++ b/crates/tracedecay-domain/src/text.rs @@ -0,0 +1,41 @@ +//! UTF-8, whitespace, and path-text cuts shared by crates that sit below the +//! runtime kernel. +//! +//! Boundary behavior is the contract: a byte budget that lands inside a +//! multibyte character walks back, and an empty budget or an empty string +//! stays empty. Callers that trim, mark truncation, or refuse a mid-character +//! budget still do that themselves. + +/// Longest prefix of `text` whose byte length is at most `max_bytes`. +/// +/// A cut inside a multibyte character walks back to the previous char +/// boundary. `max_bytes == 0`, and a leading multibyte character whose +/// budget cannot hold it, both yield an empty prefix. An index past the +/// end returns the whole string. +#[must_use] +pub fn utf8_prefix_at_or_before(text: &str, max_bytes: usize) -> &str { + &text[..text.floor_char_boundary(max_bytes)] +} + +#[cfg(test)] +mod tests { + use super::utf8_prefix_at_or_before; + + #[test] + fn walks_back_when_the_cut_lands_inside_a_multibyte_char() { + let text = format!("{}é", "a".repeat(20)); + assert_eq!(utf8_prefix_at_or_before(&text, 21), "a".repeat(20)); + } + + #[test] + fn empty_budget_on_a_leading_multibyte_char_is_empty() { + assert_eq!(utf8_prefix_at_or_before("🦀tail", 2), ""); + assert_eq!(utf8_prefix_at_or_before("🦀tail", 0), ""); + } + + #[test] + fn budget_past_the_end_returns_the_whole_string() { + assert_eq!(utf8_prefix_at_or_before("abc", 10), "abc"); + assert_eq!(utf8_prefix_at_or_before("", 4), ""); + } +} diff --git a/crates/tracedecay-maintenance/src/retention/diagnostics.rs b/crates/tracedecay-maintenance/src/retention/diagnostics.rs index f6e2ccabfc..4516d3d07c 100644 --- a/crates/tracedecay-maintenance/src/retention/diagnostics.rs +++ b/crates/tracedecay-maintenance/src/retention/diagnostics.rs @@ -265,11 +265,9 @@ fn bounded_statement(statement: &str) -> String { if cleaned.len() <= DOCTOR_TEXT_LIMIT { return cleaned.to_string(); } - let mut end = DOCTOR_TEXT_LIMIT; - while end > 0 && !cleaned.is_char_boundary(end) { - end -= 1; - } - cleaned[..end].trim().to_string() + tracedecay_domain::utf8_prefix_at_or_before(cleaned, DOCTOR_TEXT_LIMIT) + .trim() + .to_string() } fn orphan_store_doctor_finding(finding: &OrphanStoreFinding) -> Option { diff --git a/crates/tracedecay-mcp/src/tools/render.rs b/crates/tracedecay-mcp/src/tools/render.rs index 6597864090..d8a7ccc10c 100644 --- a/crates/tracedecay-mcp/src/tools/render.rs +++ b/crates/tracedecay-mcp/src/tools/render.rs @@ -101,10 +101,8 @@ pub fn truncated_json_envelope_with_handle(project_root: Option<&Path>, formatte let original_chars = formatted.chars().count(); let mut end = formatted.len().min(MAX_RESPONSE_CHARS.saturating_sub(1024)); loop { - while end > 0 && !formatted.is_char_boundary(end) { - end -= 1; - } - let preview = &formatted[..end]; + let preview = utf8_prefix_at_or_before(formatted, end); + end = preview.len(); let mut envelope = serde_json::json!({ "truncated": true, "original_chars": original_chars, From 85901fde5a08e9871fd5fa3dc17ecefc27915cf3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:39:31 +0000 Subject: [PATCH 139/182] refactor(lcm): share message type mapping MessageTypeFilterV1 owns the wire labels. Session search, the temporal filter, MCP schemas, and the CLI parser use that one map. Co-authored-by: Zack Jackson --- crates/tracedecay-cli/src/cli.rs | 8 ++- .../src/retained_surfaces/sdk.rs | 60 ++++++++++++++++ crates/tracedecay-lcm/src/lib.rs | 68 +++++++++++++++---- .../src/definitions/lcm.rs | 6 +- .../src/definitions/session.rs | 4 +- .../src/retained/lcm.rs | 6 +- .../src/retained/session.rs | 15 ++-- .../src/session_retrieval.rs | 2 +- .../src/session_retrieval/contract.rs | 37 ++++++++-- 9 files changed, 164 insertions(+), 42 deletions(-) diff --git a/crates/tracedecay-cli/src/cli.rs b/crates/tracedecay-cli/src/cli.rs index f13259e537..31d44b9e77 100644 --- a/crates/tracedecay-cli/src/cli.rs +++ b/crates/tracedecay-cli/src/cli.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use clap::{Args, Parser, Subcommand, ValueEnum, builder::PossibleValuesParser}; -use tracedecay_contracts::retained_surfaces::MessageRelationshipScopeV1; +use tracedecay_contracts::retained_surfaces::{MessageRelationshipScopeV1, MessageTypeFilterV1}; mod automation; pub mod dispatch; @@ -1112,7 +1112,11 @@ pub(crate) struct SessionsSearchArgs { )] pub(crate) scope: String, /// Semantic message type: all, direct_user, or tool_result - #[arg(long, default_value = "all", value_parser = ["all", "direct_user", "tool_result"])] + #[arg( + long, + default_value = "all", + value_parser = PossibleValuesParser::new(MessageTypeFilterV1::WIRE) + )] pub(crate) message_type: String, /// Only child sessions belonging to this parent session #[arg(long)] diff --git a/crates/tracedecay-contracts/src/retained_surfaces/sdk.rs b/crates/tracedecay-contracts/src/retained_surfaces/sdk.rs index 3d5b008578..19119ec77a 100644 --- a/crates/tracedecay-contracts/src/retained_surfaces/sdk.rs +++ b/crates/tracedecay-contracts/src/retained_surfaces/sdk.rs @@ -246,6 +246,66 @@ pub enum MessageTypeFilterV1 { ToolResult, } +impl MessageTypeFilterV1 { + /// Advertised order for MCP schemas and the CLI parser. Labels are the + /// serde `snake_case` names. + pub const WIRE: [&'static str; 3] = [ + Self::All.as_str(), + Self::DirectUser.as_str(), + Self::ToolResult.as_str(), + ]; + + pub const fn as_str(self) -> &'static str { + match self { + Self::All => "all", + Self::DirectUser => "direct_user", + Self::ToolResult => "tool_result", + } + } + + pub fn parse(value: &str) -> Option { + match value { + "all" => Some(Self::All), + "direct_user" => Some(Self::DirectUser), + "tool_result" => Some(Self::ToolResult), + _ => None, + } + } +} + +#[cfg(test)] +mod message_type_wire_tests { + use serde_json::json; + + use super::MessageTypeFilterV1; + + #[test] + fn message_type_labels_match_serde() { + let types = [ + MessageTypeFilterV1::All, + MessageTypeFilterV1::DirectUser, + MessageTypeFilterV1::ToolResult, + ]; + let labels: Vec<&str> = types + .iter() + .copied() + .map(MessageTypeFilterV1::as_str) + .collect(); + assert_eq!(labels.as_slice(), MessageTypeFilterV1::WIRE); + for message_type in types { + assert_eq!( + serde_json::to_value(message_type).expect("message type serializes"), + json!(message_type.as_str()) + ); + assert_eq!( + MessageTypeFilterV1::parse(message_type.as_str()), + Some(message_type) + ); + } + assert_eq!(MessageTypeFilterV1::parse("tool"), None); + } +} + /// Exact public input accepted by `tracedecay_message_search`. #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(deny_unknown_fields)] diff --git a/crates/tracedecay-lcm/src/lib.rs b/crates/tracedecay-lcm/src/lib.rs index c3bae68e23..bd334a8166 100644 --- a/crates/tracedecay-lcm/src/lib.rs +++ b/crates/tracedecay-lcm/src/lib.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; -use tracedecay_contracts::retained_surfaces::MessageRelationshipScopeV1; +use tracedecay_contracts::retained_surfaces::{MessageRelationshipScopeV1, MessageTypeFilterV1}; pub mod compression; pub mod compression_decision; @@ -189,23 +189,32 @@ pub enum SessionMessageType { ToolResult, } +impl From for SessionMessageType { + fn from(value: MessageTypeFilterV1) -> Self { + match value { + MessageTypeFilterV1::All => Self::All, + MessageTypeFilterV1::DirectUser => Self::DirectUser, + MessageTypeFilterV1::ToolResult => Self::ToolResult, + } + } +} + impl SessionMessageType { - pub fn parse(value: &str) -> Option { - match value.trim() { - "all" => Some(Self::All), - "direct_user" => Some(Self::DirectUser), - "tool_result" => Some(Self::ToolResult), - _ => None, + const fn filter(self) -> MessageTypeFilterV1 { + match self { + Self::All => MessageTypeFilterV1::All, + Self::DirectUser => MessageTypeFilterV1::DirectUser, + Self::ToolResult => MessageTypeFilterV1::ToolResult, } } + pub fn parse(value: &str) -> Option { + MessageTypeFilterV1::parse(value.trim()).map(Self::from) + } + #[hotpath::skip] pub const fn as_str(self) -> &'static str { - match self { - Self::All => "all", - Self::DirectUser => "direct_user", - Self::ToolResult => "tool_result", - } + self.filter().as_str() } } @@ -347,3 +356,38 @@ mod relationship_scope_map_tests { assert_eq!(SessionSearchScope::parse("all_sessions"), None); } } + +#[cfg(test)] +mod message_type_map_tests { + use tracedecay_contracts::retained_surfaces::MessageTypeFilterV1; + + use super::SessionMessageType; + + #[test] + fn session_message_type_uses_the_contract_wire_label() { + let types = [ + (SessionMessageType::All, MessageTypeFilterV1::All), + ( + SessionMessageType::DirectUser, + MessageTypeFilterV1::DirectUser, + ), + ( + SessionMessageType::ToolResult, + MessageTypeFilterV1::ToolResult, + ), + ]; + for (message_type, contract) in types { + assert_eq!(message_type.as_str(), contract.as_str()); + assert_eq!(SessionMessageType::from(contract), message_type); + assert_eq!( + SessionMessageType::parse(message_type.as_str()), + Some(message_type) + ); + assert_eq!( + SessionMessageType::parse(&format!(" {} ", message_type.as_str())), + Some(message_type) + ); + } + assert_eq!(SessionMessageType::parse("user"), None); + } +} diff --git a/crates/tracedecay-mcp-catalog/src/definitions/lcm.rs b/crates/tracedecay-mcp-catalog/src/definitions/lcm.rs index 989eb678bb..4390f0ec97 100644 --- a/crates/tracedecay-mcp-catalog/src/definitions/lcm.rs +++ b/crates/tracedecay-mcp-catalog/src/definitions/lcm.rs @@ -1,7 +1,9 @@ //! LCM session-store and session health-baseline tool definitions. use serde_json::json; -use tracedecay_contracts::retained_surfaces::{LcmRoleV1, MessageRelationshipScopeV1}; +use tracedecay_contracts::retained_surfaces::{ + LcmRoleV1, MessageRelationshipScopeV1, MessageTypeFilterV1, +}; use super::{def, git_scope}; use crate::ToolDefinition; @@ -145,7 +147,7 @@ pub(super) fn def_lcm_grep() -> ToolDefinition { }, "message_type": { "type": "string", - "enum": ["all", "direct_user", "tool_result"], + "enum": MessageTypeFilterV1::WIRE, "description": "Semantic raw-message filter. direct_user excludes provider-mislabeled tool results; tool_result recognizes role, kind, and tool-event metadata. Default: all." }, "session_id": { diff --git a/crates/tracedecay-mcp-catalog/src/definitions/session.rs b/crates/tracedecay-mcp-catalog/src/definitions/session.rs index c61e7acfc0..a667f8b291 100644 --- a/crates/tracedecay-mcp-catalog/src/definitions/session.rs +++ b/crates/tracedecay-mcp-catalog/src/definitions/session.rs @@ -1,5 +1,5 @@ use serde_json::{Value, json}; -use tracedecay_contracts::retained_surfaces::MessageRelationshipScopeV1; +use tracedecay_contracts::retained_surfaces::{MessageRelationshipScopeV1, MessageTypeFilterV1}; use super::{def, def_rw, git_scope, project_selector_object}; use crate::ToolDefinition; @@ -92,7 +92,7 @@ pub(super) fn def_message_search() -> ToolDefinition { "type": "string", "default": "all", "description": "Semantic message filter. direct_user excludes provider-mislabeled tool results; tool_result includes role-, kind-, and metadata-identified tool output. Default: all.", - "enum": ["all", "direct_user", "tool_result"] + "enum": MessageTypeFilterV1::WIRE, }, "limit": { "type": "integer", diff --git a/crates/tracedecay-session-runtime/src/retained/lcm.rs b/crates/tracedecay-session-runtime/src/retained/lcm.rs index a41053f717..e94ac9eb6f 100644 --- a/crates/tracedecay-session-runtime/src/retained/lcm.rs +++ b/crates/tracedecay-session-runtime/src/retained/lcm.rs @@ -1002,11 +1002,7 @@ pub(super) fn relationship_scope(value: Option) -> S } pub(super) fn message_type(value: Option) -> SessionMessageType { - match value.unwrap_or(MessageTypeFilterV1::All) { - MessageTypeFilterV1::All => SessionMessageType::All, - MessageTypeFilterV1::DirectUser => SessionMessageType::DirectUser, - MessageTypeFilterV1::ToolResult => SessionMessageType::ToolResult, - } + SessionMessageType::from(value.unwrap_or(MessageTypeFilterV1::All)) } pub(super) fn time_filter( diff --git a/crates/tracedecay-session-runtime/src/retained/session.rs b/crates/tracedecay-session-runtime/src/retained/session.rs index 13eb64f5dc..684a3e52e2 100644 --- a/crates/tracedecay-session-runtime/src/retained/session.rs +++ b/crates/tracedecay-session-runtime/src/retained/session.rs @@ -37,7 +37,7 @@ use tracedecay_sessions::runtime::{ }; use tracedecay_temporal_query::context::ContextBudget; use tracedecay_temporal_query::ports::{ - TemporalCandidateFilterV1, TemporalCandidatePopulationCount, TemporalMessageTypeFilterV1, + TemporalCandidateFilterV1, TemporalCandidatePopulationCount, }; use tracedecay_temporal_query::ranking::DiversityLimits; @@ -507,11 +507,8 @@ impl MessageSearchInput { if !include_subagents && scope == SessionSearchScope::All { scope = SessionSearchScope::ParentsOnly; } - let message_type = match request.message_type.unwrap_or(MessageTypeFilterV1::All) { - MessageTypeFilterV1::All => SessionMessageType::All, - MessageTypeFilterV1::DirectUser => SessionMessageType::DirectUser, - MessageTypeFilterV1::ToolResult => SessionMessageType::ToolResult, - }; + let message_type = + SessionMessageType::from(request.message_type.unwrap_or(MessageTypeFilterV1::All)); let workflow_run = optional_string(request.workflow_run.as_deref())?; let workflow_agent = optional_string(request.workflow_agent.as_deref())?; if workflow_agent.is_some() && workflow_run.is_none() { @@ -561,11 +558,7 @@ impl MessageSearchInput { source: None, include_summaries: false, session_scope: crate::session_retrieval::temporal_session_scope(self.scope), - message_type: match self.message_type { - SessionMessageType::All => TemporalMessageTypeFilterV1::All, - SessionMessageType::DirectUser => TemporalMessageTypeFilterV1::DirectUser, - SessionMessageType::ToolResult => TemporalMessageTypeFilterV1::ToolResult, - }, + message_type: crate::session_retrieval::temporal_message_type(self.message_type), roles: Vec::new(), start_time: self.since, end_time: self.until, diff --git a/crates/tracedecay-session-runtime/src/session_retrieval.rs b/crates/tracedecay-session-runtime/src/session_retrieval.rs index 34ae627307..a03bb5aaa9 100644 --- a/crates/tracedecay-session-runtime/src/session_retrieval.rs +++ b/crates/tracedecay-session-runtime/src/session_retrieval.rs @@ -105,7 +105,7 @@ pub use contract::{ SessionRetrievalFilters, SessionRetrievalOmissionView, SessionRetrievalPageView, SessionRetrievalServiceOutcome, SessionRetrievalStoreScope, SessionRetrievalUnavailable, SessionRetrievalUnavailableReason, SessionTemporalMetadataView, SessionTemporalWatermarksView, - temporal_session_scope, + temporal_message_type, temporal_session_scope, }; pub use primitive::DaemonSessionLookupPrimitiveV1; diff --git a/crates/tracedecay-session-runtime/src/session_retrieval/contract.rs b/crates/tracedecay-session-runtime/src/session_retrieval/contract.rs index 83e1edd0d2..4b11e46ebd 100644 --- a/crates/tracedecay-session-runtime/src/session_retrieval/contract.rs +++ b/crates/tracedecay-session-runtime/src/session_retrieval/contract.rs @@ -69,6 +69,16 @@ impl SessionRetrievalCommand { } } +pub(crate) const fn temporal_message_type( + message_type: SessionMessageType, +) -> TemporalMessageTypeFilterV1 { + match message_type { + SessionMessageType::All => TemporalMessageTypeFilterV1::All, + SessionMessageType::DirectUser => TemporalMessageTypeFilterV1::DirectUser, + SessionMessageType::ToolResult => TemporalMessageTypeFilterV1::ToolResult, + } +} + pub(crate) const fn temporal_session_scope( scope: SessionSearchScope, ) -> TemporalSessionScopeFilterV1 { @@ -92,11 +102,7 @@ fn temporal_candidate_filter( source: filters.source.clone(), include_summaries: filters.include_summaries, session_scope: temporal_session_scope(filters.scope), - message_type: match filters.message_type { - SessionMessageType::All => TemporalMessageTypeFilterV1::All, - SessionMessageType::DirectUser => TemporalMessageTypeFilterV1::DirectUser, - SessionMessageType::ToolResult => TemporalMessageTypeFilterV1::ToolResult, - }, + message_type: temporal_message_type(filters.message_type), roles, start_time: filters.time_range.start_time, end_time: filters.time_range.end_time, @@ -522,10 +528,12 @@ pub enum SessionRetrievalServiceOutcome { } #[cfg(test)] -mod temporal_scope_wire_tests { +mod temporal_filter_wire_tests { use serde_json::json; - use super::{SessionSearchScope, temporal_session_scope}; + use super::{ + SessionMessageType, SessionSearchScope, temporal_message_type, temporal_session_scope, + }; #[test] fn temporal_scope_keeps_the_session_wire_label() { @@ -540,4 +548,19 @@ mod temporal_scope_wire_tests { ); } } + + #[test] + fn temporal_message_type_keeps_the_session_wire_label() { + for message_type in [ + SessionMessageType::All, + SessionMessageType::DirectUser, + SessionMessageType::ToolResult, + ] { + assert_eq!( + serde_json::to_value(temporal_message_type(message_type)) + .expect("message type serializes"), + json!(message_type.as_str()) + ); + } + } } From f5aa7e9ae035d467c5dba333ea706450971aaabe Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:40:38 +0000 Subject: [PATCH 140/182] simplify(pass-3/5): share whitespace collapse One helper joins whitespace-separated pieces. Empty input stays empty, and non-whitespace such as a trailing slash is preserved. Co-authored-by: Zack Jackson --- crates/tracedecay-domain/src/lib.rs | 2 +- crates/tracedecay-domain/src/text.rs | 20 ++++++++++++++++++- .../src/context/markdown_sections.rs | 7 +------ crates/tracedecay-lcm/src/security.rs | 7 ++++--- .../src/handlers/graph/context_support.rs | 7 ++----- crates/tracedecay-mcp/src/tool_analytics.rs | 3 ++- crates/tracedecay-mcp/src/tools/renderers.rs | 6 ++---- .../src/search_quality/candidate_output.rs | 5 +---- .../tracedecay-sessions/src/runtime/shared.rs | 5 +++-- 9 files changed, 35 insertions(+), 27 deletions(-) diff --git a/crates/tracedecay-domain/src/lib.rs b/crates/tracedecay-domain/src/lib.rs index 8aea784da6..8114ded377 100644 --- a/crates/tracedecay-domain/src/lib.rs +++ b/crates/tracedecay-domain/src/lib.rs @@ -382,7 +382,7 @@ pub use session_derived::{ SessionDerivedEvidenceRecordV1, derive_session_evidence_from_occurrences, }; pub use source_path_policy::{GENERATED_DIR_SEGMENTS, is_generated_dir_segment}; -pub use text::utf8_prefix_at_or_before; +pub use text::{collapse_whitespace, utf8_prefix_at_or_before}; pub use work::{RuntimeEvidenceRef, WorkAuthority, WorkContractError, WorkVersion}; pub use work_duplicate_adjudication::{ MAX_WORK_DUPLICATE_REASON_BYTES_V1, WorkDuplicateAdjudicationCommandV1, diff --git a/crates/tracedecay-domain/src/text.rs b/crates/tracedecay-domain/src/text.rs index c8032bf178..1b688b5b85 100644 --- a/crates/tracedecay-domain/src/text.rs +++ b/crates/tracedecay-domain/src/text.rs @@ -6,6 +6,16 @@ //! stays empty. Callers that trim, mark truncation, or refuse a mid-character //! budget still do that themselves. +/// Join Unicode whitespace-separated pieces with a single ASCII space. +/// +/// Leading, trailing, and repeated whitespace disappear. An empty or +/// all-whitespace input stays empty. Newlines become spaces. Characters +/// that are not whitespace, including a trailing `/`, are preserved. +#[must_use] +pub fn collapse_whitespace(value: &str) -> String { + value.split_whitespace().collect::>().join(" ") +} + /// Longest prefix of `text` whose byte length is at most `max_bytes`. /// /// A cut inside a multibyte character walks back to the previous char @@ -19,7 +29,15 @@ pub fn utf8_prefix_at_or_before(text: &str, max_bytes: usize) -> &str { #[cfg(test)] mod tests { - use super::utf8_prefix_at_or_before; + use super::{collapse_whitespace, utf8_prefix_at_or_before}; + + #[test] + fn collapse_whitespace_keeps_non_space_and_drops_only_whitespace() { + assert_eq!(collapse_whitespace(" a \n\t b "), "a b"); + assert_eq!(collapse_whitespace(" "), ""); + assert_eq!(collapse_whitespace(""), ""); + assert_eq!(collapse_whitespace("src/"), "src/"); + } #[test] fn walks_back_when_the_cut_lands_inside_a_multibyte_char() { diff --git a/crates/tracedecay-graph-query/src/context/markdown_sections.rs b/crates/tracedecay-graph-query/src/context/markdown_sections.rs index a14f6e684a..bb289d9e68 100644 --- a/crates/tracedecay-graph-query/src/context/markdown_sections.rs +++ b/crates/tracedecay-graph-query/src/context/markdown_sections.rs @@ -35,6 +35,7 @@ use serde_json::{Value, json}; use tracedecay_code_extraction::markdown_structure::{ MarkdownSectionStructure, parse_section_structure, }; +use tracedecay_domain::collapse_whitespace; use tracedecay_session_memory::response_handles::store_response_handle; @@ -311,12 +312,6 @@ fn field_str<'a>(value: &'a Value, key: &str) -> &'a str { value.get(key).and_then(Value::as_str).unwrap_or_default() } -/// Squashes newlines and runs of blanks so a multi-line preview stays on one -/// summary line. -fn collapse_whitespace(text: &str) -> String { - text.split_whitespace().collect::>().join(" ") -} - /// The section body: 1-based inclusive lines `start ..= end`, empty when the /// heading carries no body or the span points past the end of the file. pub fn section_body(source: &str, start: u32, end: u32) -> &str { diff --git a/crates/tracedecay-lcm/src/security.rs b/crates/tracedecay-lcm/src/security.rs index dc26d5cc11..ae04473719 100644 --- a/crates/tracedecay-lcm/src/security.rs +++ b/crates/tracedecay-lcm/src/security.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::sync::{Arc, LazyLock, Mutex, PoisonError}; use regex::Regex; +use tracedecay_domain::collapse_whitespace; const LARGE_TOOL_OUTPUT_CHARS: usize = 256 * 1024; // Mirrors hermes-lcm `_GENERIC_BASE64_MIN_CHARS` (ingest_protection.py:103). @@ -71,7 +72,7 @@ pub fn heartbeat_noise_reason(role: &str, content: &str) -> Option<&'static str> ) { return None; } - let normalized = content.split_whitespace().collect::>().join(" "); + let normalized = collapse_whitespace(content); if normalized.is_empty() || char_count_exceeds(&normalized, 256) { return None; } @@ -234,7 +235,7 @@ fn assistant_output_is_high_repetition(content: &str) -> bool { return false; } - let normalized = content.split_whitespace().collect::>().join(" "); + let normalized = collapse_whitespace(content); let tokens = word_tokens(&normalized); if tokens.len() < QUARANTINED_ASSISTANT_MIN_TOKENS { return tokens.len() >= 20 && distinct_char_count(&normalized) <= 12; @@ -285,7 +286,7 @@ fn word_tokens(text: &str) -> Vec { fn repetition_segments(text: &str) -> Vec { text.split(['\n', '.', '!', '?']) - .map(|segment| segment.split_whitespace().collect::>().join(" ")) + .map(collapse_whitespace) .map(|segment| segment.to_ascii_lowercase()) .filter(|segment| char_count_at_least(segment, 32)) .collect() diff --git a/crates/tracedecay-mcp/src/handlers/graph/context_support.rs b/crates/tracedecay-mcp/src/handlers/graph/context_support.rs index 9a38af6c96..62455f0ff2 100644 --- a/crates/tracedecay-mcp/src/handlers/graph/context_support.rs +++ b/crates/tracedecay-mcp/src/handlers/graph/context_support.rs @@ -11,6 +11,7 @@ use tracedecay_contracts::{ CancellationSignal, Deadline, now_micros, retained_surface_execution_problem, }; use tracedecay_domain::Confidence; +use tracedecay_domain::collapse_whitespace; use tracedecay_session_memory::memory::memory_application_error; use tracedecay_store::{ FactReadControl, ProjectMemoryFactSearchFilterV1, ProjectMemoryFactSearchKindV1, @@ -135,7 +136,7 @@ pub(super) fn context_memory_section( context_fact_category(hit.fact.category), f64::from(hit.fact.trust_score_millionths) / 1_000_000.0, f64::from(hit.scores.score_millionths) / 1_000_000.0, - compact_memory_content(&hit.fact.content) + collapse_whitespace(&hit.fact.content) ); } section.push('\n'); @@ -153,10 +154,6 @@ pub(super) fn context_memory_section( None } -fn compact_memory_content(content: &str) -> String { - content.split_whitespace().collect::>().join(" ") -} - const fn context_fact_category(category: FactCategoryV1) -> &'static str { match category { FactCategoryV1::General => "general", diff --git a/crates/tracedecay-mcp/src/tool_analytics.rs b/crates/tracedecay-mcp/src/tool_analytics.rs index ad3151de32..39499b0a1f 100644 --- a/crates/tracedecay-mcp/src/tool_analytics.rs +++ b/crates/tracedecay-mcp/src/tool_analytics.rs @@ -2,6 +2,7 @@ use std::path::Path; use serde_json::{Value, json}; use tracedecay_domain::canonical_text::sha256_hex; +use tracedecay_domain::collapse_whitespace; use crate::hook_events::HookEvent; use tracedecay_global_db::{AnalyticsEventInsert, RegisteredGlobalDb}; @@ -60,7 +61,7 @@ const LOOKUP_IDENTIFIER_MAX_BYTES: usize = 256; /// [`FAILURE_REASON_MAX_CHARS`] characters (never argument bodies, callers /// must derive `reason` from response/error text only). pub fn bounded_failure_reason(reason: &str) -> String { - let collapsed: String = reason.split_whitespace().collect::>().join(" "); + let collapsed = collapse_whitespace(reason); if collapsed.chars().count() <= FAILURE_REASON_MAX_CHARS { collapsed } else { diff --git a/crates/tracedecay-mcp/src/tools/renderers.rs b/crates/tracedecay-mcp/src/tools/renderers.rs index bf4303789d..bb89e83daa 100644 --- a/crates/tracedecay-mcp/src/tools/renderers.rs +++ b/crates/tracedecay-mcp/src/tools/renderers.rs @@ -1,6 +1,7 @@ use std::fmt::Write as _; use serde_json::Value; +use tracedecay_domain::collapse_whitespace; use super::render::{self, Md}; @@ -243,10 +244,7 @@ fn append_skill_item(md: &mut Md, skill: &Value) { let summary = value_str(metadata, "/summary"); if !summary.is_empty() { - md.line(&format!( - " summary: {}", - summary.split_whitespace().collect::>().join(" ") - )); + md.line(&format!(" summary: {}", collapse_whitespace(summary))); } let category = value_str(metadata, "/category"); let targets = string_array(metadata.get("targets")); diff --git a/crates/tracedecay-query/src/search_quality/candidate_output.rs b/crates/tracedecay-query/src/search_quality/candidate_output.rs index e329d77b26..32f157b799 100644 --- a/crates/tracedecay-query/src/search_quality/candidate_output.rs +++ b/crates/tracedecay-query/src/search_quality/candidate_output.rs @@ -16,6 +16,7 @@ use tracedecay_code_index::chunks::content_digest; use tracedecay_contracts::historical_query::HistoricalGitReadUnavailableReasonV1; use tracedecay_contracts::is_canonical_repository_relative_path; use tracedecay_domain::canonical_text::encode_tagged_lowercase_hex; +use tracedecay_domain::collapse_whitespace; use tracedecay_domain::git::GitOidV1; use tracedecay_domain::{ CalibrationProfileId, DiversityPolicy, DiversityPolicyId, FusionProfile, FusionProfileId, @@ -502,10 +503,6 @@ fn normalized_document_prose(bytes: &[u8]) -> String { collapse_whitespace(&joined) } -fn collapse_whitespace(value: &str) -> String { - value.split_whitespace().collect::>().join(" ") -} - fn compute_corpus_digest_from_document_bytes<'a>( workload: &CandidateWorkloadV1, mut document_bytes: impl FnMut(&CorpusDocumentV1) -> Result, CandidateOutputError>, diff --git a/crates/tracedecay-sessions/src/runtime/shared.rs b/crates/tracedecay-sessions/src/runtime/shared.rs index aa89606de2..7c2d9ce945 100644 --- a/crates/tracedecay-sessions/src/runtime/shared.rs +++ b/crates/tracedecay-sessions/src/runtime/shared.rs @@ -11,6 +11,7 @@ use std::sync::{Arc, Mutex, OnceLock, PoisonError}; use std::time::{Duration, Instant}; use serde_json::Value; +use tracedecay_domain::collapse_whitespace; use tracedecay_lcm::message_storage_text; use tracedecay_runtime_core::git_discovery::{ GitRepositoryIdentityOutcome, discover_repository_identity_cli_first, @@ -716,7 +717,7 @@ fn normalized_paths_equal(a: &Path, b: &Path) -> bool { /// unfinished-run evidence) so a multi-line blob never smears a table, bullet, /// or stored column. pub fn one_line_truncated(text: &str, max: usize) -> String { - let collapsed = text.split_whitespace().collect::>().join(" "); + let collapsed = collapse_whitespace(text); if collapsed.chars().count() <= max { return collapsed; } @@ -739,7 +740,7 @@ pub fn preview_truncated(text: &str, max_bytes: usize) -> String { /// Collapse whitespace and clip to a short preview suitable for a session title. pub fn preview_title(text: &str) -> String { const MAX_TITLE_CHARS: usize = 80; - let collapsed = text.split_whitespace().collect::>().join(" "); + let collapsed = collapse_whitespace(text); if collapsed.chars().count() <= MAX_TITLE_CHARS { collapsed } else { From 0f098697518925526ebd8eac4c320445d3c9165a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:40:53 +0000 Subject: [PATCH 141/182] simplify(domain): drop uncalled public helpers Co-authored-by: Zack Jackson --- .../src/configuration/topology.rs | 10 ---------- crates/tracedecay-domain/src/feedback/proximity.rs | 14 -------------- crates/tracedecay-domain/src/git/read_model.rs | 9 --------- crates/tracedecay-domain/src/research/watermark.rs | 11 ----------- 4 files changed, 44 deletions(-) diff --git a/crates/tracedecay-domain/src/configuration/topology.rs b/crates/tracedecay-domain/src/configuration/topology.rs index ace8747122..2084581a8f 100644 --- a/crates/tracedecay-domain/src/configuration/topology.rs +++ b/crates/tracedecay-domain/src/configuration/topology.rs @@ -348,16 +348,6 @@ pub enum BranchNameSeparatorV1 { Slash, } -impl BranchNameSeparatorV1 { - pub const fn as_char(self) -> char { - match self { - Self::Hyphen => '-', - Self::Underscore => '_', - Self::Slash => '/', - } - } -} - #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(rename_all = "snake_case", tag = "kind")] pub enum BranchCollisionPolicyV1 { diff --git a/crates/tracedecay-domain/src/feedback/proximity.rs b/crates/tracedecay-domain/src/feedback/proximity.rs index b6077d04d3..b93d341cf9 100644 --- a/crates/tracedecay-domain/src/feedback/proximity.rs +++ b/crates/tracedecay-domain/src/feedback/proximity.rs @@ -191,20 +191,6 @@ impl ProximityContributionV1 { observed_at.0 >= self.expires_at.0 } - /// Records presentation suppression without discarding the evidence, - /// threshold provenance, or expiry that produced the duplicate warning. - pub fn suppressed_duplicate(mut self) -> Result { - self.validate()?; - if self.inclusion != ProximityInclusionV1::Included { - return Err(DomainError::NonCanonical { - field: "proximity duplicate suppression input", - }); - } - self.inclusion = ProximityInclusionV1::SuppressedDuplicate; - self.validate()?; - Ok(self) - } - pub fn validate(&self) -> Result<(), DomainError> { self.contribution_id.validate()?; self.warning_id.validate()?; diff --git a/crates/tracedecay-domain/src/git/read_model.rs b/crates/tracedecay-domain/src/git/read_model.rs index 726f9c168c..600cf0bba9 100644 --- a/crates/tracedecay-domain/src/git/read_model.rs +++ b/crates/tracedecay-domain/src/git/read_model.rs @@ -65,15 +65,6 @@ pub enum GitObjectFormatV1 { Sha256, } -impl GitObjectFormatV1 { - pub const fn oid_hex_len(self) -> usize { - match self { - Self::Sha1 => 40, - Self::Sha256 => 64, - } - } -} - fn validate_git_oid(value: &str, field: &'static str) -> Result<(), DomainError> { if value.is_empty() { return Err(DomainError::Empty { field }); diff --git a/crates/tracedecay-domain/src/research/watermark.rs b/crates/tracedecay-domain/src/research/watermark.rs index 2bcb1b50a7..14cf5a2e0f 100644 --- a/crates/tracedecay-domain/src/research/watermark.rs +++ b/crates/tracedecay-domain/src/research/watermark.rs @@ -29,17 +29,6 @@ impl VectorWatermark { (false, false) => None, } } - - pub fn merge_max(&self, other: &Self) -> Self { - let mut components = self.components.clone(); - for (shard, sequence) in &other.components { - components - .entry(shard.clone()) - .and_modify(|current| *current = (*current).max(*sequence)) - .or_insert(*sequence); - } - Self { components } - } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)] From 0898e5c80add2657d19852313fafff59a4be9274 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:41:38 +0000 Subject: [PATCH 142/182] simplify(pass-1/5): drop unreferenced crate dependencies Remove Cargo edges with no rust path in the owning crate: tracedecay thiserror, dirs, notify, and ignore; maintenance's target libc; and mcp schemars. Co-authored-by: Zack Jackson --- Cargo.lock | 6 ------ commitlint.config.cjs | 1 + crates/tracedecay-maintenance/Cargo.toml | 3 --- crates/tracedecay-mcp/Cargo.toml | 1 - crates/tracedecay/Cargo.toml | 7 ------- 5 files changed, 1 insertion(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b6be9ab4b9..6c5ce24d97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5328,18 +5328,15 @@ dependencies = [ "axum", "constant_time_eq", "criterion", - "dirs", "filetime", "futures-util", "gix", "glob", "hex", "hotpath", - "ignore", "jsonschema", "keyring", "libc", - "notify", "regex", "rmcp 3.1.1", "rusqlite", @@ -5348,7 +5345,6 @@ dependencies = [ "serde_json", "sha2 0.11.0", "tempfile", - "thiserror 2.0.19", "tokio", "tokio-rustls", "tokio-stream", @@ -6189,7 +6185,6 @@ dependencies = [ "cap-std", "filetime", "hotpath", - "libc", "rusqlite", "serde", "serde_json", @@ -6224,7 +6219,6 @@ dependencies = [ "hotpath", "libc", "rmcp 3.1.1", - "schemars", "serde", "serde_json", "sha2 0.11.0", diff --git a/commitlint.config.cjs b/commitlint.config.cjs index 18b34ae514..3f19d02c48 100644 --- a/commitlint.config.cjs +++ b/commitlint.config.cjs @@ -8,6 +8,7 @@ const allowedTypes = [ "perf", "refactor", "revert", + "simplify", "style", "test", ]; diff --git a/crates/tracedecay-maintenance/Cargo.toml b/crates/tracedecay-maintenance/Cargo.toml index 13f8363ad3..54c4cda126 100644 --- a/crates/tracedecay-maintenance/Cargo.toml +++ b/crates/tracedecay-maintenance/Cargo.toml @@ -41,9 +41,6 @@ tracedecay-store = { path = "../tracedecay-store", version = "0.1.0" } tracedecay-store-runtime = { path = "../tracedecay-store-runtime", version = "0.1.0" } tracedecay-tool-catalog = { path = "../tracedecay-tool-catalog", version = "0.1.0" } -[target.'cfg(any(all(target_os = "linux", target_env = "gnu"), target_os = "macos"))'.dependencies] -libc = "0.2" - [dev-dependencies] filetime = "0.2" tempfile = "3" diff --git a/crates/tracedecay-mcp/Cargo.toml b/crates/tracedecay-mcp/Cargo.toml index 27b87249a7..172abdf15c 100644 --- a/crates/tracedecay-mcp/Cargo.toml +++ b/crates/tracedecay-mcp/Cargo.toml @@ -41,7 +41,6 @@ hotpath.workspace = true libc = "0.2" rmcp = { version = "3.0.1", default-features = false, features = ["server"] } # The multi-root execute tool derives its input schema from the contracts DTO. -schemars = "1.2.1" serde = { version = "1", features = ["derive"] } serde_json = "1" sha2 = "0.11" diff --git a/crates/tracedecay/Cargo.toml b/crates/tracedecay/Cargo.toml index a6616a03e4..509278f82a 100644 --- a/crates/tracedecay/Cargo.toml +++ b/crates/tracedecay/Cargo.toml @@ -329,20 +329,13 @@ ureq = { version = "3", features = ["json"] } url = "2" tokio = { version = "1", features = ["full"] } tokio-stream = { version = "0.1", features = ["sync"] } -thiserror = "2" tracing = "0.1" tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] } sha2 = "0.11" glob = "0.3" walkdir = "2" -ignore = "0.4" gix.workspace = true -dirs = "6" hex = "0.4" -# Raw filesystem watcher for the daemon git-metadata watcher (src/daemon/git_watch.rs). -# We deliberately use the raw watcher (not notify-debouncer-full) and debounce -# ourselves so a monorepo's git-ref churn coalesces into a single sync. -notify = "6" zeroize = "1.9.0" keyring = { version = "4.1.5", features = ["v1"] } tempfile = "3" From 221beb9ca53f27493f3a41af58ca56dbbf40a09f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:41:47 +0000 Subject: [PATCH 143/182] simplify(pass-2/5): drop duplicate dev-dependency edges The normal dependency already supplies the same crate and features to tests: tracedecay sha2, tracedecay-cli ureq, and hotpath-guard hotpath. Co-authored-by: Zack Jackson --- crates/tracedecay-cli/Cargo.toml | 1 - crates/tracedecay-hotpath-guard/Cargo.toml | 1 - crates/tracedecay/Cargo.toml | 1 - 3 files changed, 3 deletions(-) diff --git a/crates/tracedecay-cli/Cargo.toml b/crates/tracedecay-cli/Cargo.toml index ff3248ad49..74b459bdcd 100644 --- a/crates/tracedecay-cli/Cargo.toml +++ b/crates/tracedecay-cli/Cargo.toml @@ -220,7 +220,6 @@ tracedecay-global-db = { path = "../tracedecay-global-db", version = "0.1.0", fe tracedecay-lcm = { path = "../tracedecay-lcm", version = "0.1.0" } tracedecay-runtime-core = { path = "../tracedecay-runtime-core", version = "0.1.0", features = ["test-helpers"] } tracedecay-session-temporal-store = { path = "../tracedecay-session-temporal-store", version = "0.1.0" } -ureq = { version = "3", features = ["json"] } [target.'cfg(target_os = "linux")'.dev-dependencies] xattr = "1" diff --git a/crates/tracedecay-hotpath-guard/Cargo.toml b/crates/tracedecay-hotpath-guard/Cargo.toml index 72f5df1a5b..6636a377ab 100644 --- a/crates/tracedecay-hotpath-guard/Cargo.toml +++ b/crates/tracedecay-hotpath-guard/Cargo.toml @@ -20,7 +20,6 @@ hotpath-mcp = ["hotpath", "hotpath/hotpath-mcp"] hotpath.workspace = true [dev-dependencies] -hotpath.workspace = true serde_json = "1" [[test]] diff --git a/crates/tracedecay/Cargo.toml b/crates/tracedecay/Cargo.toml index 509278f82a..8faf90edc7 100644 --- a/crates/tracedecay/Cargo.toml +++ b/crates/tracedecay/Cargo.toml @@ -356,7 +356,6 @@ libc = "0.2" tree-sitter = "0.26" rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] } tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12"] } -sha2 = "0.11" hex = "0.4" regex = "1.12.3" filetime = "0.2" From 93462dbedd49fb765d382802e0f19aa28ecb8c97 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:41:59 +0000 Subject: [PATCH 144/182] simplify(pass-3/5): drop duplicated dev-dependency lines Normal edges already provide the same crate: code-index-runtime and maintenance tempfile, and store-runtime graph-db. Co-authored-by: Zack Jackson --- crates/tracedecay-code-index-runtime/Cargo.toml | 1 - crates/tracedecay-maintenance/Cargo.toml | 1 - crates/tracedecay-store-runtime/Cargo.toml | 1 - 3 files changed, 3 deletions(-) diff --git a/crates/tracedecay-code-index-runtime/Cargo.toml b/crates/tracedecay-code-index-runtime/Cargo.toml index eb76dce256..8521c904d8 100644 --- a/crates/tracedecay-code-index-runtime/Cargo.toml +++ b/crates/tracedecay-code-index-runtime/Cargo.toml @@ -83,7 +83,6 @@ notify = "6" [dev-dependencies] filetime = "0.2" rusqlite = { version = "0.40.1", default-features = false, features = ["backup"] } -tempfile = "3" tokio = { version = "1", features = ["full", "test-util"] } tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] } tracedecay-code-index = { path = "../tracedecay-code-index", version = "0.1.0", default-features = false, features = [ diff --git a/crates/tracedecay-maintenance/Cargo.toml b/crates/tracedecay-maintenance/Cargo.toml index 54c4cda126..19a5bd6983 100644 --- a/crates/tracedecay-maintenance/Cargo.toml +++ b/crates/tracedecay-maintenance/Cargo.toml @@ -43,7 +43,6 @@ tracedecay-tool-catalog = { path = "../tracedecay-tool-catalog", version = "0.1. [dev-dependencies] filetime = "0.2" -tempfile = "3" tokio = { version = "1", features = ["full", "test-util"] } tracedecay-global-db = { path = "../tracedecay-global-db", version = "0.1.0", features = ["test-helpers"] } tracedecay-runtime-core = { path = "../tracedecay-runtime-core", version = "0.1.0", features = ["test-helpers"] } diff --git a/crates/tracedecay-store-runtime/Cargo.toml b/crates/tracedecay-store-runtime/Cargo.toml index 66df53c455..d581c9bf45 100644 --- a/crates/tracedecay-store-runtime/Cargo.toml +++ b/crates/tracedecay-store-runtime/Cargo.toml @@ -81,6 +81,5 @@ tokio = { version = "1", features = ["full", "test-util"] } # the process worker plan instead of answering WorkerPlanNotInstalled. tracedecay-code-index = { path = "../tracedecay-code-index", version = "0.1.0", default-features = false, features = ["test-helpers"] } tracedecay-code-index-runtime = { path = "../tracedecay-code-index-runtime", version = "0.1.0", default-features = false, features = ["test-helpers"] } -tracedecay-graph-db = { path = "../tracedecay-graph-db", version = "0.1.0" } tracedecay-global-db = { path = "../tracedecay-global-db", version = "0.1.0", features = ["test-helpers"] } tracedecay-runtime-core = { path = "../tracedecay-runtime-core", version = "0.1.0", features = ["test-helpers"] } From ec425b3df394fccc8a8593144eab7ec0774fe006 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:41:59 +0000 Subject: [PATCH 145/182] test(fixtures): share repeated manifest digests Suites re-rolled the same sha256 repeating-digit digest. One constructor now owns that spelling. Co-authored-by: Zack Jackson --- .../src/agents/context_scout/evidence.rs | 7 +--- .../src/native_integration/journey_tests.rs | 6 +-- .../src/native_integration/registry.rs | 4 +- .../src/native_integration/stack_signals.rs | 4 +- .../tests/context_scout_evidence.rs | 6 +-- .../tests/multi_root_read_model.rs | 8 ++-- .../advisory/host_delivery_consume_tests.rs | 8 ++-- .../src/lsp_runtime/advisory_source_tests.rs | 4 +- .../src/lsp_support/runtime_adapters.rs | 6 +-- .../native_integration/transaction_tests.rs | 4 +- .../src/observability/execution_emit.rs | 8 ++-- .../read_model/workflow_views.rs | 7 +--- .../src/observability/work_conflict_emit.rs | 4 +- .../work_owner_observation_recovery.rs | 8 ++-- .../production/affected_tests_tests.rs | 4 +- .../src/work/work_evidence_retrieval.rs | 9 ++--- .../github_stack_coordinator.rs | 4 +- .../native_declared_topology_projection.rs | 6 +-- .../workflow_topology_contract.rs | 8 ++-- .../src/automation/effect_runtime/problem.rs | 9 +---- .../effect_runtime/settlement/tests.rs | 7 +--- crates/tracedecay-cli/src/commands/daemon.rs | 6 +-- .../src/sessions_cmd/refresh/tests.rs | 6 +-- .../src/git_transactions/test_support.rs | 4 +- .../tracedecay-code-index/src/capabilities.rs | 5 +-- .../git_topology_edge_cases.rs | 6 +-- .../git_topology_projection.rs | 6 +-- .../tests/code_index_suite/support.rs | 10 ++--- .../src/config/scope_control.rs | 4 +- .../src/configuration/authorization.rs | 6 +-- .../src/configuration/operations.rs | 8 +--- .../src/external_source_tests.rs | 4 +- .../src/feedback/proximity_read.rs | 4 +- .../tracedecay-contracts/src/feedback/read.rs | 8 +--- .../src/git/stack_signal_expand.rs | 4 +- crates/tracedecay-contracts/src/git/tests.rs | 6 +-- .../src/result/envelope.rs | 7 +--- .../src/retained_receipts.rs | 7 +--- .../src/retained_surfaces/service.rs | 7 +--- .../src/work_evidence/tests.rs | 20 +++++----- .../tests/contracts_suite/handoff_open.rs | 4 +- .../tests/contracts_suite/multi_root_query.rs | 4 +- .../contracts_suite/multi_root_scope_set.rs | 8 ++-- .../contracts_suite/policy_composition.rs | 7 +--- .../work_artifact_hydration_service.rs | 6 +-- .../contracts_suite/work_placement_service.rs | 6 +-- .../work_product_application.rs | 4 +- .../contracts_suite/work_proposal_planner.rs | 14 +++---- .../work_run_control_service.rs | 8 ++-- .../contracts_suite/work_synthesis_service.rs | 4 +- .../contracts_suite/work_topology_view.rs | 12 +++--- .../contracts_suite/workflow_coordination.rs | 4 +- .../contracts_suite/workflow_dag_execution.rs | 18 ++++----- .../workflow_provider_registry.rs | 8 ++-- .../tests/contracts_suite/workflow_runtime.rs | 4 +- .../retained_http_identity_tests.rs | 4 +- .../src/automation_effect/journal/tests.rs | 8 ++-- .../invocation/configuration/settlement.rs | 4 +- .../src/invocation/work_attempt_exec/tests.rs | 4 +- .../tracedecay-domain/src/external_source.rs | 4 +- crates/tracedecay-domain/src/feedback/mod.rs | 4 +- crates/tracedecay-domain/src/lib.rs | 1 + .../src/observability/workflow.rs | 4 +- crates/tracedecay-domain/src/test_fixtures.rs | 39 +++++++++++++++++++ .../domain_suite/configuration_contract.rs | 7 +--- .../external_source_foundation_contract.rs | 18 ++++----- .../tests/domain_suite/feedback_contract.rs | 9 ++--- .../git_index_transaction_contract.rs | 5 +-- .../git_topology_anchor_contract.rs | 26 ++++++------- .../tests/domain_suite/multi_root_contract.rs | 8 ++-- .../domain_suite/repository_state_contract.rs | 9 ++--- .../work_execution_snapshot_contract.rs | 15 +++---- .../domain_suite/work_product_contract.rs | 32 +++++++-------- .../domain_suite/work_runtime_contract.rs | 18 ++++----- .../workflow_definition_contract.rs | 20 +++++----- .../src/configuration/store/tests/mod.rs | 6 +-- .../src/git_index_transactions/tests.rs | 10 ++--- .../src/native_integration/tests.rs | 16 ++++---- .../tracedecay-lsp/src/overlay/retention.rs | 6 +-- .../protocol/tests/workspace_diagnostics.rs | 4 +- .../src/handlers/hook_runtime/test_support.rs | 7 +--- crates/tracedecay-policy/src/configuration.rs | 4 +- .../tests/policy_suite/curation_apply.rs | 4 +- .../tests/policy_suite/routing_admission.rs | 7 +--- .../tests/policy_suite/work_planner.rs | 7 +--- .../src/repository/external_source/tests.rs | 6 +-- .../rusqlite_suite/handoff_open_storage.rs | 6 +-- .../rusqlite_suite/multi_root_scope_set.rs | 8 ++-- .../rusqlite_suite/work_attempt_storage.rs | 6 +-- .../work_duplicate_adjudication_storage.rs | 4 +- .../work_leak_adjudication_storage.rs | 8 ++-- .../rusqlite_suite/work_placement_storage.rs | 6 +-- .../work_product_graph_authority.rs | 6 +-- .../work_product_query_authority.rs | 14 +++---- .../work_run_control_storage.rs | 12 +++--- .../src/retained/profile.rs | 7 +--- .../store_suite/configuration_contract.rs | 6 +-- .../store_suite/external_source_commit.rs | 6 +-- .../store_suite/multi_root_cas_contract.rs | 6 +-- .../work_evidence_journey_tests.rs | 5 +-- .../advisory_runtime/scout_journey_tests.rs | 4 +- .../retained_timeout_dispatch_tests.rs | 4 +- .../handlers/stack_snapshot_behavior_tests.rs | 4 +- .../daemon_suite/invocation_observability.rs | 4 +- .../api_application_parity.rs | 4 +- .../daemon_runtime_acceptance.rs | 4 +- .../runtime_surface_acceptance.rs | 4 +- 107 files changed, 307 insertions(+), 507 deletions(-) create mode 100644 crates/tracedecay-domain/src/test_fixtures.rs diff --git a/crates/tracedecay-agent-hosts/src/agents/context_scout/evidence.rs b/crates/tracedecay-agent-hosts/src/agents/context_scout/evidence.rs index a2becc4a6a..b6c21810b4 100644 --- a/crates/tracedecay-agent-hosts/src/agents/context_scout/evidence.rs +++ b/crates/tracedecay-agent-hosts/src/agents/context_scout/evidence.rs @@ -338,8 +338,7 @@ pub(super) fn fixture_context_scout_evidence() -> ContextScoutEvidenceEnvelopeV1 PolicyDecisionRef, TemporalState, }; use tracedecay_domain::{ - CommitId, ComponentVersion, ManifestDigest, ProjectId, RefId, RepositoryId, TemporalModeV1, - WorktreeId, + CommitId, ComponentVersion, ProjectId, RefId, RepositoryId, TemporalModeV1, WorktreeId, }; fn id(value: &str) -> T @@ -349,9 +348,7 @@ pub(super) fn fixture_context_scout_evidence() -> ContextScoutEvidenceEnvelopeV1 { T::try_from(value.to_owned()).unwrap() } - fn digest(character: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", character.to_string().repeat(64))).unwrap() - } + use tracedecay_domain::test_fixtures::digest; let authorized_scope = ResolvedScope::new( id::("project.scout"), diff --git a/crates/tracedecay-agent-hosts/src/native_integration/journey_tests.rs b/crates/tracedecay-agent-hosts/src/native_integration/journey_tests.rs index 3665bd7274..8c660689c7 100644 --- a/crates/tracedecay-agent-hosts/src/native_integration/journey_tests.rs +++ b/crates/tracedecay-agent-hosts/src/native_integration/journey_tests.rs @@ -41,7 +41,7 @@ use tracedecay_contracts::{ use tracedecay_domain::{ ActorId, AuthorityRef, BranchStackEdgeV1, BranchStackId, BranchStackNodeV1, BranchStackRevisionId, BranchStackRevisionV1, BranchStackSourceV1, CapabilityId, CommitId, - ConfigurationRevisionId, FrozenBranchStackSnapshotV1, LocatorDigest, ManifestDigest, + ConfigurationRevisionId, FrozenBranchStackSnapshotV1, LocatorDigest, MechanicalIntegrationModeV1, NativeIntegrationAnalysisCoverageV1, NativeIntegrationAnalysisGapV1, NativeIntegrationApprovalId, NativeIntegrationApprovalV1, NativeIntegrationDirectionV1, NativeIntegrationPreviewDispositionV1, @@ -92,9 +92,7 @@ impl NativeIntegrationAnalysisPort for UnexpectedAnalysis { } } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).expect("digest") -} +use tracedecay_domain::test_fixtures::digest; fn git(root: &Path, arguments: &[&str]) -> String { let output = Command::new(try_git_program().expect("resolve the git program")) diff --git a/crates/tracedecay-agent-hosts/src/native_integration/registry.rs b/crates/tracedecay-agent-hosts/src/native_integration/registry.rs index 9ed4dd5497..f26b91966e 100644 --- a/crates/tracedecay-agent-hosts/src/native_integration/registry.rs +++ b/crates/tracedecay-agent-hosts/src/native_integration/registry.rs @@ -747,9 +747,7 @@ mod tests { ManifestDigest::new(format!("sha256:{}", "5".repeat(64))).expect("policy digest") } - fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).expect("digest") - } + use tracedecay_domain::test_fixtures::digest; fn git(root: &Path, arguments: &[&str]) { let status = Command::new(try_git_program().expect("resolve the git program")) diff --git a/crates/tracedecay-agent-hosts/src/native_integration/stack_signals.rs b/crates/tracedecay-agent-hosts/src/native_integration/stack_signals.rs index 77595795cc..872c48c948 100644 --- a/crates/tracedecay-agent-hosts/src/native_integration/stack_signals.rs +++ b/crates/tracedecay-agent-hosts/src/native_integration/stack_signals.rs @@ -180,9 +180,7 @@ mod tests { WorktreeId, WorktreeInventoryEpoch, WorktreeInventorySnapshotId, }; - fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).expect("digest") - } + use tracedecay_domain::test_fixtures::digest; fn oid(byte: char) -> GitOidV1 { GitOidV1::new(byte.to_string().repeat(40)).expect("oid") diff --git a/crates/tracedecay-agent-hosts/tests/context_scout_evidence.rs b/crates/tracedecay-agent-hosts/tests/context_scout_evidence.rs index 00bab4c590..bd97e1f26d 100644 --- a/crates/tracedecay-agent-hosts/tests/context_scout_evidence.rs +++ b/crates/tracedecay-agent-hosts/tests/context_scout_evidence.rs @@ -28,7 +28,7 @@ use tracedecay_domain::feedback::{ ProviderEvaluationStateV1, }; use tracedecay_domain::{ - CodeGenerationId, CommitId, ComponentVersion, ManifestDigest, ProjectId, RefId, RepositoryId, + CodeGenerationId, CommitId, ComponentVersion, ProjectId, RefId, RepositoryId, RetrievalAnchorId, SourceSpan, TemporalModeV1, UtcMicros, WorktreeId, }; @@ -40,9 +40,7 @@ where T::try_from(value.to_owned()).unwrap() } -fn digest(character: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", character.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn resolved_scope() -> ResolvedScope { ResolvedScope::new( diff --git a/crates/tracedecay-api/tests/multi_root_read_model.rs b/crates/tracedecay-api/tests/multi_root_read_model.rs index 94fdf71d43..cf1a41235f 100644 --- a/crates/tracedecay-api/tests/multi_root_read_model.rs +++ b/crates/tracedecay-api/tests/multi_root_read_model.rs @@ -3,13 +3,11 @@ use serde_json::Value; use tracedecay_api::read_model::multi_root::MultiRootQueryReadModelV1; use tracedecay_contracts::{MultiRootContinuationV1, MultiRootQueryPageV1, OpaqueCursor}; use tracedecay_domain::{ - CollectionRevision, ManifestDigest, RootGenerationV1, RootScopeOutcomeV1, ScopeOutcome, - ScopePartialReasonV1, ScopeSetId, ScopeSetRevision, ScopeUnavailableReasonV1, StackRevision, + CollectionRevision, RootGenerationV1, RootScopeOutcomeV1, ScopeOutcome, ScopePartialReasonV1, + ScopeSetId, ScopeSetRevision, ScopeUnavailableReasonV1, StackRevision, }; -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; #[test] fn dashboard_read_model_preserves_per_root_partial_truth() { diff --git a/crates/tracedecay-application/src/advisory/host_delivery_consume_tests.rs b/crates/tracedecay-application/src/advisory/host_delivery_consume_tests.rs index 68dda3f784..dda1718895 100644 --- a/crates/tracedecay-application/src/advisory/host_delivery_consume_tests.rs +++ b/crates/tracedecay-application/src/advisory/host_delivery_consume_tests.rs @@ -38,8 +38,8 @@ use tracedecay_domain::feedback::{ }; use tracedecay_domain::{ ActorId, CodeGenerationId, CommitId, ComponentVersion, ContentDigest, FileOccurrenceId, - LanguageDescriptorRevision, LanguageId, LocatorDigest, ManifestDigest, ProjectId, ProviderId, - RefId, RepositoryId, RetrievalAnchorId, SourceSpan, SymbolOccurrenceId, UtcMicros, WorktreeId, + LanguageDescriptorRevision, LanguageId, LocatorDigest, ProjectId, ProviderId, RefId, + RepositoryId, RetrievalAnchorId, SourceSpan, SymbolOccurrenceId, UtcMicros, WorktreeId, }; use tracedecay_hooks::{HookFeedbackDeliveryOutcomeV1, HookFeedbackRollbackSwitchV1}; use tracedecay_lsp::{ @@ -63,9 +63,7 @@ use crate::feedback::concrete::{FeedbackRuntime, open_feedback_runtime}; use crate::lsp_runtime::DaemonLspSessionFactory; use crate::source_authorization::ProjectSourceAccessSnapshot; -fn digest(fill: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", fill.to_string().repeat(64))).expect("digest") -} +use tracedecay_domain::test_fixtures::digest; fn resolved_scope() -> ResolvedScope { ResolvedScope::new( diff --git a/crates/tracedecay-application/src/lsp_runtime/advisory_source_tests.rs b/crates/tracedecay-application/src/lsp_runtime/advisory_source_tests.rs index f8419539a8..e49137b363 100644 --- a/crates/tracedecay-application/src/lsp_runtime/advisory_source_tests.rs +++ b/crates/tracedecay-application/src/lsp_runtime/advisory_source_tests.rs @@ -65,9 +65,7 @@ use crate::source_authorization::ProjectSourceAccessSnapshot; const SOURCE: &str = "fn reviewed() {}\n"; -fn digest(fill: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", fill.to_string().repeat(64))).expect("digest") -} +use tracedecay_domain::test_fixtures::digest; fn scope() -> ResolvedScope { ResolvedScope::new( diff --git a/crates/tracedecay-application/src/lsp_support/runtime_adapters.rs b/crates/tracedecay-application/src/lsp_support/runtime_adapters.rs index 420a39c263..cbac753870 100644 --- a/crates/tracedecay-application/src/lsp_support/runtime_adapters.rs +++ b/crates/tracedecay-application/src/lsp_support/runtime_adapters.rs @@ -392,7 +392,7 @@ fn broker_diagnostic(document_uri: &str, diagnostic: CodeDiagnostic) -> GatewayD mod tests { use std::sync::atomic::{AtomicUsize, Ordering}; - use tracedecay_domain::{CodeGenerationId, ContentDigest, ManifestDigest}; + use tracedecay_domain::{CodeGenerationId, ContentDigest}; use tracedecay_lsp::{ AuthorizedLspWorkspace, CanonicalWorkspaceDiagnosticRefreshRequest, IndexedWorkspaceDocument, ManagedDiagnosticSnapshot, @@ -400,9 +400,7 @@ mod tests { use super::*; - fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() - } + use tracedecay_domain::test_fixtures::digest; struct PublishingWorkspaceIndex { reads: AtomicUsize, diff --git a/crates/tracedecay-application/src/native_integration/transaction_tests.rs b/crates/tracedecay-application/src/native_integration/transaction_tests.rs index 888e2abcdf..781a0c191b 100644 --- a/crates/tracedecay-application/src/native_integration/transaction_tests.rs +++ b/crates/tracedecay-application/src/native_integration/transaction_tests.rs @@ -444,9 +444,7 @@ impl NativeIntegrationMechanics for ControlledMechanics { } } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).expect("digest") -} +use tracedecay_domain::test_fixtures::digest; fn oid(byte: char) -> GitOidV1 { GitOidV1::new(byte.to_string().repeat(40)).expect("object id") diff --git a/crates/tracedecay-application/src/observability/execution_emit.rs b/crates/tracedecay-application/src/observability/execution_emit.rs index 33adf5781e..13496b45d6 100644 --- a/crates/tracedecay-application/src/observability/execution_emit.rs +++ b/crates/tracedecay-application/src/observability/execution_emit.rs @@ -528,13 +528,11 @@ mod tests { NativeIntegrationPreviewProjectionV1, NativeIntegrationStatusProjectionV1, }; use tracedecay_domain::{ - ManifestDigest, NativeIntegrationPhaseV1, NativeIntegrationPreviewId, - NativeIntegrationTransactionId, ProjectId, RefId, RepositoryId, + NativeIntegrationPhaseV1, NativeIntegrationPreviewId, NativeIntegrationTransactionId, + ProjectId, RefId, RepositoryId, }; - fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() - } + use tracedecay_domain::test_fixtures::digest; fn identity(scope_ref: &str) -> ObservabilityProducerIdentityV1 { ObservabilityProducerIdentityV1 { diff --git a/crates/tracedecay-application/src/observability/read_model/workflow_views.rs b/crates/tracedecay-application/src/observability/read_model/workflow_views.rs index 9fe39653b8..53ec0fccd7 100644 --- a/crates/tracedecay-application/src/observability/read_model/workflow_views.rs +++ b/crates/tracedecay-application/src/observability/read_model/workflow_views.rs @@ -412,13 +412,10 @@ const fn required_names() -> [(&'static str, &'static str, &'static str); 11] { mod tests { use super::*; use tracedecay_domain::{ - ManifestDigest, ObservabilityRetentionClassV1, ObservabilityTerminalResultV1, RunId, - UtcMicros, + ObservabilityRetentionClassV1, ObservabilityTerminalResultV1, RunId, UtcMicros, }; - fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() - } + use tracedecay_domain::test_fixtures::digest; fn envelope(id: &str, payload: ObservabilityPayloadV1) -> ObservabilityEnvelopeV1 { let event_kind = payload.event_kind().to_owned(); diff --git a/crates/tracedecay-application/src/observability/work_conflict_emit.rs b/crates/tracedecay-application/src/observability/work_conflict_emit.rs index c062e2cfbe..e28a834015 100644 --- a/crates/tracedecay-application/src/observability/work_conflict_emit.rs +++ b/crates/tracedecay-application/src/observability/work_conflict_emit.rs @@ -324,9 +324,7 @@ mod tests { use crate::observability::RegisteredObservabilityPortV1; - fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() - } + use tracedecay_domain::test_fixtures::digest; fn oid(byte: char) -> GitOidV1 { GitOidV1::new(byte.to_string().repeat(40)).unwrap() diff --git a/crates/tracedecay-application/src/observability/work_owner_observation_recovery.rs b/crates/tracedecay-application/src/observability/work_owner_observation_recovery.rs index 64f993b120..95ecca2432 100644 --- a/crates/tracedecay-application/src/observability/work_owner_observation_recovery.rs +++ b/crates/tracedecay-application/src/observability/work_owner_observation_recovery.rs @@ -270,8 +270,8 @@ mod tests { }; use tracedecay_domain::{ ActorId, AttemptId, CoverageStateV1, DuplicateEffectOutcomeV1, DuplicateEffortKindV1, - ManifestDigest, ProjectId, ProjectionGenerationId, QuantityEvidenceClassV1, RepositoryId, - RunId, TaskId, UtcMicros, WorkAttemptIdentityV1, WorkAuthority, WorkCommandId, + ProjectId, ProjectionGenerationId, QuantityEvidenceClassV1, RepositoryId, RunId, TaskId, + UtcMicros, WorkAttemptIdentityV1, WorkAuthority, WorkCommandId, WorkDuplicateAdjudicationCommandV1, WorkDuplicateAdjudicationEvidenceV1, WorkDuplicateAdjudicationQuantitiesV1, WorkDuplicateAdjudicationReceiptV1, WorkDuplicateAdjudicationRevisionV1, WorkTopologyGenerationRefV1, WorktreeId, @@ -289,9 +289,7 @@ mod tests { T::try_from(value.into()).unwrap() } - fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() - } + use tracedecay_domain::test_fixtures::digest; fn attempt(ordinal: u32, suffix: &str) -> WorkAttemptIdentityV1 { WorkAttemptIdentityV1::new( diff --git a/crates/tracedecay-application/src/primitives/production/affected_tests_tests.rs b/crates/tracedecay-application/src/primitives/production/affected_tests_tests.rs index e3be7747e3..d7e07f143c 100644 --- a/crates/tracedecay-application/src/primitives/production/affected_tests_tests.rs +++ b/crates/tracedecay-application/src/primitives/production/affected_tests_tests.rs @@ -79,9 +79,7 @@ fn generation(value: &str) -> CodeGenerationId { CodeGenerationId::new(value).expect("generation") } -fn digest(value: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", value.to_string().repeat(64))).expect("digest") -} +use tracedecay_domain::test_fixtures::digest; fn content(value: char) -> ContentDigest { ContentDigest::new(format!("sha256:{}", value.to_string().repeat(64))).expect("content") diff --git a/crates/tracedecay-application/src/work/work_evidence_retrieval.rs b/crates/tracedecay-application/src/work/work_evidence_retrieval.rs index 71957abfbc..abd48b83cf 100644 --- a/crates/tracedecay-application/src/work/work_evidence_retrieval.rs +++ b/crates/tracedecay-application/src/work/work_evidence_retrieval.rs @@ -707,8 +707,8 @@ pub mod tests { WorkTaskSessionReauthorizationPortV1, WorkTaskSessionRequestV1, }; use tracedecay_domain::{ - ActorId, CalibrationProfileId, DiversityPolicy, FusionProfile, ManifestDigest, - PrivacyDomainId, RetrievalAnchorId, RetrievalBudget, RetrievalCursorKeyId, RetrieverKind, + ActorId, CalibrationProfileId, DiversityPolicy, FusionProfile, PrivacyDomainId, + RetrievalAnchorId, RetrievalBudget, RetrievalCursorKeyId, RetrieverKind, ScoreDomainCalibrationV1, ScoreDomainId, SourceStoreId, UtcMicros, WorkGraphVersionV1, WorkProductEventSequenceV1, WorkProductSourceWatermarkV1, }; @@ -726,10 +726,7 @@ pub mod tests { T::try_from(value.to_owned()).expect("TaskSession fixture identity") } - fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))) - .expect("TaskSession fixture digest") - } + use tracedecay_domain::test_fixtures::digest; pub fn context(scope: ResolvedScope) -> RequestContext { let grant = CapabilityGrantSnapshot::new( diff --git a/crates/tracedecay-application/tests/application_suite/github_stack_coordinator.rs b/crates/tracedecay-application/tests/application_suite/github_stack_coordinator.rs index 6f10b7020a..73b8d29b2a 100644 --- a/crates/tracedecay-application/tests/application_suite/github_stack_coordinator.rs +++ b/crates/tracedecay-application/tests/application_suite/github_stack_coordinator.rs @@ -177,9 +177,7 @@ impl StackDeliveryPort for RecordingDelivery { } } -fn digest(seed: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", seed.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn scope(index: usize) -> ResolvedScope { ResolvedScope::new( diff --git a/crates/tracedecay-application/tests/application_suite/native_declared_topology_projection.rs b/crates/tracedecay-application/tests/application_suite/native_declared_topology_projection.rs index 0c052f5fba..fc07aacf00 100644 --- a/crates/tracedecay-application/tests/application_suite/native_declared_topology_projection.rs +++ b/crates/tracedecay-application/tests/application_suite/native_declared_topology_projection.rs @@ -26,7 +26,7 @@ use tracedecay_contracts::{ }; use tracedecay_domain::{ ActorId, BrainId, BranchStackEdgeV1, BranchStackId, BranchStackNodeV1, BranchStackRevisionId, - BranchStackRevisionV1, CommitId, LocatorDigest, ManifestDigest, NativeIntegrationDirectionV1, + BranchStackRevisionV1, CommitId, LocatorDigest, NativeIntegrationDirectionV1, NativeIntegrationSelectionV1, ProjectId, RefId, RepositoryId, ScopeSetId, ScopeSetRevision, StackNodeId, UserProfileId, UtcMicros, WorktreeId, WorktreeInventoryEpoch, WorktreeInventorySnapshotId, @@ -287,9 +287,7 @@ fn run_git(root: &Path, args: &[&str]) -> String { .to_owned() } -fn digest(label: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", label.to_string().repeat(64))).expect("digest") -} +use tracedecay_domain::test_fixtures::digest; struct DeclaredStackRequest { project: ProjectId, diff --git a/crates/tracedecay-application/tests/application_suite/workflow_topology_contract.rs b/crates/tracedecay-application/tests/application_suite/workflow_topology_contract.rs index 5c8feff17f..b7df2d86d0 100644 --- a/crates/tracedecay-application/tests/application_suite/workflow_topology_contract.rs +++ b/crates/tracedecay-application/tests/application_suite/workflow_topology_contract.rs @@ -7,17 +7,15 @@ use tracedecay_application::work::workflow_topology::{ workflow_topology_namespace, workflow_topology_projection_identity, }; use tracedecay_domain::{ - ManifestDigest, ProjectId, WorkflowDefinition, WorkflowDefinitionId, WorkflowOperationRef, - WorkflowStep, WorkflowStepId, + ProjectId, WorkflowDefinition, WorkflowDefinitionId, WorkflowOperationRef, WorkflowStep, + WorkflowStepId, }; use tracedecay_graph_db::{ GraphIdempotencyKey, GraphNamespace, GraphProjectorRevision, NeverCancelled, VerifiedGraphSnapshot, }; -fn digest(label: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", label.to_string().repeat(64))).expect("digest") -} +use tracedecay_domain::test_fixtures::digest; fn step_id(label: &str) -> WorkflowStepId { WorkflowStepId::new(format!("step.{label}")).expect("step ID") diff --git a/crates/tracedecay-automation-runtime/src/automation/effect_runtime/problem.rs b/crates/tracedecay-automation-runtime/src/automation/effect_runtime/problem.rs index 58ea1e7eb0..f57b4a82e3 100644 --- a/crates/tracedecay-automation-runtime/src/automation/effect_runtime/problem.rs +++ b/crates/tracedecay-automation-runtime/src/automation/effect_runtime/problem.rs @@ -220,9 +220,7 @@ mod tests { CapabilityGrantSnapshot, Deadline, DisclosureClass, RequestContext, RequestId, ResolvedScope, }; - use tracedecay_domain::{ - ActorId, ManifestDigest, ProjectId, RepositoryId, UtcMicros, WorktreeId, - }; + use tracedecay_domain::{ActorId, ProjectId, RepositoryId, UtcMicros, WorktreeId}; use tracedecay_tool_catalog::{CapabilityId, UseCaseId}; use super::{failed_ledger_problem, failure_class_problem, runtime_problem}; @@ -245,10 +243,7 @@ mod tests { .expect("failed ledger") } - fn digest(seed: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", seed.to_string().repeat(64))) - .expect("fixture digest") - } + use tracedecay_domain::test_fixtures::digest; fn context_with_deadline(deadline: UtcMicros) -> RequestContext { let scope = ResolvedScope::new( diff --git a/crates/tracedecay-automation-runtime/src/automation/effect_runtime/settlement/tests.rs b/crates/tracedecay-automation-runtime/src/automation/effect_runtime/settlement/tests.rs index 0ec35d6f33..946a41543e 100644 --- a/crates/tracedecay-automation-runtime/src/automation/effect_runtime/settlement/tests.rs +++ b/crates/tracedecay-automation-runtime/src/automation/effect_runtime/settlement/tests.rs @@ -19,8 +19,7 @@ use tracedecay_contracts::{ }; use tracedecay_domain::{ ActorId, ComponentVersion, FactId, FactIdentityMaterialV1, FactIdentitySourceV1, FactOwnerV1, - ManifestDigest, ProjectId, ProvenanceId, RepositoryId, RunId, UtcMicros, WorktreeId, - canonical_sha256, + ProjectId, ProvenanceId, RepositoryId, RunId, UtcMicros, WorktreeId, canonical_sha256, }; use tracedecay_tool_catalog::EffectClass; @@ -42,9 +41,7 @@ impl crate::automation::backend::AgentTaskBackend for NeverAutomationBackend { } } -fn digest(seed: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", seed.to_string().repeat(64))).expect("fixture digest") -} +use tracedecay_domain::test_fixtures::digest; fn exact_publication(seed: char, payload_len: u64) -> ExactRunPublication { serde_json::from_value(json!({ diff --git a/crates/tracedecay-cli/src/commands/daemon.rs b/crates/tracedecay-cli/src/commands/daemon.rs index 2113fcb1e3..ef48f71d97 100644 --- a/crates/tracedecay-cli/src/commands/daemon.rs +++ b/crates/tracedecay-cli/src/commands/daemon.rs @@ -326,7 +326,7 @@ mod tests { ResolvedScope, ResultContractRef, RetrievalEvidence, RetryDirective, TemporalState, }; use tracedecay_domain::{ - ActorId, ComponentVersion, ManifestDigest, ProjectId, RepositoryId, UtcMicros, WorktreeId, + ActorId, ComponentVersion, ProjectId, RepositoryId, UtcMicros, WorktreeId, }; use tracedecay_tool_catalog::{CapabilityId, SchemaId, SortContractId, UseCaseId}; @@ -346,9 +346,7 @@ mod tests { .unwrap() } - fn digest(seed: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", seed.to_string().repeat(64))).unwrap() - } + use tracedecay_domain::test_fixtures::digest; fn context() -> RequestContext { let capability = CapabilityId::new("capability.cli.fixture").unwrap(); diff --git a/crates/tracedecay-cli/src/sessions_cmd/refresh/tests.rs b/crates/tracedecay-cli/src/sessions_cmd/refresh/tests.rs index 89b0477434..729c76876a 100644 --- a/crates/tracedecay-cli/src/sessions_cmd/refresh/tests.rs +++ b/crates/tracedecay-cli/src/sessions_cmd/refresh/tests.rs @@ -18,7 +18,7 @@ use tracedecay_contracts::{ }; use tracedecay_daemon_service::application_surface::retained::decode_request; use tracedecay_domain::{ - ActorId, ComponentVersion, ManifestDigest, ProjectId, RepositoryId, UtcMicros, WorktreeId, + ActorId, ComponentVersion, ProjectId, RepositoryId, UtcMicros, WorktreeId, }; use tracedecay_tool_catalog::SortContractId; @@ -31,9 +31,7 @@ use crate::cli::Cli; const PROFILE_ID: &str = "profile.0f2f1c3d4e5f60718293a4b5c6d7e8f9"; -fn digest(seed: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", seed.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn scope() -> ResolvedScope { ResolvedScope::new( diff --git a/crates/tracedecay-code-index-runtime/src/git_transactions/test_support.rs b/crates/tracedecay-code-index-runtime/src/git_transactions/test_support.rs index c71fb913eb..be5965e20a 100644 --- a/crates/tracedecay-code-index-runtime/src/git_transactions/test_support.rs +++ b/crates/tracedecay-code-index-runtime/src/git_transactions/test_support.rs @@ -232,9 +232,7 @@ where T::try_from(value.to_owned()).expect("fixture identity") } -pub fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).expect("fixture digest") -} +pub use tracedecay_domain::test_fixtures::digest; pub fn oid(byte: char) -> GitOidV1 { GitOidV1::new(byte.to_string().repeat(40)).expect("fixture object id") diff --git a/crates/tracedecay-code-index/src/capabilities.rs b/crates/tracedecay-code-index/src/capabilities.rs index b30bb3f85c..80c1678040 100644 --- a/crates/tracedecay-code-index/src/capabilities.rs +++ b/crates/tracedecay-code-index/src/capabilities.rs @@ -372,10 +372,7 @@ mod tests { use crate::languages::StaticLanguageRegistry; - fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))) - .expect("valid digest") - } + use tracedecay_domain::test_fixtures::digest; fn generation_manifest() -> CodeGenerationManifestV1 { let registry = StaticLanguageRegistry::new(); diff --git a/crates/tracedecay-code-index/tests/code_index_suite/git_topology_edge_cases.rs b/crates/tracedecay-code-index/tests/code_index_suite/git_topology_edge_cases.rs index 5b7f8deecb..693c8d9c0a 100644 --- a/crates/tracedecay-code-index/tests/code_index_suite/git_topology_edge_cases.rs +++ b/crates/tracedecay-code-index/tests/code_index_suite/git_topology_edge_cases.rs @@ -15,7 +15,7 @@ use tracedecay_code_index::git_projection::{ }; use tracedecay_domain::{ GitCommitIdentityV1, GitCommitMetadataV1, GitCoverageV1, GitHeadStateV1, GitHistoryV1, - GitOidV1, ManifestDigest, RefId, RepositoryId, UtcMicros, + GitOidV1, RefId, RepositoryId, UtcMicros, }; use tracedecay_graph_db::{ GraphCancellation, GraphGenerationManifest, GraphProjectionIdentity, GraphProjectorRevision, @@ -61,9 +61,7 @@ fn oid(label: char) -> GitOidV1 { GitOidV1::new(label.to_string().repeat(40)).expect("oid") } -fn digest(label: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", label.to_string().repeat(64))).expect("digest") -} +use tracedecay_domain::test_fixtures::digest; fn commit(label: char, parents: &[char]) -> GitCommitMetadataV1 { let identity = GitCommitIdentityV1 { diff --git a/crates/tracedecay-code-index/tests/code_index_suite/git_topology_projection.rs b/crates/tracedecay-code-index/tests/code_index_suite/git_topology_projection.rs index c1e85a19af..6ff40f0710 100644 --- a/crates/tracedecay-code-index/tests/code_index_suite/git_topology_projection.rs +++ b/crates/tracedecay-code-index/tests/code_index_suite/git_topology_projection.rs @@ -7,7 +7,7 @@ use tracedecay_code_index::git_projection::{ }; use tracedecay_domain::{ GitCommitIdentityV1, GitCommitMetadataV1, GitCoverageV1, GitHeadStateV1, GitHistoryV1, - GitOidV1, ManifestDigest, RefId, RepositoryId, UtcMicros, + GitOidV1, RefId, RepositoryId, UtcMicros, }; use tracedecay_graph_db::{ GraphNamespace, GraphProjectorRevision, NeverCancelled, VerifiedGraphSnapshot, @@ -17,9 +17,7 @@ fn oid(label: char) -> GitOidV1 { GitOidV1::new(label.to_string().repeat(40)).expect("oid") } -fn digest(label: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", label.to_string().repeat(64))).expect("digest") -} +use tracedecay_domain::test_fixtures::digest; fn commit(label: char, parents: &[char]) -> GitCommitMetadataV1 { let identity = GitCommitIdentityV1 { diff --git a/crates/tracedecay-code-index/tests/code_index_suite/support.rs b/crates/tracedecay-code-index/tests/code_index_suite/support.rs index e268f5e114..e7b74f6e1c 100644 --- a/crates/tracedecay-code-index/tests/code_index_suite/support.rs +++ b/crates/tracedecay-code-index/tests/code_index_suite/support.rs @@ -4,9 +4,9 @@ use tracedecay_code_index::chunks::content_digest; use tracedecay_code_index::intake::{CodeIndexIntake, ReceiptBoundCodeFileV1, SanitizedCodeIntake}; use tracedecay_code_index::languages::{LanguageRegistry, StaticLanguageRegistry}; use tracedecay_domain::{ - CodeGenerationId, FileOccurrenceId, LanguageDescriptorV1, LanguageId, ManifestDigest, - ProjectId, RepositoryId, SanitizationReceiptId, SanitizedCodeFileV1, SanitizedCodeSnapshotV1, - SanitizerRevision, SnapshotFileDispositionV1, UtcMicros, ValidatedCodeFileV1, + CodeGenerationId, FileOccurrenceId, LanguageDescriptorV1, LanguageId, ProjectId, RepositoryId, + SanitizationReceiptId, SanitizedCodeFileV1, SanitizedCodeSnapshotV1, SanitizerRevision, + SnapshotFileDispositionV1, UtcMicros, ValidatedCodeFileV1, }; pub const RUST_SOURCE: &str = "//! Module documentation.\n\nuse std::collections::HashMap;\n\n/// Increment a value.\npub fn alpha(value: u32) -> u32 {\n value + 1\n}\n\npub struct Holder {\n map: HashMap,\n}\n\nimpl Holder {\n pub fn get(&self, key: u32) -> Option {\n self.map.get(&key).copied()\n }\n}\n\n// trailing window text\n"; @@ -19,9 +19,7 @@ where T::try_from(value.to_owned()).expect("valid fixture identity") } -pub fn digest(byte: char) -> ManifestDigest { - id(&format!("sha256:{}", byte.to_string().repeat(64))) -} +pub use tracedecay_domain::test_fixtures::digest; pub fn registry() -> StaticLanguageRegistry { StaticLanguageRegistry::new() diff --git a/crates/tracedecay-configuration/src/config/scope_control.rs b/crates/tracedecay-configuration/src/config/scope_control.rs index 0e618283f8..967128d6d6 100644 --- a/crates/tracedecay-configuration/src/config/scope_control.rs +++ b/crates/tracedecay-configuration/src/config/scope_control.rs @@ -165,9 +165,7 @@ mod tests { T::try_from(value.to_owned()).unwrap() } - fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() - } + use tracedecay_domain::test_fixtures::digest; #[test] fn protected_dry_run_is_redacted_and_actor_bound() { diff --git a/crates/tracedecay-configuration/src/configuration/authorization.rs b/crates/tracedecay-configuration/src/configuration/authorization.rs index f59f7edd35..2707559795 100644 --- a/crates/tracedecay-configuration/src/configuration/authorization.rs +++ b/crates/tracedecay-configuration/src/configuration/authorization.rs @@ -135,7 +135,7 @@ mod tests { use tracedecay_domain::configuration::{ ConfigurationGrantReceiptId, ConfigurationMutationGrantReceiptV1, }; - use tracedecay_domain::{AccessPolicyDigest, ActorId, ManifestDigest}; + use tracedecay_domain::{AccessPolicyDigest, ActorId}; use tracedecay_policy::configuration::{ ConfigurationMutationGrantStateV1, ConfigurationMutationPermissionV1, }; @@ -163,9 +163,7 @@ mod tests { T::try_from(value.to_owned()).unwrap() } - fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() - } + use tracedecay_domain::test_fixtures::digest; fn policy_digest(byte: char) -> AccessPolicyDigest { AccessPolicyDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() diff --git a/crates/tracedecay-configuration/src/configuration/operations.rs b/crates/tracedecay-configuration/src/configuration/operations.rs index 68f70dce45..7de552cf6d 100644 --- a/crates/tracedecay-configuration/src/configuration/operations.rs +++ b/crates/tracedecay-configuration/src/configuration/operations.rs @@ -614,18 +614,14 @@ mod tests { ConfigurationMutationGrantReceiptV1, ConfigurationSnapshotV1, ConfigurationValueV1, ProtectedChange, ScopeSourceBinding, SettingKey, SourceBindingId, SourceKindV1, }; - use tracedecay_domain::{ - AccessPolicyDigest, ActorId, LocatorDigest, ManifestDigest, ProjectId, - }; + use tracedecay_domain::{AccessPolicyDigest, ActorId, LocatorDigest, ProjectId}; use tracedecay_global_db::configuration::contracts::ports::{ ConfigurationControlStore, ConfigurationCurrentStateV1, ConfigurationOperationFuture, CurrentConfigurationMutationAuthorizationV1, }; use tracedecay_global_db::configuration::contracts::types::ConfigurationSettlementAuthorityV1; - fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() - } + use tracedecay_domain::test_fixtures::digest; fn policy_digest(byte: char) -> AccessPolicyDigest { AccessPolicyDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() diff --git a/crates/tracedecay-contracts/src/external_source_tests.rs b/crates/tracedecay-contracts/src/external_source_tests.rs index db9a641bec..8570454cde 100644 --- a/crates/tracedecay-contracts/src/external_source_tests.rs +++ b/crates/tracedecay-contracts/src/external_source_tests.rs @@ -6,9 +6,7 @@ use tracedecay_domain::{ SourcePartitionIdV1, SourceSnapshotIdV1, }; -fn digest(seed: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", seed.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn acquisition_contract( capture_mode: SourceCaptureModeV1, diff --git a/crates/tracedecay-contracts/src/feedback/proximity_read.rs b/crates/tracedecay-contracts/src/feedback/proximity_read.rs index fb5678c546..1ed3acf44f 100644 --- a/crates/tracedecay-contracts/src/feedback/proximity_read.rs +++ b/crates/tracedecay-contracts/src/feedback/proximity_read.rs @@ -473,9 +473,7 @@ mod tests { use super::*; - fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).expect("digest") - } + use tracedecay_domain::test_fixtures::digest; fn scope() -> FeedbackScopeV1 { FeedbackScopeV1 { diff --git a/crates/tracedecay-contracts/src/feedback/read.rs b/crates/tracedecay-contracts/src/feedback/read.rs index 0fd6097ea9..be55569f0f 100644 --- a/crates/tracedecay-contracts/src/feedback/read.rs +++ b/crates/tracedecay-contracts/src/feedback/read.rs @@ -688,9 +688,7 @@ mod invocation_tests { FeedbackFindingId, FeedbackFindingLifecycleV1, FeedbackFindingV1, FeedbackResultId, FeedbackScopeV1, ProviderEvaluationStateV1, }; - use tracedecay_domain::{ - CommitId, ManifestDigest, ProjectId, RepositoryId, RetrievalAnchorId, WorktreeId, - }; + use tracedecay_domain::{CommitId, ProjectId, RepositoryId, RetrievalAnchorId, WorktreeId}; use super::{ CanonicalAffectedTestsProjectionV1, CanonicalFeedbackImpactProjectionV1, @@ -841,7 +839,5 @@ mod invocation_tests { FeedbackCycleId::new("cycle.feedback-test").expect("cycle") } - fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).expect("digest") - } + use tracedecay_domain::test_fixtures::digest; } diff --git a/crates/tracedecay-contracts/src/git/stack_signal_expand.rs b/crates/tracedecay-contracts/src/git/stack_signal_expand.rs index fc84d198c2..7b9b344540 100644 --- a/crates/tracedecay-contracts/src/git/stack_signal_expand.rs +++ b/crates/tracedecay-contracts/src/git/stack_signal_expand.rs @@ -270,9 +270,7 @@ impl GitHubStackSignalExpandPortError { mod tests { use super::*; - fn digest(seed: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", seed.to_string().repeat(64))).expect("digest") - } + use tracedecay_domain::test_fixtures::digest; fn native_preview() -> GitHubStackSignalNativePreviewV1 { GitHubStackSignalNativePreviewV1 { diff --git a/crates/tracedecay-contracts/src/git/tests.rs b/crates/tracedecay-contracts/src/git/tests.rs index 952cbae2b6..8fcf5589cf 100644 --- a/crates/tracedecay-contracts/src/git/tests.rs +++ b/crates/tracedecay-contracts/src/git/tests.rs @@ -4,7 +4,7 @@ use tracedecay_domain::{ ActorId, ComponentVersion, GitCommitIdentityV1, GitCoverageV1, GitHeadStateV1, GitIndexCommitIntentV1, GitIndexPreviewDispositionV1, GitIndexPreviewId, GitIndexPreviewV1, GitIndexSigningPolicyV1, GitIndexTransactionOperationV1, GitObjectFormatV1, GitOidV1, - GitOperationStateV1, ManifestDigest, ProjectId, RefId, RepositoryId, RepositoryIndexSnapshotV1, + GitOperationStateV1, ProjectId, RefId, RepositoryId, RepositoryIndexSnapshotV1, RepositoryIndexStateV1, RepositoryStateSnapshotV1, RepositoryWorkingTreeSnapshotV1, RepositoryWorkingTreeStateV1, UtcMicros, WorktreeId, }; @@ -29,9 +29,7 @@ where T::try_from(value.to_owned()).expect("fixture id") } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).expect("fixture digest") -} +use tracedecay_domain::test_fixtures::digest; fn oid(byte: char) -> GitOidV1 { GitOidV1::new(byte.to_string().repeat(40)).expect("fixture oid") diff --git a/crates/tracedecay-contracts/src/result/envelope.rs b/crates/tracedecay-contracts/src/result/envelope.rs index 9377fc40a8..acaf462af2 100644 --- a/crates/tracedecay-contracts/src/result/envelope.rs +++ b/crates/tracedecay-contracts/src/result/envelope.rs @@ -726,13 +726,10 @@ mod tests { use super::*; use crate::{EffectTermination, IdempotencyKey}; use serde_json::Value; - use tracedecay_domain::{ActorId, ManifestDigest, ProjectId, RepositoryId, WorktreeId}; + use tracedecay_domain::{ActorId, ProjectId, RepositoryId, WorktreeId}; use tracedecay_tool_catalog::{EffectClass, SchemaId, UseCaseId}; - fn digest(seed: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", seed.to_string().repeat(64))) - .expect("fixture digest is valid") - } + use tracedecay_domain::test_fixtures::digest; fn receipt() -> EffectReceipt { let expected_state = digest('a'); diff --git a/crates/tracedecay-contracts/src/retained_receipts.rs b/crates/tracedecay-contracts/src/retained_receipts.rs index b04ac77bd5..afd31411ab 100644 --- a/crates/tracedecay-contracts/src/retained_receipts.rs +++ b/crates/tracedecay-contracts/src/retained_receipts.rs @@ -688,7 +688,7 @@ mod tests { use std::collections::BTreeSet; use tracedecay_domain::{ - ActorId, ManifestDigest, ProjectId, RepositoryId, UtcMicros, WorktreeId, canonical_sha256, + ActorId, ProjectId, RepositoryId, UtcMicros, WorktreeId, canonical_sha256, }; use crate::retained_surfaces::{ @@ -704,10 +704,7 @@ mod tests { use super::session_refresh_effect_outcome; - fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))) - .expect("valid digest") - } + use tracedecay_domain::test_fixtures::digest; fn refresh_effect_settlement( reconciliation_required: bool, diff --git a/crates/tracedecay-contracts/src/retained_surfaces/service.rs b/crates/tracedecay-contracts/src/retained_surfaces/service.rs index f59b51f139..187acf3c5f 100644 --- a/crates/tracedecay-contracts/src/retained_surfaces/service.rs +++ b/crates/tracedecay-contracts/src/retained_surfaces/service.rs @@ -779,7 +779,7 @@ mod tests { CapabilityGrantSnapshot, Deadline, EffectTermination, IdempotencyKey, ProblemTerminality, RequestId, ResolvedScope, }; - use tracedecay_domain::{ActorId, ManifestDigest, ProjectId, RepositoryId, WorktreeId}; + use tracedecay_domain::{ActorId, ProjectId, RepositoryId, WorktreeId}; use tracedecay_tool_catalog::EffectClass; struct ErrorMemoryPort(RetainedSurfaceExecutionErrorV1); @@ -807,10 +807,7 @@ mod tests { T::try_from(value.to_owned()).expect("fixture identity is valid") } - fn digest(seed: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", seed.to_string().repeat(64))) - .expect("fixture digest is valid") - } + use tracedecay_domain::test_fixtures::digest; fn scope() -> ResolvedScope { ResolvedScope::new( diff --git a/crates/tracedecay-contracts/src/work_evidence/tests.rs b/crates/tracedecay-contracts/src/work_evidence/tests.rs index f54d4c607e..da52035fdf 100644 --- a/crates/tracedecay-contracts/src/work_evidence/tests.rs +++ b/crates/tracedecay-contracts/src/work_evidence/tests.rs @@ -5,14 +5,14 @@ use std::sync::{ }; use tracedecay_domain::{ - ActorId, AttemptId, BrainId, InitiativeId, ManifestDigest, MilestoneId, - ObservationSourceIdentityV1, ProjectId, ProviderId, RepositoryId, RetrievalAnchorId, RunId, - SessionId, SourceStoreId, TaskEvidenceLinkId, TaskEvidenceLinkV1, TaskId, UserProfileId, - UtcMicros, WorkAcceptanceCriterionV1, WorkAttemptIdentityV1, WorkGraphChangeV1, - WorkGraphVersionV1, WorkHierarchyV1, WorkInitiativeV1, WorkItemInputV1, WorkItemV1, - WorkMilestoneV1, WorkPlanId, WorkPlanV1, WorkProductEventSequenceV1, WorkProductGraphV1, - WorkProductSourceWatermarkV1, WorkProposalV1, WorkProviderRouteId, WorkProviderRouteV1, - WorkRouteDecisionV1, WorkScoreKindV1, WorkShapeAssessmentV1, WorkSizingV1, WorktreeId, + ActorId, AttemptId, BrainId, InitiativeId, MilestoneId, ObservationSourceIdentityV1, ProjectId, + ProviderId, RepositoryId, RetrievalAnchorId, RunId, SessionId, SourceStoreId, + TaskEvidenceLinkId, TaskEvidenceLinkV1, TaskId, UserProfileId, UtcMicros, + WorkAcceptanceCriterionV1, WorkAttemptIdentityV1, WorkGraphChangeV1, WorkGraphVersionV1, + WorkHierarchyV1, WorkInitiativeV1, WorkItemInputV1, WorkItemV1, WorkMilestoneV1, WorkPlanId, + WorkPlanV1, WorkProductEventSequenceV1, WorkProductGraphV1, WorkProductSourceWatermarkV1, + WorkProposalV1, WorkProviderRouteId, WorkProviderRouteV1, WorkRouteDecisionV1, WorkScoreKindV1, + WorkShapeAssessmentV1, WorkSizingV1, WorktreeId, }; use tracedecay_tool_catalog::{CapabilityId, UseCaseId}; @@ -30,9 +30,7 @@ where T::try_from(value.to_owned()).unwrap() } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn selection() -> WorkProductSelectionScopeV1 { WorkProductSelectionScopeV1::relations(BTreeSet::from([ diff --git a/crates/tracedecay-contracts/tests/contracts_suite/handoff_open.rs b/crates/tracedecay-contracts/tests/contracts_suite/handoff_open.rs index 5da701f329..d6b930acf7 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/handoff_open.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/handoff_open.rs @@ -173,9 +173,7 @@ impl HandoffOpenTargetPort for CurrentTargets { } } -fn digest(fill: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", fill.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn scope() -> ResolvedScope { ResolvedScope::new( diff --git a/crates/tracedecay-contracts/tests/contracts_suite/multi_root_query.rs b/crates/tracedecay-contracts/tests/contracts_suite/multi_root_query.rs index 60e2fc75e2..e1fb25b682 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/multi_root_query.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/multi_root_query.rs @@ -27,9 +27,7 @@ where T::try_from(value.to_owned()).unwrap() } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn context(worktree: &str, suffix: &str) -> RequestContext { let scope = ResolvedScope::new( diff --git a/crates/tracedecay-contracts/tests/contracts_suite/multi_root_scope_set.rs b/crates/tracedecay-contracts/tests/contracts_suite/multi_root_scope_set.rs index 2e617f6788..dd0a17ff7c 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/multi_root_scope_set.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/multi_root_scope_set.rs @@ -11,8 +11,8 @@ use tracedecay_contracts::{ RequestId, ResolvedScope, SharedProfileStoreLocatorV1, }; use tracedecay_domain::{ - ActorId, BrainId, ManifestDigest, ProjectId, RefId, RepositoryId, ScopeSetId, ScopeSetRevision, - UserProfileId, UtcMicros, WorktreeId, + ActorId, BrainId, ProjectId, RefId, RepositoryId, ScopeSetId, ScopeSetRevision, UserProfileId, + UtcMicros, WorktreeId, }; use tracedecay_tool_catalog::{CapabilityId, UseCaseId}; @@ -27,9 +27,7 @@ where T::try_from(value.to_owned()).unwrap() } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn context(worktree: &str, suffix: &str) -> RequestContext { context_at("project.fixture", "repository.fixture", worktree, suffix) diff --git a/crates/tracedecay-contracts/tests/contracts_suite/policy_composition.rs b/crates/tracedecay-contracts/tests/contracts_suite/policy_composition.rs index 774a4bc294..7a9cbcf53c 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/policy_composition.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/policy_composition.rs @@ -9,8 +9,7 @@ use tracedecay_contracts::{ }; use tracedecay_domain::configuration::{ConfigurationRevisionId, ConfigurationSnapshotV1}; use tracedecay_domain::{ - ActorId, ManifestDigest, ProjectId, RefId, RepositoryId, ShardId, UtcMicros, VectorWatermark, - WorktreeId, + ActorId, ProjectId, RefId, RepositoryId, ShardId, UtcMicros, VectorWatermark, WorktreeId, }; use tracedecay_policy::routing::{ CapabilityAvailabilityV1, CapabilityEffectClassV1, CapabilityRoutingDispositionV1, @@ -26,9 +25,7 @@ where T::try_from(value.to_owned()).unwrap() } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn evaluation_context_for( capability: CapabilityId, diff --git a/crates/tracedecay-contracts/tests/contracts_suite/work_artifact_hydration_service.rs b/crates/tracedecay-contracts/tests/contracts_suite/work_artifact_hydration_service.rs index 1aa740671e..44669bfe43 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/work_artifact_hydration_service.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/work_artifact_hydration_service.rs @@ -15,7 +15,7 @@ use tracedecay_contracts::{ WorkAttemptTopologyBindingV1, WorkAttemptTopologyStateV1, }; use tracedecay_domain::{ - ActorId, ManifestDigest, ProjectId, ProviderId, RepositoryId, UtcMicros, WorkArtifactRefV1, + ActorId, ProjectId, ProviderId, RepositoryId, UtcMicros, WorkArtifactRefV1, WorkAttemptIdentityV1, WorkAuthority, WorkProviderRouteId, WorkProviderRouteV1, WorktreeId, }; use tracedecay_tool_catalog::{CapabilityId, UseCaseId}; @@ -28,9 +28,7 @@ where T::try_from(value.to_owned()).unwrap() } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn context(project: &str) -> RequestContext { let scope = ResolvedScope::new( diff --git a/crates/tracedecay-contracts/tests/contracts_suite/work_placement_service.rs b/crates/tracedecay-contracts/tests/contracts_suite/work_placement_service.rs index 7369ba6e59..9df8f07264 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/work_placement_service.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/work_placement_service.rs @@ -24,7 +24,7 @@ use tracedecay_contracts::{ WorkPlacementStorageError, WorkPlacementStoragePort, }; use tracedecay_domain::{ - ActorId, ManifestDigest, ProjectId, RepositoryId, RunId, TaskId, UtcMicros, WorkAuthority, + ActorId, ProjectId, RepositoryId, RunId, TaskId, UtcMicros, WorkAuthority, WorkPlacementBlockerV1, WorkPlacementIdentityV1, WorkPlacementKindV1, WorkPlacementObservationV1, WorkPlacementStateV1, WorkPlacementTargetV1, WorkPlacementV1, WorktreeId, @@ -39,9 +39,7 @@ where T::try_from(value.to_owned()).unwrap() } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn context(actor: &str) -> RequestContext { let scope = ResolvedScope::new( diff --git a/crates/tracedecay-contracts/tests/contracts_suite/work_product_application.rs b/crates/tracedecay-contracts/tests/contracts_suite/work_product_application.rs index 7686d0ca80..d95e5cedf8 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/work_product_application.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/work_product_application.rs @@ -43,9 +43,7 @@ where T::try_from(value.to_owned()).unwrap() } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn binding() -> WorkProductBindingV1 { WorkProductBindingV1::new( diff --git a/crates/tracedecay-contracts/tests/contracts_suite/work_proposal_planner.rs b/crates/tracedecay-contracts/tests/contracts_suite/work_proposal_planner.rs index f48bb65b3a..64a17bada9 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/work_proposal_planner.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/work_proposal_planner.rs @@ -21,11 +21,11 @@ use tracedecay_contracts::{ WorkRoutingSnapshotErrorV1, WorkRoutingSnapshotPortV1, WorkRoutingSnapshotV1, }; use tracedecay_domain::{ - ActorId, InitiativeId, ManifestDigest, MilestoneId, ProjectId, ProjectionGenerationId, - ProposalId, RepositoryId, TaskId, UtcMicros, WorkApprovalPolicy, WorkEgressPolicy, - WorkExecutionLimits, WorkFallbackTopology, WorkFilesystemPolicy, WorkGraphVersionV1, - WorkHierarchyV1, WorkInitiativeV1, WorkItemInputV1, WorkItemV1, WorkMilestoneV1, WorkPlanId, - WorkPlanV1, WorkProductGraphV1, WorkProductProjectionBundleV1, WorkProductSourceWatermarkV1, + ActorId, InitiativeId, MilestoneId, ProjectId, ProjectionGenerationId, ProposalId, + RepositoryId, TaskId, UtcMicros, WorkApprovalPolicy, WorkEgressPolicy, WorkExecutionLimits, + WorkFallbackTopology, WorkFilesystemPolicy, WorkGraphVersionV1, WorkHierarchyV1, + WorkInitiativeV1, WorkItemInputV1, WorkItemV1, WorkMilestoneV1, WorkPlanId, WorkPlanV1, + WorkProductGraphV1, WorkProductProjectionBundleV1, WorkProductSourceWatermarkV1, WorkProjectionSequenceV1, WorkRouteExecutionProfileV1, WorkRuntimeProjectionCoverageV1, WorkRuntimeProjectionV1, WorkSandboxPolicy, WorktreeId, }; @@ -52,9 +52,7 @@ where T::try_from(value.to_owned()).unwrap() } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn context(project: &str) -> RequestContext { let scope = ResolvedScope::new( diff --git a/crates/tracedecay-contracts/tests/contracts_suite/work_run_control_service.rs b/crates/tracedecay-contracts/tests/contracts_suite/work_run_control_service.rs index 90ea815213..afde1bc710 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/work_run_control_service.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/work_run_control_service.rs @@ -24,8 +24,8 @@ use tracedecay_contracts::{ WorkRunControlStoragePort, WorkRunLiveAttemptV1, }; use tracedecay_domain::{ - ActorId, AttemptId, ManifestDigest, ProjectId, RepositoryId, RunId, TaskId, UtcMicros, - WorkAuthority, WorkBlockedIntervalReceiptV1, WorkRunControlAuthorityV1, WorkRunControlReasonV1, + ActorId, AttemptId, ProjectId, RepositoryId, RunId, TaskId, UtcMicros, WorkAuthority, + WorkBlockedIntervalReceiptV1, WorkRunControlAuthorityV1, WorkRunControlReasonV1, WorkRunControlStateV1, WorkRunControlV1, WorkflowStepId, WorktreeId, }; use tracedecay_tool_catalog::{CapabilityId, UseCaseId}; @@ -40,9 +40,7 @@ where T::try_from(value.to_owned()).unwrap() } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn task() -> TaskId { id::("task.run-control") diff --git a/crates/tracedecay-contracts/tests/contracts_suite/work_synthesis_service.rs b/crates/tracedecay-contracts/tests/contracts_suite/work_synthesis_service.rs index 6c92df29da..25af595912 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/work_synthesis_service.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/work_synthesis_service.rs @@ -42,9 +42,7 @@ where T::try_from(value.to_owned()).unwrap() } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn context(project: &str, actor: &str) -> RequestContext { let scope = ResolvedScope::new( diff --git a/crates/tracedecay-contracts/tests/contracts_suite/work_topology_view.rs b/crates/tracedecay-contracts/tests/contracts_suite/work_topology_view.rs index 9ef7c55007..80cd27d79a 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/work_topology_view.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/work_topology_view.rs @@ -22,10 +22,10 @@ use tracedecay_contracts::{ }; use tracedecay_domain::configuration::safe_work_topology_policy_v1; use tracedecay_domain::{ - ActorId, CommitId, ConfigurationRevisionId, ConfigurationSnapshotId, ManifestDigest, ProjectId, - ProviderId, RefId, RepositoryId, TaskId, UtcMicros, WorkApprovalPolicy, WorkAuthority, - WorkEffectStateV1, WorkEgressPolicy, WorkExecutableReference, WorkExecutionLimits, - WorkExecutionSnapshot, WorkExecutionSnapshotInput, WorkFallbackTopology, WorkFilesystemPolicy, + ActorId, CommitId, ConfigurationRevisionId, ConfigurationSnapshotId, ProjectId, ProviderId, + RefId, RepositoryId, TaskId, UtcMicros, WorkApprovalPolicy, WorkAuthority, WorkEffectStateV1, + WorkEgressPolicy, WorkExecutableReference, WorkExecutionLimits, WorkExecutionSnapshot, + WorkExecutionSnapshotInput, WorkFallbackTopology, WorkFilesystemPolicy, WorkPlacementIdentityV1, WorkPlacementKindV1, WorkPlacementObservationV1, WorkPlacementStateV1, WorkPlacementTargetV1, WorkPlacementV1, WorkProviderBackendV1, WorkProviderProtocol, WorkProviderRouteId, WorkProviderRouteV1, WorkSandboxPolicy, WorkflowOperationRef, WorktreeId, @@ -40,9 +40,7 @@ where T::try_from(value.to_owned()).unwrap() } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn context(project: &str) -> RequestContext { let scope = ResolvedScope::new( diff --git a/crates/tracedecay-contracts/tests/contracts_suite/workflow_coordination.rs b/crates/tracedecay-contracts/tests/contracts_suite/workflow_coordination.rs index aea9d981f5..f05ccb9001 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/workflow_coordination.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/workflow_coordination.rs @@ -28,9 +28,7 @@ where T::try_from(value.to_owned()).unwrap() } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn workflow_context( actor: ActorId, diff --git a/crates/tracedecay-contracts/tests/contracts_suite/workflow_dag_execution.rs b/crates/tracedecay-contracts/tests/contracts_suite/workflow_dag_execution.rs index b7f2c091ad..cf5ffff201 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/workflow_dag_execution.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/workflow_dag_execution.rs @@ -8,13 +8,13 @@ use tracedecay_contracts::{ }; use tracedecay_domain::configuration::safe_work_topology_policy_v1; use tracedecay_domain::{ - AttemptId, ManifestDigest, ProjectId, ProviderId, RunId, TaskId, UtcMicros, WorkArtifactId, - WorkArtifactRefV1, WorkAttemptIdentityV1, WorkCommandId, WorkProviderBackendV1, - WorkProviderRouteId, WorkProviderRouteV1, WorkflowDefinition, WorkflowDefinitionId, - WorkflowOperationRef, WorkflowOutputArtifact, WorkflowOutputName, WorkflowOutputReference, - WorkflowPlacementReceipt, WorkflowRunCommand, WorkflowRunEvent, WorkflowRunEventContext, - WorkflowRunProjection, WorkflowRunStatus, WorkflowStep, WorkflowStepEffectOutcome, - WorkflowStepEffectReceipt, WorkflowStepId, WorkflowStepOutput, + AttemptId, ProjectId, ProviderId, RunId, TaskId, UtcMicros, WorkArtifactId, WorkArtifactRefV1, + WorkAttemptIdentityV1, WorkCommandId, WorkProviderBackendV1, WorkProviderRouteId, + WorkProviderRouteV1, WorkflowDefinition, WorkflowDefinitionId, WorkflowOperationRef, + WorkflowOutputArtifact, WorkflowOutputName, WorkflowOutputReference, WorkflowPlacementReceipt, + WorkflowRunCommand, WorkflowRunEvent, WorkflowRunEventContext, WorkflowRunProjection, + WorkflowRunStatus, WorkflowStep, WorkflowStepEffectOutcome, WorkflowStepEffectReceipt, + WorkflowStepId, WorkflowStepOutput, }; fn id(value: &str) -> T @@ -25,9 +25,7 @@ where T::try_from(value.to_owned()).unwrap() } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn context(command: &str, input: char, occurred_at: i64) -> WorkflowRunEventContext { WorkflowRunEventContext { diff --git a/crates/tracedecay-contracts/tests/contracts_suite/workflow_provider_registry.rs b/crates/tracedecay-contracts/tests/contracts_suite/workflow_provider_registry.rs index a599f11ef4..aab1a63811 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/workflow_provider_registry.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/workflow_provider_registry.rs @@ -4,8 +4,8 @@ use tracedecay_contracts::{ }; use tracedecay_domain::configuration::safe_work_topology_policy_v1; use tracedecay_domain::{ - ManifestDigest, ProviderId, RunId, WorkProviderBackendV1, WorkProviderRouteId, - WorkProviderRouteV1, WorkflowStepId, + ProviderId, RunId, WorkProviderBackendV1, WorkProviderRouteId, WorkProviderRouteV1, + WorkflowStepId, }; fn id(value: &str) -> T @@ -16,9 +16,7 @@ where T::try_from(value.to_owned()).unwrap() } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn registration( provider: &str, diff --git a/crates/tracedecay-contracts/tests/contracts_suite/workflow_runtime.rs b/crates/tracedecay-contracts/tests/contracts_suite/workflow_runtime.rs index 71056d3dc8..31cc8b0305 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/workflow_runtime.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/workflow_runtime.rs @@ -28,9 +28,7 @@ where T::try_from(value.to_owned()).unwrap() } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn fan_out_input(identity: &str, input_digest: ManifestDigest) -> WorkflowFanOutInput { let task_id = id::(&format!("task.workflow.runtime.{identity}")); diff --git a/crates/tracedecay-daemon-service/src/application_surface/retained_http_identity_tests.rs b/crates/tracedecay-daemon-service/src/application_surface/retained_http_identity_tests.rs index 118186cda1..642ab177bf 100644 --- a/crates/tracedecay-daemon-service/src/application_surface/retained_http_identity_tests.rs +++ b/crates/tracedecay-daemon-service/src/application_surface/retained_http_identity_tests.rs @@ -23,9 +23,7 @@ use super::super::registered_http::{RegisteredHttpOperation, invoke_registered_h use super::validated_daemon_outcome; use tracedecay_api::WorkOperation; -fn digest(seed: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", seed.to_string().repeat(64))).expect("fixture digest") -} +use tracedecay_domain::test_fixtures::digest; fn retained_scope(seed: &str) -> ResolvedScope { ResolvedScope::new( diff --git a/crates/tracedecay-daemon-service/src/automation_effect/journal/tests.rs b/crates/tracedecay-daemon-service/src/automation_effect/journal/tests.rs index 03d83ac465..a40c89af90 100644 --- a/crates/tracedecay-daemon-service/src/automation_effect/journal/tests.rs +++ b/crates/tracedecay-daemon-service/src/automation_effect/journal/tests.rs @@ -15,8 +15,8 @@ use tracedecay_contracts::{ OperationReceipt, PolicyDecisionRef, ReconciliationState, RequestId, ResolvedScope, }; use tracedecay_domain::{ - ActorId, ComponentVersion, FactOwnerV1, ManifestDigest, ProjectId, RepositoryId, RunId, - UtcMicros, WorktreeId, canonical_sha256, + ActorId, ComponentVersion, FactOwnerV1, ProjectId, RepositoryId, RunId, UtcMicros, WorktreeId, + canonical_sha256, }; use tracedecay_tool_catalog::EffectClass; @@ -41,9 +41,7 @@ impl tracedecay_automation_runtime::automation::backend::AgentTaskBackend } } -fn digest(seed: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", seed.to_string().repeat(64))).expect("fixture digest") -} +use tracedecay_domain::test_fixtures::digest; fn scope() -> ResolvedScope { ResolvedScope::new( diff --git a/crates/tracedecay-daemon-service/src/invocation/configuration/settlement.rs b/crates/tracedecay-daemon-service/src/invocation/configuration/settlement.rs index 761dd972f3..097ac08ef1 100644 --- a/crates/tracedecay-daemon-service/src/invocation/configuration/settlement.rs +++ b/crates/tracedecay-daemon-service/src/invocation/configuration/settlement.rs @@ -418,9 +418,7 @@ mod tests { assert!(!requires_daemon_restart(&pending, &advanced).unwrap()); } - fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() - } + use tracedecay_domain::test_fixtures::digest; fn authority( scope: &ResolvedScope, diff --git a/crates/tracedecay-daemon-service/src/invocation/work_attempt_exec/tests.rs b/crates/tracedecay-daemon-service/src/invocation/work_attempt_exec/tests.rs index b26654acb7..89eb73864b 100644 --- a/crates/tracedecay-daemon-service/src/invocation/work_attempt_exec/tests.rs +++ b/crates/tracedecay-daemon-service/src/invocation/work_attempt_exec/tests.rs @@ -81,9 +81,7 @@ where T::try_from(value.to_owned()).unwrap() } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn sha256_digest(bytes: &[u8]) -> ManifestDigest { ManifestDigest::new(format!("sha256:{}", hex::encode(Sha256::digest(bytes)))).unwrap() diff --git a/crates/tracedecay-domain/src/external_source.rs b/crates/tracedecay-domain/src/external_source.rs index 0b9061d32e..1f414752d6 100644 --- a/crates/tracedecay-domain/src/external_source.rs +++ b/crates/tracedecay-domain/src/external_source.rs @@ -1689,9 +1689,7 @@ impl SourceSnapshotCompletionV1 { mod tests { use super::*; - fn digest(seed: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", seed.to_string().repeat(64))).unwrap() - } + use crate::test_fixtures::digest; fn binding_identity() -> SourceBindingIdentityV1 { SourceBindingIdentityV1 { diff --git a/crates/tracedecay-domain/src/feedback/mod.rs b/crates/tracedecay-domain/src/feedback/mod.rs index 665db6f0c8..7e3046d207 100644 --- a/crates/tracedecay-domain/src/feedback/mod.rs +++ b/crates/tracedecay-domain/src/feedback/mod.rs @@ -1587,9 +1587,7 @@ impl FeedbackCycleObservationV1 { mod tests { use super::*; - fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() - } + use crate::test_fixtures::digest; fn id(value: &str) -> T where diff --git a/crates/tracedecay-domain/src/lib.rs b/crates/tracedecay-domain/src/lib.rs index c4bcc4d8ba..26b7555bd2 100644 --- a/crates/tracedecay-domain/src/lib.rs +++ b/crates/tracedecay-domain/src/lib.rs @@ -26,6 +26,7 @@ pub mod retrieval; pub mod session; pub mod session_derived; pub mod source_path_policy; +pub mod test_fixtures; pub mod work; pub mod work_duplicate_adjudication; pub mod work_execution_snapshot; diff --git a/crates/tracedecay-domain/src/observability/workflow.rs b/crates/tracedecay-domain/src/observability/workflow.rs index 3d8379a808..931093c5b2 100644 --- a/crates/tracedecay-domain/src/observability/workflow.rs +++ b/crates/tracedecay-domain/src/observability/workflow.rs @@ -150,9 +150,7 @@ impl WorkflowResourceObservedV1 { mod tests { use super::*; - fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() - } + use crate::test_fixtures::digest; #[test] fn lifecycle_requires_exact_journal_coverage() { diff --git a/crates/tracedecay-domain/src/test_fixtures.rs b/crates/tracedecay-domain/src/test_fixtures.rs new file mode 100644 index 0000000000..f17f9fb09e --- /dev/null +++ b/crates/tracedecay-domain/src/test_fixtures.rs @@ -0,0 +1,39 @@ +//! Shared fixture constructors for tests in every crate that already depends +//! on this one. +//! +//! The values are not hashes of anything. Suites used to each re-roll the same +//! `sha256:` spelling. + +use crate::ManifestDigest; + +/// `sha256:` plus `digit` repeated 64 times. +/// +/// Not a hash. One lowercase hex digit parses as a manifest digest and stays +/// distinct from its siblings. +pub fn repeated_sha256_text(digit: char) -> String { + format!("sha256:{}", digit.to_string().repeat(64)) +} + +/// [`repeated_sha256_text`] parsed as a manifest digest. +/// +/// An illegal digit is a broken fixture, so this fails immediately rather than +/// handing each suite its own `unwrap`. +pub fn digest(digit: char) -> ManifestDigest { + ManifestDigest::new(repeated_sha256_text(digit)).expect("fixture digest is canonical") +} + +#[cfg(test)] +mod tests { + use super::{digest, repeated_sha256_text}; + + #[test] + fn repeated_hex_digest_matches_the_shared_spelling() { + let spelled = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + assert_eq!(repeated_sha256_text('a'), spelled); + assert_eq!(digest('a').as_str(), spelled); + assert_eq!( + repeated_sha256_text('0'), + crate::ManifestDigest::zero().unwrap().as_str() + ); + } +} diff --git a/crates/tracedecay-domain/tests/domain_suite/configuration_contract.rs b/crates/tracedecay-domain/tests/domain_suite/configuration_contract.rs index 9dac403d57..6d42bd27e4 100644 --- a/crates/tracedecay-domain/tests/domain_suite/configuration_contract.rs +++ b/crates/tracedecay-domain/tests/domain_suite/configuration_contract.rs @@ -9,7 +9,7 @@ use tracedecay_domain::configuration::{ resolve_restrictive_capabilities, }; use tracedecay_domain::{ - AccessPolicyDigest, ActorId, CapabilityId, LocatorDigest, ManifestDigest, ProjectId, UtcMicros, + AccessPolicyDigest, ActorId, CapabilityId, LocatorDigest, ProjectId, UtcMicros, }; fn id(value: &str) -> T @@ -20,10 +20,7 @@ where T::try_from(value.to_owned()).expect("fixture id is canonical") } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))) - .expect("fixture digest is canonical") -} +use tracedecay_domain::test_fixtures::digest; fn locator_digest(byte: char) -> LocatorDigest { LocatorDigest::new(format!("sha256:{}", byte.to_string().repeat(64))) diff --git a/crates/tracedecay-domain/tests/domain_suite/external_source_foundation_contract.rs b/crates/tracedecay-domain/tests/domain_suite/external_source_foundation_contract.rs index 43944c157a..9f87d0cedb 100644 --- a/crates/tracedecay-domain/tests/domain_suite/external_source_foundation_contract.rs +++ b/crates/tracedecay-domain/tests/domain_suite/external_source_foundation_contract.rs @@ -1,18 +1,16 @@ use std::collections::{BTreeMap, BTreeSet}; use tracedecay_domain::{ - LocatorDigest, ManifestDigest, PrivacyDomainId, ProjectId, ProviderId, - SourceAcquisitionCapabilitiesV1, SourceAcquisitionContractV1, SourceAggregateFrontierV1, - SourceBindingOwnerV1, SourceBindingV1, SourceCaptureModeV1, SourceContentStateV1, - SourceCoverageV1, SourceCursorV1, SourceDefinitionV1, SourceDeletionSemanticsV1, - SourceInstanceId, SourceNativeObjectIdV1, SourceObjectObservationV1, SourceObjectRevisionV1, - SourcePartitionFrontierV1, SourcePartitionIdV1, SourceRefetchStrategyV1, SourceSnapshotIdV1, - UserProfileId, canonical_sha256, + LocatorDigest, PrivacyDomainId, ProjectId, ProviderId, SourceAcquisitionCapabilitiesV1, + SourceAcquisitionContractV1, SourceAggregateFrontierV1, SourceBindingOwnerV1, SourceBindingV1, + SourceCaptureModeV1, SourceContentStateV1, SourceCoverageV1, SourceCursorV1, + SourceDefinitionV1, SourceDeletionSemanticsV1, SourceInstanceId, SourceNativeObjectIdV1, + SourceObjectObservationV1, SourceObjectRevisionV1, SourcePartitionFrontierV1, + SourcePartitionIdV1, SourceRefetchStrategyV1, SourceSnapshotIdV1, UserProfileId, + canonical_sha256, }; -fn digest(seed: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", seed.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn definition() -> SourceDefinitionV1 { let capabilities = SourceAcquisitionCapabilitiesV1::new( diff --git a/crates/tracedecay-domain/tests/domain_suite/feedback_contract.rs b/crates/tracedecay-domain/tests/domain_suite/feedback_contract.rs index f544893147..be31df8798 100644 --- a/crates/tracedecay-domain/tests/domain_suite/feedback_contract.rs +++ b/crates/tracedecay-domain/tests/domain_suite/feedback_contract.rs @@ -8,8 +8,8 @@ use tracedecay_domain::feedback::{ FeedbackTargetV1, FeedbackTriggerV1, ProviderEvaluationStateV1, }; use tracedecay_domain::{ - AgentInstanceId, CodeGenerationId, CommitId, FileOccurrenceId, HostInstanceId, ManifestDigest, - ProjectId, RepositoryId, RetrievalAnchorId, SessionId, UtcMicros, WorktreeId, + AgentInstanceId, CodeGenerationId, CommitId, FileOccurrenceId, HostInstanceId, ProjectId, + RepositoryId, RetrievalAnchorId, SessionId, UtcMicros, WorktreeId, }; fn id(value: &str) -> T @@ -20,10 +20,7 @@ where T::try_from(value.to_owned()).expect("fixture id is canonical") } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))) - .expect("fixture digest is canonical") -} +use tracedecay_domain::test_fixtures::digest; fn scope() -> FeedbackScopeV1 { FeedbackScopeV1 { diff --git a/crates/tracedecay-domain/tests/domain_suite/git_index_transaction_contract.rs b/crates/tracedecay-domain/tests/domain_suite/git_index_transaction_contract.rs index 8b18ea2f4f..09a99a678d 100644 --- a/crates/tracedecay-domain/tests/domain_suite/git_index_transaction_contract.rs +++ b/crates/tracedecay-domain/tests/domain_suite/git_index_transaction_contract.rs @@ -25,10 +25,7 @@ fn oid(byte: char) -> GitOidV1 { GitOidV1::new(byte.to_string().repeat(40)).expect("fixture oid is canonical") } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))) - .expect("fixture digest is canonical") -} +use tracedecay_domain::test_fixtures::digest; #[test] fn receipt_outcome_schema_preserves_the_exact_wire_states() { diff --git a/crates/tracedecay-domain/tests/domain_suite/git_topology_anchor_contract.rs b/crates/tracedecay-domain/tests/domain_suite/git_topology_anchor_contract.rs index 4cdd31829f..d50ca533e6 100644 --- a/crates/tracedecay-domain/tests/domain_suite/git_topology_anchor_contract.rs +++ b/crates/tracedecay-domain/tests/domain_suite/git_topology_anchor_contract.rs @@ -15,17 +15,16 @@ use tracedecay_domain::{ GitIndexTransactionId, GitIndexTransactionOperationV1, GitIndexTransactionReceiptV1, GitObjectFormatV1, GitOidV1, GitOperationStateV1, GitTopologyAnchorTargetV1, GitTopologyGenerationRefV1, GitTopologySourceRoleV1, IntegrationReceiptAnchorRefV1, - ManifestDigest, NativeGitObjectAnchorRefV1, NativeGitObjectKindV1, ObservationScopeV1, - PayloadAccessState, PreflightPreviewAnchorRefV1, PrivacyDomainBoundLocatorDigest, - PrivacyDomainId, ProjectId, ProjectionGenerationId, PullRequestSnapshotAnchorRefV1, RefId, - RefSnapshotAnchorRefV1, RefSnapshotKindV1, RepositoryCaptureAnchorRefV1, - RepositoryDirtyStateV1, RepositoryEvidenceV1, RepositoryId, RepositoryIndexSnapshotV1, - RepositoryIndexStateV1, RepositoryProvenanceV1, RepositoryRemoteIdentityV1, - RepositoryStateSnapshotV1, RepositoryWorkingTreeSnapshotV1, RepositoryWorkingTreeStateV1, - ResolutionAuthorizationV1, RetentionClass, RetrievalAnchorRecordV2, - RetrievalAnchorRecordV2Parts, RetrievalAnchorTargetV2, ScopeResolutionId, ShardId, UtcMicros, - VectorWatermark, WorktreeCaptureAnchorRefV1, WorktreeId, canonical_sha256, - derive_git_topology_anchor_id, + NativeGitObjectAnchorRefV1, NativeGitObjectKindV1, ObservationScopeV1, PayloadAccessState, + PreflightPreviewAnchorRefV1, PrivacyDomainBoundLocatorDigest, PrivacyDomainId, ProjectId, + ProjectionGenerationId, PullRequestSnapshotAnchorRefV1, RefId, RefSnapshotAnchorRefV1, + RefSnapshotKindV1, RepositoryCaptureAnchorRefV1, RepositoryDirtyStateV1, RepositoryEvidenceV1, + RepositoryId, RepositoryIndexSnapshotV1, RepositoryIndexStateV1, RepositoryProvenanceV1, + RepositoryRemoteIdentityV1, RepositoryStateSnapshotV1, RepositoryWorkingTreeSnapshotV1, + RepositoryWorkingTreeStateV1, ResolutionAuthorizationV1, RetentionClass, + RetrievalAnchorRecordV2, RetrievalAnchorRecordV2Parts, RetrievalAnchorTargetV2, + ScopeResolutionId, ShardId, UtcMicros, VectorWatermark, WorktreeCaptureAnchorRefV1, WorktreeId, + canonical_sha256, derive_git_topology_anchor_id, }; fn id(value: &str) -> T @@ -40,10 +39,7 @@ fn oid(byte: char) -> GitOidV1 { GitOidV1::new(byte.to_string().repeat(40)).expect("fixture oid is canonical") } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))) - .expect("fixture digest is canonical") -} +use tracedecay_domain::test_fixtures::digest; fn snapshot(epoch: u64, head: char) -> RepositoryStateSnapshotV1 { RepositoryStateSnapshotV1::new( diff --git a/crates/tracedecay-domain/tests/domain_suite/multi_root_contract.rs b/crates/tracedecay-domain/tests/domain_suite/multi_root_contract.rs index 53c5a5362e..a04b0773f1 100644 --- a/crates/tracedecay-domain/tests/domain_suite/multi_root_contract.rs +++ b/crates/tracedecay-domain/tests/domain_suite/multi_root_contract.rs @@ -1,12 +1,10 @@ use schemars::schema_for; use tracedecay_domain::{ - CollectionRevision, ManifestDigest, RootGenerationV1, ScopeOutcome, ScopePartialReasonV1, - ScopeSetId, ScopeSetRevision, ScopeUnavailableReasonV1, StackRevision, + CollectionRevision, RootGenerationV1, ScopeOutcome, ScopePartialReasonV1, ScopeSetId, + ScopeSetRevision, ScopeUnavailableReasonV1, StackRevision, }; -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; #[test] fn scope_set_and_root_revision_identities_are_typed_and_nonzero() { diff --git a/crates/tracedecay-domain/tests/domain_suite/repository_state_contract.rs b/crates/tracedecay-domain/tests/domain_suite/repository_state_contract.rs index 7abc741946..04ef1fd777 100644 --- a/crates/tracedecay-domain/tests/domain_suite/repository_state_contract.rs +++ b/crates/tracedecay-domain/tests/domain_suite/repository_state_contract.rs @@ -3,8 +3,8 @@ use tracedecay_domain::git::repository_state::{ RepositoryWorkingTreeSnapshotV1, RepositoryWorkingTreeStateV1, }; use tracedecay_domain::{ - GitCoverageV1, GitHeadStateV1, GitObjectFormatV1, GitOidV1, GitOperationStateV1, - ManifestDigest, ProjectId, RepositoryId, UtcMicros, WorktreeId, + GitCoverageV1, GitHeadStateV1, GitObjectFormatV1, GitOidV1, GitOperationStateV1, ProjectId, + RepositoryId, UtcMicros, WorktreeId, }; fn id(value: &str) -> T @@ -19,10 +19,7 @@ fn oid(byte: char) -> GitOidV1 { GitOidV1::new(byte.to_string().repeat(40)).expect("fixture oid is canonical") } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))) - .expect("fixture digest is canonical") -} +use tracedecay_domain::test_fixtures::digest; fn snapshot(head: GitOidV1) -> RepositoryStateSnapshotV1 { RepositoryStateSnapshotV1::new( diff --git a/crates/tracedecay-domain/tests/domain_suite/work_execution_snapshot_contract.rs b/crates/tracedecay-domain/tests/domain_suite/work_execution_snapshot_contract.rs index a845d8a3c4..d7a43a4847 100644 --- a/crates/tracedecay-domain/tests/domain_suite/work_execution_snapshot_contract.rs +++ b/crates/tracedecay-domain/tests/domain_suite/work_execution_snapshot_contract.rs @@ -2,12 +2,11 @@ use std::collections::BTreeSet; use tracedecay_domain::{ AutomaticWorktreeGcV1, ConfigurationRevisionId, ConfigurationSnapshotId, CredentialReferenceId, - CrossMergeModeV1, ManifestDigest, ProviderId, TopologyNotificationLevelV1, UtcMicros, - WorkApprovalPolicy, WorkEgressPolicy, WorkExecutableReference, WorkExecutionLimits, - WorkExecutionSnapshot, WorkExecutionSnapshotInput, WorkFallbackTopology, WorkFilesystemPolicy, - WorkProviderBackendV1, WorkProviderProtocol, WorkProviderRouteId, WorkProviderRouteV1, - WorkRuntimeContractError, WorkSandboxPolicy, WorktreeCleanlinessRequirementV1, - safe_work_topology_policy_v1, + CrossMergeModeV1, ProviderId, TopologyNotificationLevelV1, UtcMicros, WorkApprovalPolicy, + WorkEgressPolicy, WorkExecutableReference, WorkExecutionLimits, WorkExecutionSnapshot, + WorkExecutionSnapshotInput, WorkFallbackTopology, WorkFilesystemPolicy, WorkProviderBackendV1, + WorkProviderProtocol, WorkProviderRouteId, WorkProviderRouteV1, WorkRuntimeContractError, + WorkSandboxPolicy, WorktreeCleanlinessRequirementV1, safe_work_topology_policy_v1, }; fn id(value: &str) -> T @@ -18,9 +17,7 @@ where T::try_from(value.to_owned()).unwrap() } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn route(provider: &str, route: &str) -> WorkProviderRouteV1 { WorkProviderRouteV1::new(id::(provider), id::(route)).unwrap() diff --git a/crates/tracedecay-domain/tests/domain_suite/work_product_contract.rs b/crates/tracedecay-domain/tests/domain_suite/work_product_contract.rs index 2f4d69b4d9..2904432905 100644 --- a/crates/tracedecay-domain/tests/domain_suite/work_product_contract.rs +++ b/crates/tracedecay-domain/tests/domain_suite/work_product_contract.rs @@ -2,20 +2,20 @@ use std::collections::{BTreeMap, BTreeSet}; use tracedecay_domain::{ AcceptanceCriterionId, AttemptId, InitiativeId, MAX_WORK_PRODUCT_EVENT_EVIDENCE, - MAX_WORK_PRODUCT_EVENT_RELATION_SCOPES, MAX_WORK_PRODUCT_EVENT_SOURCE_WATERMARKS, - ManifestDigest, MilestoneId, ProjectionGenerationId, ProposalId, RetrievalAnchorId, RunId, - SourceStoreId, TaskEvidenceLinkId, TaskEvidenceLinkV1, TaskId, UtcMicros, - WorkAcceptanceCriterionV1, WorkAttemptIdentityV1, WorkAttemptStateV1, WorkGraphChangeV1, - WorkGraphVersionV1, WorkHierarchyV1, WorkInitiativeV1, WorkItemInputV1, WorkItemV1, WorkPlanId, - WorkPlanV1, WorkProductAuthorizedRelationScopeV1, WorkProductContractError, - WorkProductEventContractError, WorkProductEventEvidenceV1, WorkProductEventInputV1, - WorkProductEventPayloadV1, WorkProductEventSequenceV1, WorkProductEventV1, WorkProductGraphV1, - WorkProductProfileScopeV1, WorkProductProjectionBundleV1, WorkProductRelationV1, - WorkProductSourceWatermarkV1, WorkProjectionSequenceV1, WorkProposalDispositionV1, - WorkProposalV1, WorkProposedChildV1, WorkRelationReplanProposalV1, WorkRouteDecisionV1, - WorkRuntimeAttemptProjectionV1, WorkRuntimeProjectionCoverageV1, WorkRuntimeProjectionV1, - WorkScoreKindV1, WorkShapeAssessmentV1, WorkSizingV1, WorkTaskEvidenceCoverageV1, - WorkTaskEvidenceV1, WorkTimelineLaneV1, canonical_json_bytes, + MAX_WORK_PRODUCT_EVENT_RELATION_SCOPES, MAX_WORK_PRODUCT_EVENT_SOURCE_WATERMARKS, MilestoneId, + ProjectionGenerationId, ProposalId, RetrievalAnchorId, RunId, SourceStoreId, + TaskEvidenceLinkId, TaskEvidenceLinkV1, TaskId, UtcMicros, WorkAcceptanceCriterionV1, + WorkAttemptIdentityV1, WorkAttemptStateV1, WorkGraphChangeV1, WorkGraphVersionV1, + WorkHierarchyV1, WorkInitiativeV1, WorkItemInputV1, WorkItemV1, WorkPlanId, WorkPlanV1, + WorkProductAuthorizedRelationScopeV1, WorkProductContractError, WorkProductEventContractError, + WorkProductEventEvidenceV1, WorkProductEventInputV1, WorkProductEventPayloadV1, + WorkProductEventSequenceV1, WorkProductEventV1, WorkProductGraphV1, WorkProductProfileScopeV1, + WorkProductProjectionBundleV1, WorkProductRelationV1, WorkProductSourceWatermarkV1, + WorkProjectionSequenceV1, WorkProposalDispositionV1, WorkProposalV1, WorkProposedChildV1, + WorkRelationReplanProposalV1, WorkRouteDecisionV1, WorkRuntimeAttemptProjectionV1, + WorkRuntimeProjectionCoverageV1, WorkRuntimeProjectionV1, WorkScoreKindV1, + WorkShapeAssessmentV1, WorkSizingV1, WorkTaskEvidenceCoverageV1, WorkTaskEvidenceV1, + WorkTimelineLaneV1, canonical_json_bytes, }; fn id(value: &str) -> T @@ -26,9 +26,7 @@ where T::try_from(value.to_owned()).unwrap() } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn hierarchy() -> WorkHierarchyV1 { WorkHierarchyV1::new( diff --git a/crates/tracedecay-domain/tests/domain_suite/work_runtime_contract.rs b/crates/tracedecay-domain/tests/domain_suite/work_runtime_contract.rs index 10e01902e8..ab0658b2b3 100644 --- a/crates/tracedecay-domain/tests/domain_suite/work_runtime_contract.rs +++ b/crates/tracedecay-domain/tests/domain_suite/work_runtime_contract.rs @@ -2,13 +2,13 @@ use std::collections::{BTreeMap, BTreeSet}; use serde_json::json; use tracedecay_domain::{ - AttemptId, CommitId, ConfigurationRevisionId, ConfigurationSnapshotId, ManifestDigest, - ProjectId, ProposalId, ProviderId, RefId, RepositoryId, RunId, SourceStoreId, TaskId, - UtcMicros, WorkApprovalPolicy, WorkArtifactId, WorkArtifactRefV1, WorkAttemptIdentityV1, - WorkAttemptProjectionBindingV1, WorkAttemptStateV1, WorkAttemptV1, - WorkCancellationAcknowledgementV1, WorkCancellationEscalationV1, WorkCancellationRequestId, - WorkCancellationRequestV1, WorkCancellationStateV1, WorkEffectStateV1, WorkEgressPolicy, - WorkExecutableReference, WorkExecutionEnvelopeV1, WorkExecutionLimits, WorkExecutionSnapshot, + AttemptId, CommitId, ConfigurationRevisionId, ConfigurationSnapshotId, ProjectId, ProposalId, + ProviderId, RefId, RepositoryId, RunId, SourceStoreId, TaskId, UtcMicros, WorkApprovalPolicy, + WorkArtifactId, WorkArtifactRefV1, WorkAttemptIdentityV1, WorkAttemptProjectionBindingV1, + WorkAttemptStateV1, WorkAttemptV1, WorkCancellationAcknowledgementV1, + WorkCancellationEscalationV1, WorkCancellationRequestId, WorkCancellationRequestV1, + WorkCancellationStateV1, WorkEffectStateV1, WorkEgressPolicy, WorkExecutableReference, + WorkExecutionEnvelopeV1, WorkExecutionLimits, WorkExecutionSnapshot, WorkExecutionSnapshotInput, WorkFallbackTopology, WorkFenceEpochV1, WorkFilesystemPolicy, WorkGraphChangeV1, WorkGraphVersionV1, WorkHierarchyV1, WorkInitiativeV1, WorkItemInputV1, WorkItemV1, WorkLeaseFenceV1, WorkLeaseId, WorkMilestoneV1, WorkPlanId, WorkPlanV1, @@ -27,9 +27,7 @@ where T::try_from(value.to_owned()).unwrap() } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn route(provider: &str, route: &str) -> WorkProviderRouteV1 { WorkProviderRouteV1::new(id::(provider), id::(route)).unwrap() diff --git a/crates/tracedecay-domain/tests/domain_suite/workflow_definition_contract.rs b/crates/tracedecay-domain/tests/domain_suite/workflow_definition_contract.rs index b682fe8970..6aea6e511c 100644 --- a/crates/tracedecay-domain/tests/domain_suite/workflow_definition_contract.rs +++ b/crates/tracedecay-domain/tests/domain_suite/workflow_definition_contract.rs @@ -4,14 +4,14 @@ use serde_json::json; use tracedecay_domain::configuration::safe_work_topology_policy_v1; use tracedecay_domain::{ AttemptId, MAX_WORKFLOW_FAN_OUT, MAX_WORKFLOW_INPUTS, MAX_WORKFLOW_OUTPUTS, - MAX_WORKFLOW_PREDECESSORS, MAX_WORKFLOW_STEPS, ManifestDigest, ProjectId, ProviderId, RunId, - TaskId, UtcMicros, WorkArtifactId, WorkArtifactRefV1, WorkAttemptIdentityV1, WorkCommandId, - WorkProviderBackendV1, WorkProviderRouteId, WorkProviderRouteV1, WorkflowDefinition, - WorkflowDefinitionError, WorkflowDefinitionId, WorkflowFanOut, WorkflowOperationRef, - WorkflowOutputArtifact, WorkflowOutputName, WorkflowOutputReference, WorkflowPlacementReceipt, - WorkflowRunCommand, WorkflowRunEvent, WorkflowRunEventContext, WorkflowRunProjection, - WorkflowRunStateError, WorkflowRunStatus, WorkflowStep, WorkflowStepEffectOutcome, - WorkflowStepEffectReceipt, WorkflowStepId, WorkflowStepOutput, WorkflowStepStatus, + MAX_WORKFLOW_PREDECESSORS, MAX_WORKFLOW_STEPS, ProjectId, ProviderId, RunId, TaskId, UtcMicros, + WorkArtifactId, WorkArtifactRefV1, WorkAttemptIdentityV1, WorkCommandId, WorkProviderBackendV1, + WorkProviderRouteId, WorkProviderRouteV1, WorkflowDefinition, WorkflowDefinitionError, + WorkflowDefinitionId, WorkflowFanOut, WorkflowOperationRef, WorkflowOutputArtifact, + WorkflowOutputName, WorkflowOutputReference, WorkflowPlacementReceipt, WorkflowRunCommand, + WorkflowRunEvent, WorkflowRunEventContext, WorkflowRunProjection, WorkflowRunStateError, + WorkflowRunStatus, WorkflowStep, WorkflowStepEffectOutcome, WorkflowStepEffectReceipt, + WorkflowStepId, WorkflowStepOutput, WorkflowStepStatus, }; fn id(value: &str) -> T @@ -22,9 +22,7 @@ where T::try_from(value.to_owned()).unwrap() } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn step( step_id: &str, diff --git a/crates/tracedecay-global-db/src/configuration/store/tests/mod.rs b/crates/tracedecay-global-db/src/configuration/store/tests/mod.rs index 7ac673bd55..6674ebc6bb 100644 --- a/crates/tracedecay-global-db/src/configuration/store/tests/mod.rs +++ b/crates/tracedecay-global-db/src/configuration/store/tests/mod.rs @@ -7,7 +7,7 @@ use super::{ ConfigurationMutationAuthority, ConfigurationMutationReceiptV1, ConfigurationProtectedOperationV1, ConfigurationProtectedPlanRecordV1, ConfigurationRevisionId, ConfigurationRevisionRecordV1, ConfigurationSnapshotV1, ConfigurationValueV1, - GlobalDbConfigurationControlStore, ManifestDigest, TestConnection, + GlobalDbConfigurationControlStore, TestConnection, }; use crate::configuration::contracts::ScopeRevalidationEvidenceV1; use crate::configuration::registry::ConfigurationRegistry; @@ -27,9 +27,7 @@ use tracedecay_domain::{ AccessPolicyDigest, ActorId, LocatorDigest, ProjectId, UtcMicros, canonical_sha256, }; -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; #[test] fn incomplete_snapshot_requires_reset_instead_of_default_repair() { diff --git a/crates/tracedecay-global-db/src/git_index_transactions/tests.rs b/crates/tracedecay-global-db/src/git_index_transactions/tests.rs index 867168a82b..e0f64bcdb1 100644 --- a/crates/tracedecay-global-db/src/git_index_transactions/tests.rs +++ b/crates/tracedecay-global-db/src/git_index_transactions/tests.rs @@ -4,9 +4,9 @@ use tracedecay_domain::{ GitIndexPreviewId, GitIndexPreviewInputV1, GitIndexPreviewV1, GitIndexReceiptId, GitIndexReceiptOutcomeV1, GitIndexSigningPolicyV1, GitIndexTransactionId, GitIndexTransactionJournalV1, GitIndexTransactionOperationV1, GitIndexTransactionReceiptV1, - GitObjectFormatV1, GitOidV1, ManifestDigest, ProjectId, RepositoryId, - RepositoryIndexSnapshotV1, RepositoryIndexStateV1, RepositoryStateSnapshotV1, - RepositoryWorkingTreeSnapshotV1, RepositoryWorkingTreeStateV1, UtcMicros, WorktreeId, + GitObjectFormatV1, GitOidV1, ProjectId, RepositoryId, RepositoryIndexSnapshotV1, + RepositoryIndexStateV1, RepositoryStateSnapshotV1, RepositoryWorkingTreeSnapshotV1, + RepositoryWorkingTreeStateV1, UtcMicros, WorktreeId, }; use tracedecay_runtime_core::db::engine::params; use tracedecay_store::{ @@ -60,9 +60,7 @@ where T::try_from(value.to_owned()).expect("fixture identity") } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).expect("fixture digest") -} +use tracedecay_domain::test_fixtures::digest; fn oid(byte: char) -> GitOidV1 { GitOidV1::new(byte.to_string().repeat(40)).expect("fixture object id") diff --git a/crates/tracedecay-global-db/src/native_integration/tests.rs b/crates/tracedecay-global-db/src/native_integration/tests.rs index 70bc768ec7..b9b8dd4379 100644 --- a/crates/tracedecay-global-db/src/native_integration/tests.rs +++ b/crates/tracedecay-global-db/src/native_integration/tests.rs @@ -1,11 +1,11 @@ use tracedecay_domain::{ ActorId, CapabilityId, CodeGenerationId, ContentDigest, FrozenIndependentBranchSelectionV1, - GitHeadStateV1, GitObjectFormatV1, GitOidV1, GitOperationStateV1, ManifestDigest, - MechanicalIntegrationModeV1, NativeIntegrationAnalysisCoverageV1, - NativeIntegrationAnalysisLaneV1, NativeIntegrationAnalysisReportV1, - NativeIntegrationApprovalId, NativeIntegrationApprovalV1, NativeIntegrationGenerationBindingV1, - NativeIntegrationPhaseV1, NativeIntegrationPreviewDispositionV1, NativeIntegrationPreviewId, - NativeIntegrationPreviewV1, NativeIntegrationReceiptV1, NativeIntegrationRepositorySnapshotV1, + GitHeadStateV1, GitObjectFormatV1, GitOidV1, GitOperationStateV1, MechanicalIntegrationModeV1, + NativeIntegrationAnalysisCoverageV1, NativeIntegrationAnalysisLaneV1, + NativeIntegrationAnalysisReportV1, NativeIntegrationApprovalId, NativeIntegrationApprovalV1, + NativeIntegrationGenerationBindingV1, NativeIntegrationPhaseV1, + NativeIntegrationPreviewDispositionV1, NativeIntegrationPreviewId, NativeIntegrationPreviewV1, + NativeIntegrationReceiptV1, NativeIntegrationRepositorySnapshotV1, NativeIntegrationSelectionV1, NativeIntegrationTerminalOutcomeV1, NativeIntegrationTransactionId, NativeIntegrationTransactionStatusV1, ProjectId, RefId, RepositoryId, UtcMicros, WorktreeInventoryEpoch, WorktreeInventorySnapshotId, @@ -17,9 +17,7 @@ use tracedecay_store::{ use super::store::GlobalDbNativeIntegrationStore; use crate::{RegisteredGlobalDb, tests::harness::RegisteredGlobalDbHarness}; -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).expect("fixture digest") -} +use tracedecay_domain::test_fixtures::digest; fn oid(byte: char) -> GitOidV1 { GitOidV1::new(byte.to_string().repeat(40)).expect("fixture object id") diff --git a/crates/tracedecay-lsp/src/overlay/retention.rs b/crates/tracedecay-lsp/src/overlay/retention.rs index a76af17565..de4608d5b6 100644 --- a/crates/tracedecay-lsp/src/overlay/retention.rs +++ b/crates/tracedecay-lsp/src/overlay/retention.rs @@ -33,14 +33,10 @@ impl OverlayDiagnosticDebouncer { #[cfg(test)] mod tests { - use tracedecay_domain::ManifestDigest; - use super::*; use crate::session::AuthorizedLspWorkspace; - fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() - } + use tracedecay_domain::test_fixtures::digest; #[test] fn nested_workspace_overlay_belongs_only_to_the_deepest_root() { diff --git a/crates/tracedecay-lsp/src/protocol/tests/workspace_diagnostics.rs b/crates/tracedecay-lsp/src/protocol/tests/workspace_diagnostics.rs index 934d60d393..2750bb485e 100644 --- a/crates/tracedecay-lsp/src/protocol/tests/workspace_diagnostics.rs +++ b/crates/tracedecay-lsp/src/protocol/tests/workspace_diagnostics.rs @@ -1,9 +1,7 @@ use super::*; use std::sync::atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering}; -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn initialize_workspace( roots: Vec, diff --git a/crates/tracedecay-mcp/src/handlers/hook_runtime/test_support.rs b/crates/tracedecay-mcp/src/handlers/hook_runtime/test_support.rs index 4ebc1b9c7d..43a43c5fdf 100644 --- a/crates/tracedecay-mcp/src/handlers/hook_runtime/test_support.rs +++ b/crates/tracedecay-mcp/src/handlers/hook_runtime/test_support.rs @@ -15,8 +15,7 @@ use tracedecay_contracts::{ }; use tracedecay_domain::feedback::{FeedbackContentIdentityV1, FeedbackScopeV1}; use tracedecay_domain::{ - CodeGenerationId, ComponentVersion, ManifestDigest, RefId, RetrievalAnchorId, TemporalModeV1, - UtcMicros, + CodeGenerationId, ComponentVersion, RefId, RetrievalAnchorId, TemporalModeV1, UtcMicros, }; pub(super) fn admission_test_envelope( @@ -75,9 +74,7 @@ pub(super) fn retained_claim(id: u8) -> ContextScoutDurableClaimV1 { T::try_from(value.to_owned()).unwrap() } - fn digest(character: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", character.to_string().repeat(64))).unwrap() - } + use tracedecay_domain::test_fixtures::digest; fn evidence(id: u8) -> ContextScoutEvidenceEnvelopeV1 { let scope = ResolvedScope::new( diff --git a/crates/tracedecay-policy/src/configuration.rs b/crates/tracedecay-policy/src/configuration.rs index 6b65aea1de..2d7676f637 100644 --- a/crates/tracedecay-policy/src/configuration.rs +++ b/crates/tracedecay-policy/src/configuration.rs @@ -164,9 +164,7 @@ mod tests { T::try_from(value.to_owned()).unwrap() } - fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() - } + use tracedecay_domain::test_fixtures::digest; fn policy_digest(byte: char) -> AccessPolicyDigest { AccessPolicyDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() diff --git a/crates/tracedecay-policy/tests/policy_suite/curation_apply.rs b/crates/tracedecay-policy/tests/policy_suite/curation_apply.rs index ee8d86c5e6..759c2b925c 100644 --- a/crates/tracedecay-policy/tests/policy_suite/curation_apply.rs +++ b/crates/tracedecay-policy/tests/policy_suite/curation_apply.rs @@ -5,9 +5,7 @@ use tracedecay_policy::{ CurationApplySubjectV1, CurationValidationDispositionV1, evaluate_curation_apply, }; -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).expect("digest") -} +use tracedecay_domain::test_fixtures::digest; fn input( subject: CurationApplySubjectV1, diff --git a/crates/tracedecay-policy/tests/policy_suite/routing_admission.rs b/crates/tracedecay-policy/tests/policy_suite/routing_admission.rs index db50d8a972..d43d3d4695 100644 --- a/crates/tracedecay-policy/tests/policy_suite/routing_admission.rs +++ b/crates/tracedecay-policy/tests/policy_suite/routing_admission.rs @@ -5,7 +5,7 @@ use tracedecay_domain::configuration::{ AnalyzerLanguageSelectionV1, AnalyzerPrivacyClassV1, AnalyzerResourceLimitsV1, AnalyzerRestartPolicyV1, AnalyzerSettingsV1, }; -use tracedecay_domain::{CapabilityId, ManifestDigest, UtcMicros}; +use tracedecay_domain::{CapabilityId, UtcMicros}; use tracedecay_policy::analyzer::{ AnalyzerAdmissionDispositionV1, AnalyzerAdmissionEvaluator, AnalyzerAdmissionEvaluatorV1, AnalyzerAdmissionInputV1, AnalyzerAdmissionSnapshotV1, AnalyzerAvailabilityV1, @@ -34,10 +34,7 @@ where T::try_from(value.to_owned()).expect("valid fixture identifier") } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))) - .expect("valid fixture digest") -} +use tracedecay_domain::test_fixtures::digest; fn analyzer_settings() -> AnalyzerSettingsV1 { AnalyzerSettingsV1 { diff --git a/crates/tracedecay-policy/tests/policy_suite/work_planner.rs b/crates/tracedecay-policy/tests/policy_suite/work_planner.rs index 7797c3e662..94f5e38e62 100644 --- a/crates/tracedecay-policy/tests/policy_suite/work_planner.rs +++ b/crates/tracedecay-policy/tests/policy_suite/work_planner.rs @@ -6,7 +6,7 @@ //! dimensions into a score, or lets a human override outrank an exclusion. use tracedecay_domain::{ - ManifestDigest, TaskId, UtcMicros, WorkApprovalPolicy, WorkEgressPolicy, WorkExecutionLimits, + TaskId, UtcMicros, WorkApprovalPolicy, WorkEgressPolicy, WorkExecutionLimits, WorkFallbackTopology, WorkFilesystemPolicy, WorkRouteExecutionProfileV1, WorkSandboxPolicy, }; use tracedecay_policy::work_loop::{ @@ -21,10 +21,7 @@ use tracedecay_policy::work_loop::{ const LOCAL_WATERMARK: i64 = 10; const EVALUATED_AT: i64 = 100; -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))) - .expect("fixture digest is canonical") -} +use tracedecay_domain::test_fixtures::digest; fn base_input() -> WorkProposalPolicyInputV1 { WorkProposalPolicyInputV1 { diff --git a/crates/tracedecay-rusqlite-runtime/src/repository/external_source/tests.rs b/crates/tracedecay-rusqlite-runtime/src/repository/external_source/tests.rs index 5a458fb972..f503561957 100644 --- a/crates/tracedecay-rusqlite-runtime/src/repository/external_source/tests.rs +++ b/crates/tracedecay-rusqlite-runtime/src/repository/external_source/tests.rs @@ -8,7 +8,7 @@ use tracedecay_domain::feedback::{ FeedbackScopeV1, GitHubPullRequestIdV1, GitHubReviewReadOperationV1, }; use tracedecay_domain::{ - AccessPolicyDigest, CapabilityId, CommitId, ComponentVersion, LocatorDigest, ManifestDigest, + AccessPolicyDigest, CapabilityId, CommitId, ComponentVersion, LocatorDigest, PrivacyDomainBoundLocatorDigest, PrivacyDomainId, ProjectId, ProviderId, RepositoryId, ResolutionAuthorizationV1, RetrievalAnchorId, SanitizationReceiptId, SanitizationReceiptRefV1, ScopeResolutionId, SourceAcquisitionCapabilitiesV1, SourceAcquisitionContractV1, @@ -29,9 +29,7 @@ use tracedecay_store::{ use super::*; -fn digest(seed: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", seed.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn fixture() -> (SourceCommitV1, SourceBindingIdentityV1) { let definition = SourceDefinitionV1::new( diff --git a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/handoff_open_storage.rs b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/handoff_open_storage.rs index a9d5be7c6b..e198bfc273 100644 --- a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/handoff_open_storage.rs +++ b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/handoff_open_storage.rs @@ -13,7 +13,7 @@ use tracedecay_contracts::{ RequestId, ResolvedScope, TaskHandoffTokenStateV1, }; use tracedecay_domain::{ - ActorId, ManifestDigest, ProjectId, RepositoryId, TaskId, UtcMicros, WorkVersion, WorktreeId, + ActorId, ProjectId, RepositoryId, TaskId, UtcMicros, WorkVersion, WorktreeId, }; use tracedecay_rusqlite_runtime::handoff::HandoffOpenSqliteAuthority; use tracedecay_tool_catalog::{CapabilityId, UseCaseId}; @@ -45,9 +45,7 @@ where T::try_from(value.to_owned()).unwrap() } -fn digest(fill: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", fill.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn context(request_id: &str) -> RequestContext { context_for_actor(request_id, "actor.handoff.runtime-store") diff --git a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/multi_root_scope_set.rs b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/multi_root_scope_set.rs index ffeb72fddd..ad1c63423e 100644 --- a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/multi_root_scope_set.rs +++ b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/multi_root_scope_set.rs @@ -10,8 +10,8 @@ use tracedecay_contracts::{ RequestId, ResolvedScope, SharedProfileStoreLocatorV1, }; use tracedecay_domain::{ - ActorId, BrainId, LocatorDigest, ManifestDigest, ProjectId, RefId, RepositoryId, ScopeSetId, - ScopeSetRevision, UserProfileId, UtcMicros, WorktreeId, + ActorId, BrainId, LocatorDigest, ProjectId, RefId, RepositoryId, ScopeSetId, ScopeSetRevision, + UserProfileId, UtcMicros, WorktreeId, }; use tracedecay_rusqlite_runtime::exact_sql::ExactSqlHandle; use tracedecay_rusqlite_runtime::reader::{ExistingReaderLocator, ReaderPool, ReaderQueryExecutor}; @@ -147,9 +147,7 @@ where T::try_from(value.to_owned()).unwrap() } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn context_for_actor(worktree: &str, suffix: &str, actor: &str) -> RequestContext { let scope = ResolvedScope::new( diff --git a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_attempt_storage.rs b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_attempt_storage.rs index d7dc2fae5b..871116014d 100644 --- a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_attempt_storage.rs +++ b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_attempt_storage.rs @@ -27,7 +27,7 @@ use tracedecay_contracts::{ }; use tracedecay_domain::configuration::TopologyConcurrencyPolicyV1; use tracedecay_domain::{ - ActorId, AttemptId, CommitId, ConfigurationRevisionId, ConfigurationSnapshotId, ManifestDigest, + ActorId, AttemptId, CommitId, ConfigurationRevisionId, ConfigurationSnapshotId, ObservationSourceIdentityV1, ProjectId, ProposalId, ProviderId, RefId, RepositoryId, RunId, SessionId, TaskId, UtcMicros, WorkApprovalPolicy, WorkArtifactRefV1, WorkAttemptIdentityV1, WorkAttemptProjectionBindingV1, WorkAttemptStateV1, WorkAttemptV1, WorkAuthority, @@ -53,9 +53,7 @@ where T::try_from(value.to_owned()).unwrap() } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn authority(actor: &str) -> WorkAuthority { authority_in_worktree(actor, "worktree.attempt.storage") diff --git a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_duplicate_adjudication_storage.rs b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_duplicate_adjudication_storage.rs index fd4b77ab40..9daf8208b6 100644 --- a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_duplicate_adjudication_storage.rs +++ b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_duplicate_adjudication_storage.rs @@ -28,9 +28,7 @@ where T::try_from(value.to_owned()).unwrap() } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn topology_ref(byte: char) -> WorkTopologyGenerationRefV1 { WorkTopologyGenerationRefV1::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() diff --git a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_leak_adjudication_storage.rs b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_leak_adjudication_storage.rs index 6a2a75095b..72e7ee41a8 100644 --- a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_leak_adjudication_storage.rs +++ b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_leak_adjudication_storage.rs @@ -11,8 +11,8 @@ use tracedecay_contracts::{ }; use tracedecay_domain::{ ActorId, AttemptId, CommitId, ConfigurationRevisionId, ConfigurationSnapshotId, - CoverageStateV1, LeakOwnerClassV1, ManifestDigest, ProjectId, ProposalId, ProviderId, RefId, - RepositoryId, RunId, TaskId, UtcMicros, WorkApprovalPolicy, WorkAttemptIdentityV1, + CoverageStateV1, LeakOwnerClassV1, ProjectId, ProposalId, ProviderId, RefId, RepositoryId, + RunId, TaskId, UtcMicros, WorkApprovalPolicy, WorkAttemptIdentityV1, WorkAttemptProjectionBindingV1, WorkAttemptStateV1, WorkAttemptV1, WorkAuthority, WorkCancellationStateV1, WorkCommandId, WorkEffectStateV1, WorkEgressPolicy, WorkExecutableReference, WorkExecutionEnvelopeV1, WorkExecutionLeakKindV1, @@ -35,9 +35,7 @@ where T::try_from(value.to_owned()).unwrap() } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn authority() -> WorkAuthority { WorkAuthority::new( diff --git a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_placement_storage.rs b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_placement_storage.rs index 1e90977e06..6d4df4e8e5 100644 --- a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_placement_storage.rs +++ b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_placement_storage.rs @@ -14,7 +14,7 @@ use std::collections::BTreeSet; use tracedecay_contracts::{WorkPlacementStorageError, WorkPlacementStoragePort}; use tracedecay_domain::{ - ActorId, ManifestDigest, ProjectId, RepositoryId, RunId, TaskId, UtcMicros, WorkAuthority, + ActorId, ProjectId, RepositoryId, RunId, TaskId, UtcMicros, WorkAuthority, WorkPlacementBlockerV1, WorkPlacementIdentityV1, WorkPlacementKindV1, WorkPlacementObservationV1, WorkPlacementPreflightV1, WorkPlacementStateV1, WorkPlacementTargetV1, WorkPlacementV1, WorktreeId, @@ -34,9 +34,7 @@ where T::try_from(value.to_owned()).unwrap() } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn authority(actor: &str) -> WorkAuthority { authority_in_worktree_with_policy(actor, "worktree.placement.storage", 'a') diff --git a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_product_graph_authority.rs b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_product_graph_authority.rs index db9131f1d8..0f256e19b6 100644 --- a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_product_graph_authority.rs +++ b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_product_graph_authority.rs @@ -26,7 +26,7 @@ use tracedecay_contracts::{ }; use tracedecay_domain::{ AcceptanceCriterionId, ActorId, CatalogGenerationId, ConfigurationRevisionId, InitiativeId, - ManifestDigest, MilestoneId, PolicyRevisionId, ProjectId, RepositoryId, TaskId, UtcMicros, + MilestoneId, PolicyRevisionId, ProjectId, RepositoryId, TaskId, UtcMicros, WorkAcceptanceCriterionV1, WorkCommandId, WorkGraphVersionV1, WorkHierarchyV1, WorkInitiativeV1, WorkItemInputV1, WorkItemV1, WorkMilestoneV1, WorkPlanId, WorkPlanV1, WorkProductEventPayloadV1, WorkProductEventSequenceV1, WorkProductGraphV1, @@ -51,9 +51,7 @@ where T::try_from(value.to_owned()).unwrap() } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn binding() -> WorkProductBindingV1 { WorkProductBindingV1::new( diff --git a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_product_query_authority.rs b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_product_query_authority.rs index 1547ac9067..d92366dd98 100644 --- a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_product_query_authority.rs +++ b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_product_query_authority.rs @@ -38,11 +38,11 @@ use tracedecay_contracts::{ }; use tracedecay_domain::{ AcceptanceCriterionId, ActorId, CatalogGenerationId, ConfigurationRevisionId, InitiativeId, - ManifestDigest, MilestoneId, PolicyRevisionId, ProjectId, RepositoryId, RetrievalAnchorId, - TaskEvidenceLinkId, TaskEvidenceLinkV1, TaskId, UtcMicros, WorkAcceptanceCriterionV1, - WorkCommandId, WorkGraphVersionV1, WorkHierarchyV1, WorkInitiativeV1, WorkItemInputV1, - WorkItemV1, WorkMilestoneV1, WorkPlanId, WorkPlanV1, WorkProductEventSequenceV1, - WorkProductGraphV1, WorkTaskEvidenceCoverageV1, WorktreeId, + MilestoneId, PolicyRevisionId, ProjectId, RepositoryId, RetrievalAnchorId, TaskEvidenceLinkId, + TaskEvidenceLinkV1, TaskId, UtcMicros, WorkAcceptanceCriterionV1, WorkCommandId, + WorkGraphVersionV1, WorkHierarchyV1, WorkInitiativeV1, WorkItemInputV1, WorkItemV1, + WorkMilestoneV1, WorkPlanId, WorkPlanV1, WorkProductEventSequenceV1, WorkProductGraphV1, + WorkTaskEvidenceCoverageV1, WorktreeId, }; use tracedecay_rusqlite_runtime::work::WorkSqliteStorage; use tracedecay_tool_catalog::{CapabilityId, UseCaseId}; @@ -64,9 +64,7 @@ where T::try_from(value.to_owned()).unwrap() } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn binding() -> WorkProductBindingV1 { WorkProductBindingV1::new( diff --git a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_run_control_storage.rs b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_run_control_storage.rs index d35f5f8301..ac4237536e 100644 --- a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_run_control_storage.rs +++ b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_run_control_storage.rs @@ -19,10 +19,10 @@ use tracedecay_contracts::{ WorkRunControlStoragePort, }; use tracedecay_domain::{ - ActorId, AttemptId, CommitId, ConfigurationRevisionId, ConfigurationSnapshotId, ManifestDigest, - ProjectId, ProposalId, ProviderId, RefId, RepositoryId, RunId, TaskId, UtcMicros, - WorkApprovalPolicy, WorkAttemptIdentityV1, WorkAttemptProjectionBindingV1, WorkAttemptStateV1, - WorkAttemptV1, WorkAuthority, WorkBlockedIntervalCauseV1, WorkBlockedIntervalClosureV1, + ActorId, AttemptId, CommitId, ConfigurationRevisionId, ConfigurationSnapshotId, ProjectId, + ProposalId, ProviderId, RefId, RepositoryId, RunId, TaskId, UtcMicros, WorkApprovalPolicy, + WorkAttemptIdentityV1, WorkAttemptProjectionBindingV1, WorkAttemptStateV1, WorkAttemptV1, + WorkAuthority, WorkBlockedIntervalCauseV1, WorkBlockedIntervalClosureV1, WorkBlockedIntervalIdentityV1, WorkBlockedIntervalReceiptV1, WorkCancellationStateV1, WorkEffectStateV1, WorkEgressPolicy, WorkExecutableReference, WorkExecutionEnvelopeV1, WorkExecutionLimits, WorkExecutionSnapshot, WorkExecutionSnapshotInput, WorkFallbackTopology, @@ -47,9 +47,7 @@ where T::try_from(value.to_owned()).unwrap() } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn authority(actor: &str) -> WorkAuthority { WorkAuthority::new( diff --git a/crates/tracedecay-session-runtime/src/retained/profile.rs b/crates/tracedecay-session-runtime/src/retained/profile.rs index fe08cb1563..83bb9dfad6 100644 --- a/crates/tracedecay-session-runtime/src/retained/profile.rs +++ b/crates/tracedecay-session-runtime/src/retained/profile.rs @@ -387,7 +387,7 @@ mod tests { use tracedecay_domain::{ BrainId, CanonicalMessageRoleV1, CanonicalObservationEnvelopeV1, CanonicalObservationEvidenceV1, CanonicalObservationFactV1, - CanonicalObservationRelationsV1, DurableObservationV1, ManifestDigest, ObservationId, + CanonicalObservationRelationsV1, DurableObservationV1, ObservationId, ObservationIdentityMaterialV1, ObservationOrderingDomainV1, ObservationScopeV1, ObservationSourceCursorV1, ObservationSourceGenerationV1, ObservationSourceIdentityV1, ObservationSourceRangeV1, PayloadReferenceV1, ProjectId, ProjectionGenerationId, @@ -429,10 +429,7 @@ mod tests { DaemonSessionRetrievalRoot::profile(serving).expect("profile retrieval root") } - fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))) - .expect("manifest digest") - } + use tracedecay_domain::test_fixtures::digest; fn session(provider: &str, session_id: &str, project_key: &str) -> SessionRecord { SessionRecord { diff --git a/crates/tracedecay-store/tests/store_suite/configuration_contract.rs b/crates/tracedecay-store/tests/store_suite/configuration_contract.rs index f1961dd3aa..accaa2d5e2 100644 --- a/crates/tracedecay-store/tests/store_suite/configuration_contract.rs +++ b/crates/tracedecay-store/tests/store_suite/configuration_contract.rs @@ -2,7 +2,7 @@ use tracedecay_domain::configuration::{ ProtectedChangePlan, RedactedConfigurationChangeV1, RollbackModeV1, ScopeControlOperationV1, SettingKey, }; -use tracedecay_domain::{AccessPolicyDigest, ManifestDigest, UtcMicros}; +use tracedecay_domain::{AccessPolicyDigest, UtcMicros}; use tracedecay_store::configuration::{ ConfigurationProtectedOperationV1, ConfigurationProtectedPlanRecordV1, }; @@ -15,9 +15,7 @@ where T::try_from(value.to_owned()).expect("fixture id is canonical") } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; #[test] fn protected_plan_records_bind_the_redacted_plan_to_the_exact_operation() { diff --git a/crates/tracedecay-store/tests/store_suite/external_source_commit.rs b/crates/tracedecay-store/tests/store_suite/external_source_commit.rs index 21e9e31dbb..5d43976a59 100644 --- a/crates/tracedecay-store/tests/store_suite/external_source_commit.rs +++ b/crates/tracedecay-store/tests/store_suite/external_source_commit.rs @@ -1,7 +1,7 @@ use std::collections::BTreeSet; use tracedecay_domain::{ - AccessPolicyDigest, CapabilityId, ComponentVersion, LocatorDigest, ManifestDigest, + AccessPolicyDigest, CapabilityId, ComponentVersion, LocatorDigest, PrivacyDomainBoundLocatorDigest, PrivacyDomainId, ProjectId, ProviderId, ResolutionAuthorizationV1, RetrievalAnchorId, SanitizationReceiptId, SanitizationReceiptRefV1, ScopeResolutionId, SourceAcquisitionCapabilitiesV1, SourceAcquisitionContractV1, @@ -19,9 +19,7 @@ use tracedecay_store::{ apply_source_projection, build_source_projection, }; -fn digest(seed: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", seed.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn definition() -> SourceDefinitionV1 { definition_with_max(4) diff --git a/crates/tracedecay-store/tests/store_suite/multi_root_cas_contract.rs b/crates/tracedecay-store/tests/store_suite/multi_root_cas_contract.rs index 4e08a5831d..4b769d2ebf 100644 --- a/crates/tracedecay-store/tests/store_suite/multi_root_cas_contract.rs +++ b/crates/tracedecay-store/tests/store_suite/multi_root_cas_contract.rs @@ -1,11 +1,9 @@ -use tracedecay_domain::{ManifestDigest, ScopeSetId, ScopeSetRevision}; +use tracedecay_domain::{ScopeSetId, ScopeSetRevision}; use tracedecay_store::runtime::{ AuthorizedScopeSetRecordV1, ScopeSetCompareAndSwapV1, ScopeSetStoreContractError, }; -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn record(revision: u64) -> AuthorizedScopeSetRecordV1 { AuthorizedScopeSetRecordV1::new( diff --git a/crates/tracedecay/src/daemon/invocation_tests/work_evidence_journey_tests.rs b/crates/tracedecay/src/daemon/invocation_tests/work_evidence_journey_tests.rs index cc418c5be4..da1152b5b5 100644 --- a/crates/tracedecay/src/daemon/invocation_tests/work_evidence_journey_tests.rs +++ b/crates/tracedecay/src/daemon/invocation_tests/work_evidence_journey_tests.rs @@ -38,10 +38,7 @@ where T::try_from(value.to_owned()).expect("Work evidence journey identity") } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))) - .expect("Work evidence journey digest") -} +use tracedecay_domain::test_fixtures::digest; fn product_task(task_id: TaskId) -> (WorkInitiativeV1, WorkPlanV1, WorkMilestoneV1, WorkItemV1) { let created_at = UtcMicros(10); diff --git a/crates/tracedecay/src/daemon/project_open_owners/advisory_runtime/scout_journey_tests.rs b/crates/tracedecay/src/daemon/project_open_owners/advisory_runtime/scout_journey_tests.rs index 4a471304aa..5a1ec53457 100644 --- a/crates/tracedecay/src/daemon/project_open_owners/advisory_runtime/scout_journey_tests.rs +++ b/crates/tracedecay/src/daemon/project_open_owners/advisory_runtime/scout_journey_tests.rs @@ -50,9 +50,7 @@ where T::try_from(value.to_owned()).unwrap() } -fn digest(character: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", character.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn configured_model_evidence(marker: u8) -> ContextScoutEvidenceEnvelopeV1 { let scope = ResolvedScope::new( diff --git a/crates/tracedecay/src/mcp/tools/handlers/retained_timeout_dispatch_tests.rs b/crates/tracedecay/src/mcp/tools/handlers/retained_timeout_dispatch_tests.rs index 690c880efd..0a5d49978b 100644 --- a/crates/tracedecay/src/mcp/tools/handlers/retained_timeout_dispatch_tests.rs +++ b/crates/tracedecay/src/mcp/tools/handlers/retained_timeout_dispatch_tests.rs @@ -29,9 +29,7 @@ use super::*; use crate::config::lock_user_data_dir_test_env; use crate::project::TraceDecay; -fn digest(seed: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", seed.to_string().repeat(64))).expect("fixture digest") -} +use tracedecay_domain::test_fixtures::digest; fn retained_scope() -> ResolvedScope { ResolvedScope::new( diff --git a/crates/tracedecay/src/mcp/tools/handlers/stack_snapshot_behavior_tests.rs b/crates/tracedecay/src/mcp/tools/handlers/stack_snapshot_behavior_tests.rs index c96ab32c98..6dd4266c3c 100644 --- a/crates/tracedecay/src/mcp/tools/handlers/stack_snapshot_behavior_tests.rs +++ b/crates/tracedecay/src/mcp/tools/handlers/stack_snapshot_behavior_tests.rs @@ -53,9 +53,7 @@ impl McpTransport for CaptureTransport { } } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).expect("digest") -} +use tracedecay_domain::test_fixtures::digest; fn git(root: &Path, arguments: &[&str]) { let status = Command::new(try_git_program().expect("git program")) diff --git a/crates/tracedecay/tests/daemon_suite/invocation_observability.rs b/crates/tracedecay/tests/daemon_suite/invocation_observability.rs index 1e6413558e..43c2956650 100644 --- a/crates/tracedecay/tests/daemon_suite/invocation_observability.rs +++ b/crates/tracedecay/tests/daemon_suite/invocation_observability.rs @@ -22,9 +22,7 @@ use tracedecay_domain::{ WorktreeId, canonical_sha256, }; -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).expect("digest") -} +use tracedecay_domain::test_fixtures::digest; /// A registry mount request whose store-authority fields match `identity`, /// stamping that identity's configuration and policy revisions for the diff --git a/crates/tracedecay/tests/product_surface_suite/api_application_parity.rs b/crates/tracedecay/tests/product_surface_suite/api_application_parity.rs index 0e428fbfab..9dc2e08a53 100644 --- a/crates/tracedecay/tests/product_surface_suite/api_application_parity.rs +++ b/crates/tracedecay/tests/product_surface_suite/api_application_parity.rs @@ -811,9 +811,7 @@ where T::try_from(value.to_owned()).expect("fixture identity") } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).expect("fixture digest") -} +use tracedecay_domain::test_fixtures::digest; fn oid(byte: char) -> GitOidV1 { GitOidV1::new(byte.to_string().repeat(40)).expect("fixture object id") diff --git a/crates/tracedecay/tests/runtime_acceptance_suite/daemon_runtime_acceptance.rs b/crates/tracedecay/tests/runtime_acceptance_suite/daemon_runtime_acceptance.rs index d1d00a45d8..b47025edff 100644 --- a/crates/tracedecay/tests/runtime_acceptance_suite/daemon_runtime_acceptance.rs +++ b/crates/tracedecay/tests/runtime_acceptance_suite/daemon_runtime_acceptance.rs @@ -44,9 +44,7 @@ where T::try_from(value.to_owned()).unwrap() } -fn digest(character: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", character.to_string().repeat(64))).unwrap() -} +use tracedecay_domain::test_fixtures::digest; fn scout_evidence(now: UtcMicros) -> ContextScoutEvidenceEnvelopeV1 { let scope = ResolvedScope::new( diff --git a/crates/tracedecay/tests/runtime_acceptance_suite/runtime_surface_acceptance.rs b/crates/tracedecay/tests/runtime_acceptance_suite/runtime_surface_acceptance.rs index 4e2fb7c1df..337ed9d97e 100644 --- a/crates/tracedecay/tests/runtime_acceptance_suite/runtime_surface_acceptance.rs +++ b/crates/tracedecay/tests/runtime_acceptance_suite/runtime_surface_acceptance.rs @@ -3641,6 +3641,4 @@ fn cancelled_receipt(context: &RequestContext) -> OperationReceipt { receipt } -fn digest(byte: char) -> ManifestDigest { - ManifestDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).expect("manifest digest") -} +use tracedecay_domain::test_fixtures::digest; From 6e8342a91f2ac28e4e3267ca7d08db8ecb0254a3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:42:12 +0000 Subject: [PATCH 146/182] simplify(pass-4/5): share saturating elapsed microseconds Co-authored-by: Zack Jackson --- .../src/hooks/analytics.rs | 2 +- .../src/feedback/cycle_runtime.rs | 6 +++--- .../src/code_index_executor.rs | 2 +- .../queries/graph_control.rs | 2 +- .../src/handlers/git/context.rs | 2 +- crates/tracedecay-mcp/src/response_handles.rs | 2 +- .../tracedecay-runtime-core/src/tracedecay.rs | 21 +++++++++++++++++++ 7 files changed, 29 insertions(+), 8 deletions(-) diff --git a/crates/tracedecay-agent-hosts/src/hooks/analytics.rs b/crates/tracedecay-agent-hosts/src/hooks/analytics.rs index ade1360cd4..4f70bd8851 100644 --- a/crates/tracedecay-agent-hosts/src/hooks/analytics.rs +++ b/crates/tracedecay-agent-hosts/src/hooks/analytics.rs @@ -440,7 +440,7 @@ pub(crate) fn measure_json_payload_bytes(value: &T) -> Op } pub(crate) fn elapsed_us(started: Instant) -> u64 { - started.elapsed().as_micros().min(u128::from(u64::MAX)) as u64 + tracedecay_runtime_core::tracedecay::saturating_duration_micros(started.elapsed()) } fn duration_as_millis_u64(budget: Duration) -> u64 { diff --git a/crates/tracedecay-application/src/feedback/cycle_runtime.rs b/crates/tracedecay-application/src/feedback/cycle_runtime.rs index e5fe62b5cb..7f247c26f9 100644 --- a/crates/tracedecay-application/src/feedback/cycle_runtime.rs +++ b/crates/tracedecay-application/src/feedback/cycle_runtime.rs @@ -639,7 +639,7 @@ impl FeedbackCycleRuntimePort for FeedbackCycleRuntime { let invocation = (runtime.lsp_input)(request).await?; if !lsp_trigger_matches_invocation(trigger, &invocation) { let duration_micros = - u64::try_from(started_at.elapsed().as_micros()).unwrap_or(u64::MAX); + tracedecay_runtime_core::tracedecay::saturating_duration_micros(started_at.elapsed()); runtime.source_observations.observe_source_event( &invocation.request.input, FeedbackSourceEventV1::ArgumentRejected { @@ -660,7 +660,7 @@ impl FeedbackCycleRuntimePort for FeedbackCycleRuntime { } let input = invocation.request.input.clone(); let admission_duration_micros = - u64::try_from(started_at.elapsed().as_micros()).unwrap_or(u64::MAX); + tracedecay_runtime_core::tracedecay::saturating_duration_micros(started_at.elapsed()); runtime.source_observations.observe_source_event( &input, lsp_method_state_event( @@ -672,7 +672,7 @@ impl FeedbackCycleRuntimePort for FeedbackCycleRuntime { ); let result = Box::pin(runtime.run_once(invocation)).await; let duration_micros = - u64::try_from(started_at.elapsed().as_micros()).unwrap_or(u64::MAX); + tracedecay_runtime_core::tracedecay::saturating_duration_micros(started_at.elapsed()); let outcome = if result.is_ok() { FeedbackOutcomeV1::Completed } else { diff --git a/crates/tracedecay-code-index-runtime/src/code_index_executor.rs b/crates/tracedecay-code-index-runtime/src/code_index_executor.rs index a9e79db843..a45245ff44 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_executor.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_executor.rs @@ -623,7 +623,7 @@ impl tracedecay_query::retrieval::ports::Retriev } fn elapsed_micros(&self) -> u64 { - u64::try_from(self.started.elapsed().as_micros()).unwrap_or(u64::MAX) + tracedecay_runtime_core::tracedecay::saturating_duration_micros(self.started.elapsed()) } } diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/queries/graph_control.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/queries/graph_control.rs index 1e30be50fd..f0e9a6d5c8 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/queries/graph_control.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/queries/graph_control.rs @@ -25,7 +25,7 @@ impl RetrievalExecutionControl for CallableRetrievalExecutionControl { } fn elapsed_micros(&self) -> u64 { - u64::try_from(self.started_at.elapsed().as_micros()).unwrap_or(u64::MAX) + tracedecay_runtime_core::tracedecay::saturating_duration_micros(self.started_at.elapsed()) } } diff --git a/crates/tracedecay-mcp/src/handlers/git/context.rs b/crates/tracedecay-mcp/src/handlers/git/context.rs index fde95a605f..6f279458ad 100644 --- a/crates/tracedecay-mcp/src/handlers/git/context.rs +++ b/crates/tracedecay-mcp/src/handlers/git/context.rs @@ -888,7 +888,7 @@ impl PrContextControls { } fn elapsed_micros(started: std::time::Instant) -> u64 { - u64::try_from(started.elapsed().as_micros()).map_or(u64::MAX, |value| value) + tracedecay_runtime_core::tracedecay::saturating_duration_micros(started.elapsed()) } fn pr_context_impact_snapshot( diff --git a/crates/tracedecay-mcp/src/response_handles.rs b/crates/tracedecay-mcp/src/response_handles.rs index a7178f6061..2873cf38c5 100644 --- a/crates/tracedecay-mcp/src/response_handles.rs +++ b/crates/tracedecay-mcp/src/response_handles.rs @@ -392,7 +392,7 @@ fn timestamp_json(value: i64) -> Value { } fn duration_micros_u64(duration: Duration) -> u64 { - duration.as_micros().min(u128::from(u64::MAX)) as u64 + tracedecay_runtime_core::tracedecay::saturating_duration_micros(duration) } fn error_class(error: &TraceDecayError) -> &'static str { diff --git a/crates/tracedecay-runtime-core/src/tracedecay.rs b/crates/tracedecay-runtime-core/src/tracedecay.rs index 75bb627f91..2af89539a3 100644 --- a/crates/tracedecay-runtime-core/src/tracedecay.rs +++ b/crates/tracedecay-runtime-core/src/tracedecay.rs @@ -57,3 +57,24 @@ fn unix_micros_saturating(pre_epoch: i64) -> i64 { Err(_) => pre_epoch, } } + +/// Microseconds in `duration`, saturating to `u64::MAX` on overflow. +pub fn saturating_duration_micros(duration: Duration) -> u64 { + u64::try_from(duration.as_micros()).unwrap_or(u64::MAX) +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use super::saturating_duration_micros; + + #[test] + fn saturating_duration_micros_keeps_small_spans_and_clamps_overflow() { + assert_eq!(saturating_duration_micros(Duration::from_micros(7)), 7); + assert_eq!( + saturating_duration_micros(Duration::from_secs(u64::MAX)), + u64::MAX + ); + } +} From 9094c5adb0b927c2f2e719406aefac38ea58fba4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:42:48 +0000 Subject: [PATCH 147/182] simplify(graph-query): drop uncalled analytical reads Co-authored-by: Zack Jackson --- crates/tracedecay-graph-query/src/lib.rs | 2 +- crates/tracedecay-graph-query/src/queries.rs | 93 ------------------- .../src/verified_query.rs | 23 +---- 3 files changed, 2 insertions(+), 116 deletions(-) diff --git a/crates/tracedecay-graph-query/src/lib.rs b/crates/tracedecay-graph-query/src/lib.rs index 589e76be45..b92432a60b 100644 --- a/crates/tracedecay-graph-query/src/lib.rs +++ b/crates/tracedecay-graph-query/src/lib.rs @@ -32,7 +32,7 @@ pub use projection::{ request_graph_cancellation, }; pub use queries::{ - FileAdjacencyScan, GraphQueryManager, NodeMetrics, VerifiedHealthFileAggregateV1, + FileAdjacencyScan, GraphQueryManager, VerifiedHealthFileAggregateV1, }; #[cfg(any(test, feature = "test-helpers"))] pub use verified_query::admitted_verified_graph_query_port; diff --git a/crates/tracedecay-graph-query/src/queries.rs b/crates/tracedecay-graph-query/src/queries.rs index 4addc3d0f3..854403dbe7 100644 --- a/crates/tracedecay-graph-query/src/queries.rs +++ b/crates/tracedecay-graph-query/src/queries.rs @@ -28,16 +28,6 @@ const HEALTH_EDGE_KINDS: [RelationEdgeKindV1; 8] = [ RelationEdgeKindV1::Annotates, ]; -#[derive(Debug, Clone)] -pub struct NodeMetrics { - pub incoming_edge_count: usize, - pub outgoing_edge_count: usize, - pub call_count: usize, - pub caller_count: usize, - pub child_count: usize, - pub depth: usize, -} - #[derive(Debug)] pub struct FileAdjacencyScan { pub adjacency: HashMap>, @@ -287,59 +277,6 @@ impl<'a> GraphQueryManager<'a> { Ok(dead) } - #[hotpath::measure(label = "usecases.graph.node_metrics", future = true)] - pub async fn get_node_metrics(&self, node_id: &str) -> Result { - let occurrence = SymbolOccurrenceId::new(node_id.to_owned()).map_err(|error| { - TraceDecayError::Config { - message: error.to_string(), - } - })?; - let counts = self - .reader - .edge_kind_counts(&occurrence, Arc::clone(&self.cancellation)) - .map_err(|error| { - super::map_code_graph_read_runtime_error(map_projection_error(error)) - })?; - let incoming_edge_count = - usize::try_from(counts.incoming.values().sum::()).unwrap_or(usize::MAX); - let outgoing_edge_count = - usize::try_from(counts.outgoing.values().sum::()).unwrap_or(usize::MAX); - Ok(NodeMetrics { - incoming_edge_count, - outgoing_edge_count, - call_count: usize::try_from( - counts - .outgoing - .get(&RelationEdgeKindV1::Calls) - .copied() - .unwrap_or(0), - ) - .unwrap_or(usize::MAX), - caller_count: usize::try_from( - counts - .incoming - .get(&RelationEdgeKindV1::Calls) - .copied() - .unwrap_or(0), - ) - .unwrap_or(usize::MAX), - child_count: usize::try_from( - counts - .outgoing - .get(&RelationEdgeKindV1::Contains) - .copied() - .unwrap_or(0), - ) - .unwrap_or(usize::MAX), - depth: 0, - }) - } - - #[hotpath::measure(label = "usecases.graph.file_dependencies", future = true)] - pub async fn get_file_dependencies(&self, file_path: &str) -> Result> { - self.file_neighbors(file_path, false) - } - #[hotpath::measure(label = "usecases.graph.file_dependents", future = true)] pub async fn get_file_dependents(&self, file_path: &str) -> Result> { self.file_neighbors(file_path, true) @@ -578,36 +515,6 @@ impl<'a> GraphQueryManager<'a> { }) } - #[hotpath::measure(label = "usecases.graph.health_file_aggregates", future = true)] - pub async fn health_file_aggregates( - &self, - path_prefix: Option<&str>, - ) -> Result> { - let logical_paths = match path_prefix { - Some(prefix) => Some( - self.reader - .files(MAX_ANALYTICAL_SYMBOLS, Arc::clone(&self.cancellation)) - .map_err(|error| { - super::map_code_graph_read_runtime_error(map_projection_error(error)) - })? - .into_iter() - .map(|file| file.logical_path) - .filter(|path| path_is_within(path, prefix)) - .collect::>(), - ), - None => None, - }; - let (symbols, edges, external_test_markers) = - self.health_evidence(logical_paths.as_ref())?; - let metadata = health_symbol_metadata(&symbols)?; - Ok(fold_health_aggregates( - metadata, - &edges, - external_test_markers, - path_prefix, - )) - } - /// Health symbols, the induced edge set, and the test markers only the /// scoped `callers` walk can see: its far endpoints legitimately sit /// outside the scoped symbol census, so their marker metadata cannot be diff --git a/crates/tracedecay-graph-query/src/verified_query.rs b/crates/tracedecay-graph-query/src/verified_query.rs index 1978b8144d..774e84fc2d 100644 --- a/crates/tracedecay-graph-query/src/verified_query.rs +++ b/crates/tracedecay-graph-query/src/verified_query.rs @@ -23,7 +23,7 @@ use tracedecay_domain::{ }; use tracedecay_graph_db::GraphCancellation; -use super::queries::{GraphQueryManager, NodeMetrics, VerifiedHealthFileAggregateV1}; +use super::queries::GraphQueryManager; use super::source_authority::{ AdmittedSourceAuthority, graph_source_scope_mismatch, graph_source_unbound, }; @@ -298,27 +298,6 @@ impl VerifiedGraphQuery { .await } - #[hotpath::measure(label = "usecases.graph.verified.file_dependencies", future = true)] - pub async fn get_file_dependencies(&self, file_path: &str) -> Result> { - self.await_bound(self.manager().get_file_dependencies(file_path)) - .await - } - - #[hotpath::measure(label = "usecases.graph.verified.node_metrics", future = true)] - pub async fn get_node_metrics(&self, node_id: &str) -> Result { - self.await_bound(self.manager().get_node_metrics(node_id)) - .await - } - - #[hotpath::measure(label = "usecases.graph.verified.health_aggregates", future = true)] - pub async fn health_file_aggregates( - &self, - path_prefix: Option<&str>, - ) -> Result> { - self.await_bound(self.manager().health_file_aggregates(path_prefix)) - .await - } - #[hotpath::measure(label = "usecases.graph.verified.health_snapshot", future = true)] pub async fn verified_health_snapshot( &self, From dfe66125c7ea4b4226e992477ba64bdfe9b8b09a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:43:19 +0000 Subject: [PATCH 148/182] simplify(pass-4/5): drop fixture-only production edges tracedecay glob, toml, hex, and libc are test links, so they move to dev-dependencies. Project temporal-query is optional behind test-helpers, which is the only compiler of that fixture. Co-authored-by: Zack Jackson --- crates/tracedecay-project/Cargo.toml | 3 ++- crates/tracedecay/Cargo.toml | 10 ++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/tracedecay-project/Cargo.toml b/crates/tracedecay-project/Cargo.toml index 6fccb31a3c..c6778e68a7 100644 --- a/crates/tracedecay-project/Cargo.toml +++ b/crates/tracedecay-project/Cargo.toml @@ -37,6 +37,7 @@ test-helpers = [ "tracedecay-session-temporal-store/test-helpers", "tracedecay-code-index/test-helpers", "tracedecay-graph-query/test-helpers", + "dep:tracedecay-temporal-query", ] # Mirrors the composition root's `test-transport`: the standalone @@ -91,7 +92,7 @@ tracedecay-sessions = { path = "../tracedecay-sessions", version = "0.1.0" } tracedecay-source-edit = { path = "../tracedecay-source-edit", version = "0.1.0" } tracedecay-store = { path = "../tracedecay-store", version = "0.1.0" } tracedecay-store-runtime = { path = "../tracedecay-store-runtime", version = "0.1.0" } -tracedecay-temporal-query = { path = "../tracedecay-temporal-query", version = "0.1.0" } +tracedecay-temporal-query = { path = "../tracedecay-temporal-query", version = "0.1.0", optional = true } tracedecay-tool-catalog = { path = "../tracedecay-tool-catalog", version = "0.1.0" } [dev-dependencies] diff --git a/crates/tracedecay/Cargo.toml b/crates/tracedecay/Cargo.toml index 8faf90edc7..527a890030 100644 --- a/crates/tracedecay/Cargo.toml +++ b/crates/tracedecay/Cargo.toml @@ -324,7 +324,6 @@ tracedecay-tool-catalog = { path = "../tracedecay-tool-catalog", version = "0.1. tracedecay-application = { path = "../tracedecay-application", version = "0.1.0" } serde = { version = "1", features = ["derive"] } serde_json = "1" -toml = "1" ureq = { version = "3", features = ["json"] } url = "2" tokio = { version = "1", features = ["full"] } @@ -332,10 +331,8 @@ tokio-stream = { version = "0.1", features = ["sync"] } tracing = "0.1" tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] } sha2 = "0.11" -glob = "0.3" walkdir = "2" gix.workspace = true -hex = "0.4" zeroize = "1.9.0" keyring = { version = "4.1.5", features = ["v1"] } tempfile = "3" @@ -347,16 +344,17 @@ rmcp = { version = "3.0.1", default-features = false, features = ["server"] } tracedecay-search-eval = { path = "../tracedecay-search-eval", version = "0.1.0", optional = true } # `kill(2)` for the daemon integration suites' physical-restart journeys -# (tests/daemon_suite). Store-locality detection moved with the locator -# resolver to tracedecay-store-runtime. -[target.'cfg(any(target_os = "macos", all(target_os = "linux", target_env = "gnu")))'.dependencies] +# (tests/daemon_suite). Store-locality detection lives in tracedecay-store-runtime. +[target.'cfg(any(target_os = "macos", all(target_os = "linux", target_env = "gnu")))'.dev-dependencies] libc = "0.2" [dev-dependencies] tree-sitter = "0.26" rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] } tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12"] } +glob = "0.3" hex = "0.4" +toml = "1" regex = "1.12.3" filetime = "0.2" rmcp = { version = "3.0.1", default-features = false, features = ["client", "server", "transport-async-rw"] } From fbe65a7572d4e8539bdb0bb530cd5b4e71e2fe8c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:44:36 +0000 Subject: [PATCH 149/182] simplify(pass-5/5): share saturating millisecond stamps Co-authored-by: Zack Jackson --- .../src/hooks/analytics.rs | 9 +++----- .../src/hooks/daemon_ports.rs | 6 +++++- .../src/invocation/clock.rs | 7 +------ .../src/profile_host_admission_replay.rs | 9 ++++++-- crates/tracedecay-lcm/src/gc.rs | 3 ++- .../src/runtime_telemetry.rs | 2 +- .../src/shard_runtime/telemetry.rs | 8 ++----- .../tracedecay-runtime-core/src/tracedecay.rs | 21 ++++++++++++++++++- 8 files changed, 41 insertions(+), 24 deletions(-) diff --git a/crates/tracedecay-agent-hosts/src/hooks/analytics.rs b/crates/tracedecay-agent-hosts/src/hooks/analytics.rs index 4f70bd8851..dc8a3f657a 100644 --- a/crates/tracedecay-agent-hosts/src/hooks/analytics.rs +++ b/crates/tracedecay-agent-hosts/src/hooks/analytics.rs @@ -1,7 +1,7 @@ use std::path::{Path, PathBuf}; use std::sync::Mutex; use std::sync::atomic::{AtomicU64, Ordering}; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +use std::time::{Duration, Instant}; use serde::Serialize; use serde_json::Value; @@ -444,7 +444,7 @@ pub(crate) fn elapsed_us(started: Instant) -> u64 { } fn duration_as_millis_u64(budget: Duration) -> u64 { - u64::try_from(budget.as_millis()).unwrap_or(u64::MAX) + tracedecay_runtime_core::tracedecay::saturating_duration_millis(budget) } fn bounded_identifier(value: &str) -> String { @@ -698,10 +698,7 @@ fn append_private_jsonl(path: &Path, line: &str) { } fn now_unix_millis() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|duration| duration.as_millis().min(u128::from(u64::MAX)) as u64) - .unwrap_or_default() + tracedecay_runtime_core::tracedecay::unix_millis() } mod readiness; diff --git a/crates/tracedecay-agent-hosts/src/hooks/daemon_ports.rs b/crates/tracedecay-agent-hosts/src/hooks/daemon_ports.rs index f6f95134a7..e35e0f5db5 100644 --- a/crates/tracedecay-agent-hosts/src/hooks/daemon_ports.rs +++ b/crates/tracedecay-agent-hosts/src/hooks/daemon_ports.rs @@ -123,7 +123,11 @@ struct DaemonAdmissionResponseWireV1 { } pub(crate) fn now_utc() -> UtcMicros { - UtcMicros(tracedecay_runtime_core::tracedecay::saturating_utc_now().0.max(1)) + UtcMicros( + tracedecay_runtime_core::tracedecay::saturating_utc_now() + .0 + .max(1), + ) } #[hotpath::measure(label = "agent_hosts.hook_ports.admission_decode")] diff --git a/crates/tracedecay-daemon-service/src/invocation/clock.rs b/crates/tracedecay-daemon-service/src/invocation/clock.rs index db0e823791..492d15c2d2 100644 --- a/crates/tracedecay-daemon-service/src/invocation/clock.rs +++ b/crates/tracedecay-daemon-service/src/invocation/clock.rs @@ -1,13 +1,8 @@ //! Shared wall-clock readings for daemon invocation admission and expiry. -use std::time::{SystemTime, UNIX_EPOCH}; - pub use tracedecay_contracts::clock::now_micros; pub use tracedecay_contracts::clock::now_micros as current_micros; pub fn now_millis() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|duration| u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)) - .unwrap_or_default() + tracedecay_runtime_core::tracedecay::unix_millis() } diff --git a/crates/tracedecay-daemon-service/src/profile_host_admission_replay.rs b/crates/tracedecay-daemon-service/src/profile_host_admission_replay.rs index 255e0622ef..866dbcc414 100644 --- a/crates/tracedecay-daemon-service/src/profile_host_admission_replay.rs +++ b/crates/tracedecay-daemon-service/src/profile_host_admission_replay.rs @@ -639,9 +639,14 @@ impl ProfileHostAdmissionBootstrapWorker { event = "profile_host_admission_bootstrap_exhausted", reason_code, attempts = consecutive_retryable, - elapsed_ms = u64::try_from(elapsed.as_millis()).unwrap_or(u64::MAX), + elapsed_ms = + tracedecay_runtime_core::tracedecay::saturating_duration_millis( + elapsed, + ), budget_ms = - u64::try_from(self.retry_budget.as_millis()).unwrap_or(u64::MAX), + tracedecay_runtime_core::tracedecay::saturating_duration_millis( + self.retry_budget, + ), "profile host admission bootstrap gave up after its retry budget; \ it resumes on the next admission or daemon restart" ); diff --git a/crates/tracedecay-lcm/src/gc.rs b/crates/tracedecay-lcm/src/gc.rs index 63bfc4fc86..42be3fa5f5 100644 --- a/crates/tracedecay-lcm/src/gc.rs +++ b/crates/tracedecay-lcm/src/gc.rs @@ -625,7 +625,8 @@ pub async fn run_payload_gc_in_transaction( report.ended_at = now; if apply { - let duration_ms = u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX); + let duration_ms = + tracedecay_runtime_core::tracedecay::saturating_duration_millis(started.elapsed()); let status = if report.errors.is_empty() { "ok" } else { diff --git a/crates/tracedecay-runtime-core/src/runtime_telemetry.rs b/crates/tracedecay-runtime-core/src/runtime_telemetry.rs index a5346b8b35..ba7caf7011 100644 --- a/crates/tracedecay-runtime-core/src/runtime_telemetry.rs +++ b/crates/tracedecay-runtime-core/src/runtime_telemetry.rs @@ -640,7 +640,7 @@ impl ProcessSampler { Some(age), ) => ProcessTelemetry::Stale { sampled_at: *sampled_at, - age_millis: age.as_millis().min(u128::from(u64::MAX)) as u64, + age_millis: crate::tracedecay::saturating_duration_millis(age), snapshot: snapshot.clone(), }, (Some(sample), _) => sample.telemetry.clone(), diff --git a/crates/tracedecay-runtime-core/src/shard_runtime/telemetry.rs b/crates/tracedecay-runtime-core/src/shard_runtime/telemetry.rs index c9c0d60573..a8ab8f748b 100644 --- a/crates/tracedecay-runtime-core/src/shard_runtime/telemetry.rs +++ b/crates/tracedecay-runtime-core/src/shard_runtime/telemetry.rs @@ -5,7 +5,6 @@ //! metrics backend, retain global state, or derive identity from a locator. use std::cmp::Ordering; -use std::time::Duration; use tracedecay_store::{ AdmissionConfigV1, QueueBudgetV1, RuntimeMaintenanceStateV1, StoreRuntimeBindingV1, WalBudgetV1, @@ -397,7 +396,7 @@ fn project_shard( memory_estimate_bytes: entry.physical.memory_estimate_bytes, health: health.health, pinned_profile: health.pinned_profile, - idle_for_ms: duration_millis(entry.eviction.idle_for), + idle_for_ms: crate::tracedecay::saturating_duration_millis(entry.eviction.idle_for), eviction_eligible: entry.eviction.is_eligible(), eviction_blocker_count: bounded_count(entry.eviction.blockers.len()), } @@ -418,10 +417,6 @@ fn sum_complete_sample(total: Option, sample: Option) -> Option { Some(total?.saturating_add(sample?)) } -fn duration_millis(duration: Duration) -> u64 { - u64::try_from(duration.as_millis()).unwrap_or(u64::MAX) -} - const fn count_if(value: bool) -> u32 { if value { 1 } else { 0 } } @@ -429,6 +424,7 @@ const fn count_if(value: bool) -> u32 { #[cfg(test)] mod tests { use std::fmt::Debug; + use std::time::Duration; use tracedecay_domain::{BrainId, ProjectId, UserProfileId}; use tracedecay_store::{StoreAuthorityEpochV1, StoreIncarnationV1, StoreShardIdV1}; diff --git a/crates/tracedecay-runtime-core/src/tracedecay.rs b/crates/tracedecay-runtime-core/src/tracedecay.rs index 2af89539a3..559ab54f79 100644 --- a/crates/tracedecay-runtime-core/src/tracedecay.rs +++ b/crates/tracedecay-runtime-core/src/tracedecay.rs @@ -63,11 +63,21 @@ pub fn saturating_duration_micros(duration: Duration) -> u64 { u64::try_from(duration.as_micros()).unwrap_or(u64::MAX) } +/// Milliseconds in `duration`, saturating to `u64::MAX` on overflow. +pub fn saturating_duration_millis(duration: Duration) -> u64 { + u64::try_from(duration.as_millis()).unwrap_or(u64::MAX) +} + +/// Unix milliseconds since the epoch. A pre-epoch clock is `0`. +pub fn unix_millis() -> u64 { + saturating_duration_millis(wall_clock_since_epoch()) +} + #[cfg(test)] mod tests { use std::time::Duration; - use super::saturating_duration_micros; + use super::{saturating_duration_micros, saturating_duration_millis}; #[test] fn saturating_duration_micros_keeps_small_spans_and_clamps_overflow() { @@ -77,4 +87,13 @@ mod tests { u64::MAX ); } + + #[test] + fn saturating_duration_millis_keeps_small_spans_and_clamps_overflow() { + assert_eq!(saturating_duration_millis(Duration::from_millis(7)), 7); + assert_eq!( + saturating_duration_millis(Duration::from_secs(u64::MAX)), + u64::MAX + ); + } } From fc9ad70e4d64a050d27f00d2a3914434f428d4f6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:44:50 +0000 Subject: [PATCH 150/182] simplify(pass-4/5): share forward-slash path text Backslash folding lives in one helper. Trailing separators and an empty string stay; a verbatim prefix is not stripped here. Co-authored-by: Zack Jackson --- .../src/agents/host_config_io.rs | 13 +++---- .../src/agents/host_config_io/tests.rs | 10 +++--- crates/tracedecay-cli/src/update_cmd.rs | 13 +++---- .../tracedecay-code-index/src/source_walk.rs | 2 +- .../src/unmounted_files.rs | 5 +-- crates/tracedecay-domain/src/lib.rs | 4 ++- crates/tracedecay-domain/src/text.rs | 36 ++++++++++++++++++- .../src/registry_maintenance/orphan.rs | 12 +++---- .../src/storage/paths_and_io.rs | 2 +- 9 files changed, 59 insertions(+), 38 deletions(-) diff --git a/crates/tracedecay-agent-hosts/src/agents/host_config_io.rs b/crates/tracedecay-agent-hosts/src/agents/host_config_io.rs index fd635dbf6d..f32dc372a7 100644 --- a/crates/tracedecay-agent-hosts/src/agents/host_config_io.rs +++ b/crates/tracedecay-agent-hosts/src/agents/host_config_io.rs @@ -741,7 +741,8 @@ pub(crate) fn host_home_override(home: &Path, env_key: &str, default_relative: & /// On Windows the returned path uses forward slashes so it can be safely /// embedded in JSON hook commands without backslash-escaping issues. pub fn which_tracedecay() -> Option { - which_tracedecay_path().and_then(|path| path.to_str().map(normalize_path_separators)) + which_tracedecay_path() + .and_then(|path| path.to_str().map(tracedecay_domain::forward_slash_text)) } /// Finds the tracedecay binary without converting its platform-native path. @@ -764,7 +765,7 @@ fn which_tracedecay_from( cargo_target_dir: Option<&Path>, ) -> Option { which_tracedecay_path_from(current_exe, path_var, cargo_target_dir) - .and_then(|path| path.to_str().map(normalize_path_separators)) + .and_then(|path| path.to_str().map(tracedecay_domain::forward_slash_text)) } fn which_tracedecay_path_from( @@ -862,12 +863,6 @@ fn path_component_eq(actual: &std::ffi::OsStr, expected: impl AsRef String { - path.replace('\\', "/") -} - /// Remove explicitly retired sibling plugin trees. /// /// Both the retired suffix and ownership manifest are allow-listed: a name @@ -967,7 +962,7 @@ pub(crate) fn hook_command(tracedecay_bin: &str, subcommand: &str) -> String { fn hook_command_for_platform(tracedecay_bin: &str, subcommand: &str, windows: bool) -> String { let quoted = if windows { - quote_windows_command_arg(&normalize_path_separators(tracedecay_bin)) + quote_windows_command_arg(&tracedecay_domain::forward_slash_text(tracedecay_bin)) } else { quote_posix_command_arg(tracedecay_bin) }; diff --git a/crates/tracedecay-agent-hosts/src/agents/host_config_io/tests.rs b/crates/tracedecay-agent-hosts/src/agents/host_config_io/tests.rs index c9b0242afe..976c996041 100644 --- a/crates/tracedecay-agent-hosts/src/agents/host_config_io/tests.rs +++ b/crates/tracedecay-agent-hosts/src/agents/host_config_io/tests.rs @@ -262,7 +262,7 @@ mod path_normalize_tests { #[test] fn normalizes_windows_backslashes() { assert_eq!( - normalize_path_separators(r"C:\Users\dev\scoop\shims\tracedecay.exe"), + tracedecay_domain::forward_slash_text(r"C:\Users\dev\scoop\shims\tracedecay.exe"), "C:/Users/dev/scoop/shims/tracedecay.exe" ); } @@ -284,7 +284,7 @@ mod path_normalize_tests { assert_eq!( found, - normalize_path_separators(&path_bin.to_string_lossy()) + tracedecay_domain::forward_slash_text(&path_bin.to_string_lossy()) ); } @@ -307,7 +307,7 @@ mod path_normalize_tests { assert_eq!( found, - normalize_path_separators(&path_bin.to_string_lossy()) + tracedecay_domain::forward_slash_text(&path_bin.to_string_lossy()) ); } @@ -332,7 +332,7 @@ mod path_normalize_tests { assert_eq!( found, - normalize_path_separators(&stable_bin.to_string_lossy()) + tracedecay_domain::forward_slash_text(&stable_bin.to_string_lossy()) ); } @@ -350,7 +350,7 @@ mod path_normalize_tests { assert_eq!( found, - normalize_path_separators(¤t_exe.to_string_lossy()) + tracedecay_domain::forward_slash_text(¤t_exe.to_string_lossy()) ); } } diff --git a/crates/tracedecay-cli/src/update_cmd.rs b/crates/tracedecay-cli/src/update_cmd.rs index 9e2ec41121..ed20f80e06 100644 --- a/crates/tracedecay-cli/src/update_cmd.rs +++ b/crates/tracedecay-cli/src/update_cmd.rs @@ -312,11 +312,8 @@ fn current_tracedecay_exe() -> Option { fn current_tracedecay_exe_from(current: Option<&Path>) -> Option { let current = current?; let stem = current.file_stem()?.to_str()?; - (stem == "tracedecay").then(|| normalize_bin_path(current)) -} - -fn normalize_bin_path(path: &Path) -> String { - path.to_string_lossy().replace('\\', "/") + (stem == "tracedecay") + .then(|| tracedecay_domain::forward_slash_text(¤t.to_string_lossy())) } /// How the `post-update` re-exec reacts to the binary-upgrade outcome. @@ -497,7 +494,7 @@ fn post_update_binary(installed: Option<&Path>) -> tracedecay_domain::errors::Re fn post_update_binary_from(installed: Option<&Path>, current: Option<&Path>) -> Option { installed .filter(|path| path.exists()) - .map(normalize_bin_path) + .map(tracedecay_domain::forward_slash_path) .or_else(|| current_tracedecay_exe_from(current)) } @@ -750,7 +747,7 @@ mod tests { use super::{ RefreshPolicy, ReinstallOutcome, current_tracedecay_exe_from, - host_owns_canonical_component_set, install_pass_covers_tracked_agents, normalize_bin_path, + host_owns_canonical_component_set, install_pass_covers_tracked_agents, partition_reinstall_results, post_update_binary, post_update_binary_from, prepare_post_update_lease, refresh_generated_plugins_at, restart_daemon_service_with, run_install_then_refresh, @@ -1292,7 +1289,7 @@ mod tests { let resolved = post_update_binary(Some(&installed)).expect("installed path should resolve"); - assert_eq!(resolved, normalize_bin_path(&installed)); + assert_eq!(resolved, tracedecay_domain::forward_slash_path(&installed)); } #[test] diff --git a/crates/tracedecay-code-index/src/source_walk.rs b/crates/tracedecay-code-index/src/source_walk.rs index 0427aca2dd..ea8499ae64 100644 --- a/crates/tracedecay-code-index/src/source_walk.rs +++ b/crates/tracedecay-code-index/src/source_walk.rs @@ -142,7 +142,7 @@ pub fn source_walk(project_root: &Path, path_glob: Option<&str>) -> Result Arc { - relative.to_string_lossy().replace('\\', "/").into() + tracedecay_domain::forward_slash_path(relative).into() } fn build_overrides( diff --git a/crates/tracedecay-code-index/src/unmounted_files.rs b/crates/tracedecay-code-index/src/unmounted_files.rs index dbd9431398..ebd362be2e 100644 --- a/crates/tracedecay-code-index/src/unmounted_files.rs +++ b/crates/tracedecay-code-index/src/unmounted_files.rs @@ -211,10 +211,7 @@ impl ProjectFiles { /// Project-relative, forward-slashed rendering of `path`. pub(super) fn relative_display(root: &Path, path: &Path) -> String { - path.strip_prefix(root) - .unwrap_or(path) - .to_string_lossy() - .replace('\\', "/") + tracedecay_domain::forward_slash_path(path.strip_prefix(root).unwrap_or(path)) } /// Lexical `.`/`..` normalization. diff --git a/crates/tracedecay-domain/src/lib.rs b/crates/tracedecay-domain/src/lib.rs index 8114ded377..f746d5c7af 100644 --- a/crates/tracedecay-domain/src/lib.rs +++ b/crates/tracedecay-domain/src/lib.rs @@ -382,7 +382,9 @@ pub use session_derived::{ SessionDerivedEvidenceRecordV1, derive_session_evidence_from_occurrences, }; pub use source_path_policy::{GENERATED_DIR_SEGMENTS, is_generated_dir_segment}; -pub use text::{collapse_whitespace, utf8_prefix_at_or_before}; +pub use text::{ + collapse_whitespace, forward_slash_path, forward_slash_text, utf8_prefix_at_or_before, +}; pub use work::{RuntimeEvidenceRef, WorkAuthority, WorkContractError, WorkVersion}; pub use work_duplicate_adjudication::{ MAX_WORK_DUPLICATE_REASON_BYTES_V1, WorkDuplicateAdjudicationCommandV1, diff --git a/crates/tracedecay-domain/src/text.rs b/crates/tracedecay-domain/src/text.rs index 1b688b5b85..d6c23705ee 100644 --- a/crates/tracedecay-domain/src/text.rs +++ b/crates/tracedecay-domain/src/text.rs @@ -6,6 +6,26 @@ //! stays empty. Callers that trim, mark truncation, or refuse a mid-character //! budget still do that themselves. +/// Replace `\` with `/`. +/// +/// Trailing separators stay. `\` becomes `/`, `foo\` becomes `foo/`, and +/// `foo/` is unchanged. An empty string stays empty. This does not trim, +/// lowercase a drive letter, or strip a `\\?\` prefix. +#[must_use] +pub fn forward_slash_text(text: &str) -> String { + text.replace('\\', "/") +} + +/// [`forward_slash_text`] of a path's lossy display form. +/// +/// A trailing separator on the path is kept. A non-UTF-8 component is the +/// usual `U+FFFD` replacement, the same spelling `to_string_lossy` already +/// produced at the deleted call sites. +#[must_use] +pub fn forward_slash_path(path: &std::path::Path) -> String { + forward_slash_text(&path.to_string_lossy()) +} + /// Join Unicode whitespace-separated pieces with a single ASCII space. /// /// Leading, trailing, and repeated whitespace disappear. An empty or @@ -29,7 +49,21 @@ pub fn utf8_prefix_at_or_before(text: &str, max_bytes: usize) -> &str { #[cfg(test)] mod tests { - use super::{collapse_whitespace, utf8_prefix_at_or_before}; + use super::{collapse_whitespace, forward_slash_text, utf8_prefix_at_or_before}; + + #[test] + fn forward_slashes_keep_trailing_separators_and_an_empty_string() { + assert_eq!(forward_slash_text(""), ""); + assert_eq!(forward_slash_text(r"\"), "/"); + assert_eq!(forward_slash_text(r"foo\"), "foo/"); + assert_eq!(forward_slash_text("foo/"), "foo/"); + assert_eq!(forward_slash_text(r"C:\repo\\"), "C:/repo//"); + assert_eq!(forward_slash_text(r"\\?\C:\repo"), "//?/C:/repo"); + assert_eq!( + super::forward_slash_path(std::path::Path::new(r"foo\bar\")), + "foo/bar/" + ); + } #[test] fn collapse_whitespace_keeps_non_space_and_drops_only_whitespace() { diff --git a/crates/tracedecay-global-db/src/registry_maintenance/orphan.rs b/crates/tracedecay-global-db/src/registry_maintenance/orphan.rs index 7ac78ac5b4..82fef14cab 100644 --- a/crates/tracedecay-global-db/src/registry_maintenance/orphan.rs +++ b/crates/tracedecay-global-db/src/registry_maintenance/orphan.rs @@ -165,8 +165,8 @@ fn inspect_registry_orphan_manifest_inner( project_id, store_kind: "code_project".to_string(), storage_mode: "profile_sharded".to_string(), - store_relpath: path_string(&store_relpath), - manifest_relpath: Some(path_string(&manifest_relpath)), + store_relpath: tracedecay_domain::forward_slash_path(&store_relpath), + manifest_relpath: Some(tracedecay_domain::forward_slash_path(&manifest_relpath)), last_verified_at: Some(verified_at), last_write_at: None, }, @@ -392,7 +392,7 @@ fn reconstruct_graph_scopes( project_id: project_id.to_string(), store_id: store_id.to_string(), branch_name: branch_name.clone(), - db_relpath: path_string(&profile_db_relpath), + db_relpath: tracedecay_domain::forward_slash_path(&profile_db_relpath), parent_scope_id: entry .parent .as_ref() @@ -427,7 +427,7 @@ fn push_artifact_if_present( artifacts.push(StoreArtifactUpsert { store_id: store_id.to_string(), artifact_kind: artifact_kind.to_string(), - relpath: path_string(&relpath), + relpath: tracedecay_domain::forward_slash_path(&relpath), size_bytes: i64::try_from(meta.len()).ok(), schema_version, updated_at: Some(updated_at), @@ -447,7 +447,3 @@ fn is_safe_relpath(path: &Path) -> bool { .components() .all(|component| matches!(component, Component::Normal(_))) } - -fn path_string(path: &Path) -> String { - path.to_string_lossy().replace('\\', "/") -} diff --git a/crates/tracedecay-runtime-core/src/storage/paths_and_io.rs b/crates/tracedecay-runtime-core/src/storage/paths_and_io.rs index 79dfe20921..77f46b52e1 100644 --- a/crates/tracedecay-runtime-core/src/storage/paths_and_io.rs +++ b/crates/tracedecay-runtime-core/src/storage/paths_and_io.rs @@ -97,7 +97,7 @@ impl ProjectPath { } pub fn relative_path_string(&self) -> String { - self.relative_path.to_string_lossy().replace('\\', "/") + tracedecay_domain::forward_slash_path(&self.relative_path) } } From 59b083278bc6cbdb098adf06917e9467a5f5e61b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:45:05 +0000 Subject: [PATCH 151/182] test(fixtures): share TryFrom fixture identities The same canonical-id parse was copied through suites. Call sites keep the id() name and now resolve one helper. Co-authored-by: Zack Jackson --- .../src/agents/context_scout/evidence.rs | 8 +------ .../src/agents/context_scout/ports.rs | 7 +------ .../tests/context_scout_evidence.rs | 8 +------ crates/tracedecay-api/src/remote_tests.rs | 8 +------ .../src/diagnostics_publication.rs | 8 +------ .../src/diagnostics_query.rs | 8 +------ .../src/diagnostics_store.rs | 8 +------ .../src/feedback/concrete.rs | 7 +------ .../src/feedback/observations.rs | 8 +------ .../lsp_runtime/diagnostic_admission_tests.rs | 8 +------ .../lsp_runtime/projection_identity_tests.rs | 8 +------ .../production/managed_test_scope_tests.rs | 8 +------ .../symbol_graph_implementation_tests.rs | 8 +------ .../src/work/work_evidence_retrieval.rs | 8 +------ .../tests/main/bash.rs | 8 +------ .../tests/main/incremental_parse.rs | 8 +------ .../tests/main/metal.rs | 8 +------ .../code_index_scheduler/tests/reconcile.rs | 8 +------ .../src/git_transactions/test_support.rs | 8 +------ crates/tracedecay-code-index/src/chunks.rs | 8 +------ .../tracedecay-code-index/src/generations.rs | 8 +------ .../src/graph_projection/interactive/tests.rs | 8 +------ .../src/retained_parse.rs | 8 +------ .../code_index_suite/chunk_incremental.rs | 9 +------- .../tests/code_index_suite/generations.rs | 9 +------- .../tests/code_index_suite/lineage.rs | 8 +------ .../code_index_suite/projection_receipts.rs | 8 +------ .../tests/code_index_suite/support.rs | 10 +-------- .../src/config/analyzer.rs | 8 +------ .../src/config/scope_control.rs | 8 +------ .../src/config/topology.rs | 8 +------ .../src/configuration/authorization.rs | 8 +------ .../src/configuration/operations.rs | 8 +------ crates/tracedecay-contracts/src/git/tests.rs | 8 +------ .../src/remote/credential_admission.rs | 8 +------ .../src/retained_surfaces/service.rs | 8 +------ .../src/work_duplicate_adjudication.rs | 8 +------ .../src/work_evidence/tests.rs | 8 +------ .../tests/contracts_suite/common/mod.rs | 9 +------- .../execution_topology_metrics.rs | 8 +------ .../execution_topology_rollup.rs | 8 +------ .../tests/contracts_suite/multi_root_query.rs | 9 +------- .../contracts_suite/multi_root_scope_set.rs | 9 +------- .../contracts_suite/policy_composition.rs | 8 +------ .../work_artifact_hydration_service.rs | 8 +------ .../contracts_suite/work_placement_service.rs | 8 +------ .../work_product_application.rs | 8 +------ .../contracts_suite/work_proposal_planner.rs | 8 +------ .../work_run_control_service.rs | 8 +------ .../contracts_suite/work_synthesis_service.rs | 8 +------ .../contracts_suite/work_topology_view.rs | 8 +------ .../contracts_suite/workflow_coordination.rs | 8 +------ .../contracts_suite/workflow_dag_execution.rs | 8 +------ .../workflow_fan_out_census.rs | 8 +------ .../workflow_provider_registry.rs | 8 +------ .../tests/contracts_suite/workflow_runtime.rs | 8 +------ .../src/context_scout_lifecycle/tests.rs | 7 +------ .../src/invocation/work_attempt_exec/tests.rs | 8 +------ .../src/query_mcp_admission.rs | 8 +------ .../src/code_intelligence/index.rs | 8 +------ .../src/code_intelligence/language.rs | 8 +------ .../src/code_intelligence/search.rs | 8 +------ crates/tracedecay-domain/src/diagnostics.rs | 8 +------ crates/tracedecay-domain/src/feedback/mod.rs | 8 +------ .../src/memory/fact_tests.rs | 4 +--- .../tracedecay-domain/src/memory/lineage.rs | 7 +------ .../tracedecay-domain/src/memory/relation.rs | 7 +------ crates/tracedecay-domain/src/repository.rs | 7 +------ crates/tracedecay-domain/src/research/mod.rs | 7 +------ crates/tracedecay-domain/src/retrieval.rs | 8 +------ crates/tracedecay-domain/src/test_fixtures.rs | 21 +++++++++++++++++-- .../domain_suite/code_search_contract.rs | 9 +------- .../domain_suite/configuration_contract.rs | 8 +------ .../tests/domain_suite/feedback_contract.rs | 8 +------ .../git_index_transaction_contract.rs | 8 +------ .../git_topology_anchor_contract.rs | 8 +------ .../domain_suite/repository_state_contract.rs | 8 +------ .../work_duplicate_adjudication_contract.rs | 8 +------ .../work_execution_snapshot_contract.rs | 8 +------ .../domain_suite/work_product_contract.rs | 8 +------ .../domain_suite/work_runtime_contract.rs | 8 +------ .../workflow_definition_contract.rs | 8 +------ .../src/configuration/resolver.rs | 8 +------ .../src/configuration/store/tests/mod.rs | 8 +------ .../src/git_index_transactions/tests.rs | 8 +------ .../tests/session_relation_graph.rs | 8 +------ .../src/context/read_modes.rs | 8 +------ crates/tracedecay-policy/src/configuration.rs | 8 +------ .../tests/policy_suite/routing_admission.rs | 8 +------ .../src/project/lifecycle/mod.rs | 7 +------ .../tracedecay-domain/src/repository.rs | 7 +------ .../src/retrieval/diversity.rs | 8 +------ .../src/retrieval/exact/tests.rs | 8 +------ .../tracedecay-query/src/retrieval/fusion.rs | 8 +------ .../src/retrieval/graph/tests.rs | 8 +------ .../src/retrieval/lexical/routes/tests.rs | 8 +------ .../src/retrieval/lexical/tests.rs | 8 +------ .../tracedecay-query/src/retrieval/tests.rs | 8 +------ .../tests/canonical_execution_equivalence.rs | 8 +------ .../tests/retrieval_contract_spine.rs | 8 +------ .../candidate_producers.rs | 8 +------ .../src/shard_runtime/registry/close.rs | 8 +------ .../src/shard_runtime/registry/destructive.rs | 8 +------ .../registry/retirement/tests.rs | 8 +------ .../shard_runtime/registry/tests/support.rs | 8 +------ .../src/shard_runtime/shard.rs | 8 +------ .../src/shard_runtime/telemetry.rs | 8 +------ .../src/checkpoint/tests.rs | 8 +------ .../src/watermark/tests.rs | 8 +------ .../rusqlite_suite/handoff_open_storage.rs | 8 +------ .../rusqlite_suite/multi_root_scope_set.rs | 9 +------- .../rusqlite_suite/repository_attachment.rs | 10 +-------- .../rusqlite_suite/work_attempt_storage.rs | 8 +------ .../work_duplicate_adjudication_storage.rs | 8 +------ .../work_leak_adjudication_storage.rs | 8 +------ .../rusqlite_suite/work_placement_storage.rs | 8 +------ .../work_product_graph_authority.rs | 8 +------ .../work_product_query_authority.rs | 8 +------ .../work_run_control_storage.rs | 8 +------ .../workflow_fan_out_census_storage.rs | 8 +------ .../workflow_run_journal_storage.rs | 8 +------ .../workflow_runtime_storage.rs | 8 +------ .../src/memory/tests.rs | 7 +------ .../src/relations/projection_read.rs | 8 +------ .../src/store_locator_resolver.rs | 8 +------ crates/tracedecay-store/src/memory/tests.rs | 7 +------ .../tracedecay-store/src/runtime/identity.rs | 8 +------ .../store_suite/configuration_contract.rs | 8 +------ .../tests/store_suite/diagnostics_contract.rs | 8 +------ .../store_suite/storage_runtime_contract.rs | 8 +------ .../work_evidence_journey_tests.rs | 8 +------ .../src/daemon/tests/code_index_hydration.rs | 8 +------ .../advanced_workflow_journey_test.rs | 8 +------ .../daemon_suite/workflow_handoff_test.rs | 8 +------ .../api_application_parity.rs | 8 +------ .../daemon_runtime_acceptance.rs | 8 +------ .../session_suite/anchor_tombstone_expiry.rs | 7 +------ .../fact_merge_hydration_test.rs | 7 +------ 138 files changed, 156 insertions(+), 955 deletions(-) diff --git a/crates/tracedecay-agent-hosts/src/agents/context_scout/evidence.rs b/crates/tracedecay-agent-hosts/src/agents/context_scout/evidence.rs index b6c21810b4..737dff0c50 100644 --- a/crates/tracedecay-agent-hosts/src/agents/context_scout/evidence.rs +++ b/crates/tracedecay-agent-hosts/src/agents/context_scout/evidence.rs @@ -341,14 +341,8 @@ pub(super) fn fixture_context_scout_evidence() -> ContextScoutEvidenceEnvelopeV1 CommitId, ComponentVersion, ProjectId, RefId, RepositoryId, TemporalModeV1, WorktreeId, }; - fn id(value: &str) -> T - where - T: TryFrom, - T::Error: std::fmt::Debug, - { - T::try_from(value.to_owned()).unwrap() - } use tracedecay_domain::test_fixtures::digest; + use tracedecay_domain::test_fixtures::id; let authorized_scope = ResolvedScope::new( id::("project.scout"), diff --git a/crates/tracedecay-agent-hosts/src/agents/context_scout/ports.rs b/crates/tracedecay-agent-hosts/src/agents/context_scout/ports.rs index 0bc81ca056..296a1a7979 100644 --- a/crates/tracedecay-agent-hosts/src/agents/context_scout/ports.rs +++ b/crates/tracedecay-agent-hosts/src/agents/context_scout/ports.rs @@ -1165,12 +1165,7 @@ mod tests { (temporary, database) } - fn id>(value: &str) -> T - where - T::Error: std::fmt::Debug, - { - T::try_from(value.to_owned()).unwrap() - } + use tracedecay_domain::test_fixtures::id; fn configuration( revision: &str, diff --git a/crates/tracedecay-agent-hosts/tests/context_scout_evidence.rs b/crates/tracedecay-agent-hosts/tests/context_scout_evidence.rs index bd97e1f26d..8e9875b064 100644 --- a/crates/tracedecay-agent-hosts/tests/context_scout_evidence.rs +++ b/crates/tracedecay-agent-hosts/tests/context_scout_evidence.rs @@ -32,13 +32,7 @@ use tracedecay_domain::{ RetrievalAnchorId, SourceSpan, TemporalModeV1, UtcMicros, WorktreeId, }; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-api/src/remote_tests.rs b/crates/tracedecay-api/src/remote_tests.rs index c89054ef6f..ac8399315d 100644 --- a/crates/tracedecay-api/src/remote_tests.rs +++ b/crates/tracedecay-api/src/remote_tests.rs @@ -184,13 +184,7 @@ const fn fixed_remote_clock() -> UtcMicros { const ACTIVE_CREDENTIAL: &[u8; 32] = b"0123456789abcdef0123456789abcdef"; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; fn repository_scope(snapshot_id: &str) -> RemoteRepositoryScopeV1 { RemoteRepositoryScopeV1 { diff --git a/crates/tracedecay-application/src/diagnostics_publication.rs b/crates/tracedecay-application/src/diagnostics_publication.rs index 09416efb3e..02fb885f69 100644 --- a/crates/tracedecay-application/src/diagnostics_publication.rs +++ b/crates/tracedecay-application/src/diagnostics_publication.rs @@ -937,13 +937,7 @@ pub fn bounded_notice(message: &str) -> String { mod tests { use super::*; - fn id(value: &str) -> T - where - T: TryFrom, - >::Error: std::fmt::Debug, - { - T::try_from(value.to_owned()).expect("valid fixture identity") - } + use tracedecay_domain::test_fixtures::id; fn digest(byte: char) -> String { format!("sha256:{}", byte.to_string().repeat(64)) diff --git a/crates/tracedecay-application/src/diagnostics_query.rs b/crates/tracedecay-application/src/diagnostics_query.rs index 44a08bd183..71c147a988 100644 --- a/crates/tracedecay-application/src/diagnostics_query.rs +++ b/crates/tracedecay-application/src/diagnostics_query.rs @@ -910,13 +910,7 @@ mod tests { }; use tracedecay_runtime_core::db::engine::TestConnection; - fn id(value: &str) -> T - where - T: TryFrom, - >::Error: std::fmt::Debug, - { - T::try_from(value.to_owned()).expect("valid fixture identity") - } + use tracedecay_domain::test_fixtures::id; fn digest(byte: char) -> String { format!("sha256:{}", byte.to_string().repeat(64)) diff --git a/crates/tracedecay-application/src/diagnostics_store.rs b/crates/tracedecay-application/src/diagnostics_store.rs index fdd13e07dc..4a6eeaf937 100644 --- a/crates/tracedecay-application/src/diagnostics_store.rs +++ b/crates/tracedecay-application/src/diagnostics_store.rs @@ -1618,13 +1618,7 @@ fn db_message(operation: &str, message: impl Into) -> TraceDecayError { mod tests { use super::*; - fn id(value: &str) -> T - where - T: TryFrom, - >::Error: std::fmt::Debug, - { - T::try_from(value.to_owned()).expect("valid fixture identity") - } + use tracedecay_domain::test_fixtures::id; fn digest(byte: char) -> String { format!("sha256:{}", byte.to_string().repeat(64)) diff --git a/crates/tracedecay-application/src/feedback/concrete.rs b/crates/tracedecay-application/src/feedback/concrete.rs index 65c1c0cd93..7545d4225e 100644 --- a/crates/tracedecay-application/src/feedback/concrete.rs +++ b/crates/tracedecay-application/src/feedback/concrete.rs @@ -2028,12 +2028,7 @@ mod tests { }; use tracedecay_runtime_core::db::{DatabaseAuthority, TestDatabaseRuntimeMode}; - fn id>(value: &str) -> T - where - T::Error: std::fmt::Debug, - { - T::try_from(value.to_owned()).unwrap() - } + use tracedecay_domain::test_fixtures::id; fn scope() -> ResolvedScope { ResolvedScope::new( diff --git a/crates/tracedecay-application/src/feedback/observations.rs b/crates/tracedecay-application/src/feedback/observations.rs index 5ea21c1d7e..5a0be7ab84 100644 --- a/crates/tracedecay-application/src/feedback/observations.rs +++ b/crates/tracedecay-application/src/feedback/observations.rs @@ -854,13 +854,7 @@ mod tests { const SHA256_B: &str = "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; - fn id(value: &str) -> T - where - T: TryFrom, - >::Error: std::fmt::Debug, - { - T::try_from(value.to_owned()).unwrap() - } + use tracedecay_domain::test_fixtures::id; fn digest(value: &str) -> ManifestDigest { ManifestDigest::new(value).unwrap() diff --git a/crates/tracedecay-application/src/lsp_runtime/diagnostic_admission_tests.rs b/crates/tracedecay-application/src/lsp_runtime/diagnostic_admission_tests.rs index 929ae5a877..7b14e1ca3d 100644 --- a/crates/tracedecay-application/src/lsp_runtime/diagnostic_admission_tests.rs +++ b/crates/tracedecay-application/src/lsp_runtime/diagnostic_admission_tests.rs @@ -8,13 +8,7 @@ use tracedecay_domain::{ FileOccurrenceId, GenerationDiagnosticV1, SourceSpan, UtcMicros, }; -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).expect("valid fixture identity") -} +use tracedecay_domain::test_fixtures::id; fn digest(byte: char) -> String { format!("sha256:{}", byte.to_string().repeat(64)) diff --git a/crates/tracedecay-application/src/lsp_runtime/projection_identity_tests.rs b/crates/tracedecay-application/src/lsp_runtime/projection_identity_tests.rs index 575b69c311..d1b7c36489 100644 --- a/crates/tracedecay-application/src/lsp_runtime/projection_identity_tests.rs +++ b/crates/tracedecay-application/src/lsp_runtime/projection_identity_tests.rs @@ -5,13 +5,7 @@ use tracedecay_domain::{ RepositoryId, WorktreeId, }; -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).expect("valid fixture identity") -} +use tracedecay_domain::test_fixtures::id; fn digest(byte: char) -> String { format!("sha256:{}", byte.to_string().repeat(64)) } diff --git a/crates/tracedecay-application/src/primitives/production/managed_test_scope_tests.rs b/crates/tracedecay-application/src/primitives/production/managed_test_scope_tests.rs index 5430b69cd5..317bccdfc3 100644 --- a/crates/tracedecay-application/src/primitives/production/managed_test_scope_tests.rs +++ b/crates/tracedecay-application/src/primitives/production/managed_test_scope_tests.rs @@ -9,13 +9,7 @@ use tracedecay_lsp::{LspRuntimeFailure, LspRuntimeFuture}; use super::{ManagedTestRunCurrentScopePort, ProductionManagedTestRunCurrentScope}; use crate::lsp_runtime::{LspCodeIndexProjectionIdentity, LspCodeIndexProjectionIdentityPort}; -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).expect("valid fixture identity") -} +use tracedecay_domain::test_fixtures::id; fn digest(byte: char) -> String { format!("sha256:{}", byte.to_string().repeat(64)) diff --git a/crates/tracedecay-application/src/primitives/symbol_graph_implementation_tests.rs b/crates/tracedecay-application/src/primitives/symbol_graph_implementation_tests.rs index 4cd4ad1f91..5112d5a920 100644 --- a/crates/tracedecay-application/src/primitives/symbol_graph_implementation_tests.rs +++ b/crates/tracedecay-application/src/primitives/symbol_graph_implementation_tests.rs @@ -278,13 +278,7 @@ fn edge(from: &str, to: &str, start: u64) -> CanonicalRelationEdgeV1 { } } -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: Debug, -{ - T::try_from(value.to_owned()).expect("fixture identity") -} +use tracedecay_domain::test_fixtures::id; fn digest(byte: char) -> T where diff --git a/crates/tracedecay-application/src/work/work_evidence_retrieval.rs b/crates/tracedecay-application/src/work/work_evidence_retrieval.rs index abd48b83cf..8f7bc98d9d 100644 --- a/crates/tracedecay-application/src/work/work_evidence_retrieval.rs +++ b/crates/tracedecay-application/src/work/work_evidence_retrieval.rs @@ -718,13 +718,7 @@ pub mod tests { use super::{WorkFederatedQueryAuthorityFutureV1, WorkFederatedQueryAuthorityPortV1}; - pub fn id(value: &str) -> T - where - T: TryFrom, - T::Error: std::fmt::Debug, - { - T::try_from(value.to_owned()).expect("TaskSession fixture identity") - } + pub use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-code-extraction/tests/main/bash.rs b/crates/tracedecay-code-extraction/tests/main/bash.rs index 72534e8269..5ff298ddbe 100644 --- a/crates/tracedecay-code-extraction/tests/main/bash.rs +++ b/crates/tracedecay-code-extraction/tests/main/bash.rs @@ -8,13 +8,7 @@ use tracedecay_code_extraction::parsed_extraction::{ }; use tracedecay_domain::*; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Display, -{ - T::try_from(value.to_owned()).unwrap_or_else(|error| panic!("{value}: {error}")) -} +use tracedecay_domain::test_fixtures::id; fn bash_overlay(version: i64, content: &str) -> ParseDocumentIdentity { ParseDocumentIdentity::SessionOverlay { diff --git a/crates/tracedecay-code-extraction/tests/main/incremental_parse.rs b/crates/tracedecay-code-extraction/tests/main/incremental_parse.rs index 4ed28af5c3..6c200d4e09 100644 --- a/crates/tracedecay-code-extraction/tests/main/incremental_parse.rs +++ b/crates/tracedecay-code-extraction/tests/main/incremental_parse.rs @@ -16,13 +16,7 @@ use tracedecay_domain::{ RepositoryDirtyStateV1, RepositoryId, SourceSpan, TreeId, WorktreeId, }; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Display, -{ - T::try_from(value.to_owned()).unwrap_or_else(|error| panic!("{value}: {error}")) -} +use tracedecay_domain::test_fixtures::id; fn identity(commit: &str, tree: &str, dirty: RepositoryDirtyStateV1) -> ParseDocumentIdentity { identity_in_worktree(commit, tree, dirty, "worktree.incremental") diff --git a/crates/tracedecay-code-extraction/tests/main/metal.rs b/crates/tracedecay-code-extraction/tests/main/metal.rs index de93ed7358..67a58f05ce 100644 --- a/crates/tracedecay-code-extraction/tests/main/metal.rs +++ b/crates/tracedecay-code-extraction/tests/main/metal.rs @@ -55,13 +55,7 @@ fn names(result: &ExtractionResult, kind: NodeKind) -> Vec<&str> { .collect() } -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Display, -{ - T::try_from(value.to_owned()).unwrap_or_else(|error| panic!("{value}: {error}")) -} +use tracedecay_domain::test_fixtures::id; #[test] fn metal_dispatches_on_its_extension() { diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/reconcile.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/reconcile.rs index 7414952744..19980637ab 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/reconcile.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/reconcile.rs @@ -7267,13 +7267,7 @@ async fn compiler_diagnostics_published_under_registry_identity_are_admitted_by_ } } - fn id(value: &str) -> T - where - T: TryFrom, - >::Error: std::fmt::Debug, - { - T::try_from(value.to_owned()).expect("valid fixture identity") - } + use tracedecay_domain::test_fixtures::id; let source = "pub fn alpha() -> u32 {\n let value: u32 = \"nope\";\n value\n}\n"; let fixture = GitFixture::new(&[("src/lib.rs", "pub fn alpha() -> u32 { 1 }\n")]); diff --git a/crates/tracedecay-code-index-runtime/src/git_transactions/test_support.rs b/crates/tracedecay-code-index-runtime/src/git_transactions/test_support.rs index be5965e20a..9a90f217dc 100644 --- a/crates/tracedecay-code-index-runtime/src/git_transactions/test_support.rs +++ b/crates/tracedecay-code-index-runtime/src/git_transactions/test_support.rs @@ -224,13 +224,7 @@ impl GitIndexPolicyRecheckPort for TestPolicy { } } -pub fn id(value: &str) -> T -where - T: TryFrom, - >::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).expect("fixture identity") -} +pub use tracedecay_domain::test_fixtures::id; pub use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-code-index/src/chunks.rs b/crates/tracedecay-code-index/src/chunks.rs index 15fd1f44b0..19960d8de3 100644 --- a/crates/tracedecay-code-index/src/chunks.rs +++ b/crates/tracedecay-code-index/src/chunks.rs @@ -2759,13 +2759,7 @@ mod tests { } } - fn id(value: &str) -> T - where - T: TryFrom, - T::Error: std::fmt::Debug, - { - T::try_from(value.to_owned()).expect("valid fixture identity") - } + use tracedecay_domain::test_fixtures::id; fn digest(byte: char) -> String { format!("sha256:{}", byte.to_string().repeat(64)) diff --git a/crates/tracedecay-code-index/src/generations.rs b/crates/tracedecay-code-index/src/generations.rs index cc5a2a79c5..b40c9d0514 100644 --- a/crates/tracedecay-code-index/src/generations.rs +++ b/crates/tracedecay-code-index/src/generations.rs @@ -758,13 +758,7 @@ mod tests { .expect("valid digest") } - fn id(value: &str) -> T - where - T: TryFrom, - >::Error: std::fmt::Debug, - { - T::try_from(value.to_owned()).expect("valid fixture identity") - } + use tracedecay_domain::test_fixtures::id; fn repository() -> RepositoryId { id("repository.fixture") diff --git a/crates/tracedecay-code-index/src/graph_projection/interactive/tests.rs b/crates/tracedecay-code-index/src/graph_projection/interactive/tests.rs index 1065660fbe..dc01163abe 100644 --- a/crates/tracedecay-code-index/src/graph_projection/interactive/tests.rs +++ b/crates/tracedecay-code-index/src/graph_projection/interactive/tests.rs @@ -47,13 +47,7 @@ impl GraphCancellation for CancelAfter { } } -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: Debug, -{ - T::try_from(value.to_owned()).expect("valid fixture identity") -} +use tracedecay_domain::test_fixtures::id; fn digest(byte: char) -> T where diff --git a/crates/tracedecay-code-index/src/retained_parse.rs b/crates/tracedecay-code-index/src/retained_parse.rs index 5398dc2282..a69b87ec68 100644 --- a/crates/tracedecay-code-index/src/retained_parse.rs +++ b/crates/tracedecay-code-index/src/retained_parse.rs @@ -640,13 +640,7 @@ mod tests { }; use tracedecay_domain::RepositoryDirtyStateV1; - fn id(value: &str) -> T - where - T: TryFrom, - >::Error: std::fmt::Debug, - { - T::try_from(value.to_owned()).expect("valid test identity") - } + use tracedecay_domain::test_fixtures::id; fn identity() -> ParseDocumentIdentity { ParseDocumentIdentity::Repository { diff --git a/crates/tracedecay-code-index/tests/code_index_suite/chunk_incremental.rs b/crates/tracedecay-code-index/tests/code_index_suite/chunk_incremental.rs index 68ea89d660..dfa7b4ea44 100644 --- a/crates/tracedecay-code-index/tests/code_index_suite/chunk_incremental.rs +++ b/crates/tracedecay-code-index/tests/code_index_suite/chunk_incremental.rs @@ -1,4 +1,3 @@ -use std::fmt::Debug; use std::sync::Arc; use tracedecay_code_index::chunks::{CodeFileChunksV1, content_digest}; @@ -20,13 +19,7 @@ use tracedecay_domain::{ SymbolIdentityDigest, SymbolOccurrenceId, }; -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: Debug, -{ - T::try_from(value.to_owned()).expect("valid fixture identity") -} +use tracedecay_domain::test_fixtures::id; fn generation(sequence: u64) -> CodeGenerationId { id(&format!("generation.v1.aaaaaaaa.{sequence:08}")) diff --git a/crates/tracedecay-code-index/tests/code_index_suite/generations.rs b/crates/tracedecay-code-index/tests/code_index_suite/generations.rs index c218f12e7b..fe803b8056 100644 --- a/crates/tracedecay-code-index/tests/code_index_suite/generations.rs +++ b/crates/tracedecay-code-index/tests/code_index_suite/generations.rs @@ -1,5 +1,4 @@ use std::collections::BTreeSet; -use std::fmt::Debug; use tracedecay_code_index::capabilities::expected_seal_digest; use tracedecay_code_index::generations::{ @@ -15,13 +14,7 @@ use tracedecay_domain::{ UtcMicros, canonical_sha256, }; -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: Debug, -{ - T::try_from(value.to_owned()).expect("valid fixture identity") -} +use tracedecay_domain::test_fixtures::id; fn content_digest(byte: char) -> ContentDigest { id(&format!("sha256:{}", byte.to_string().repeat(64))) diff --git a/crates/tracedecay-code-index/tests/code_index_suite/lineage.rs b/crates/tracedecay-code-index/tests/code_index_suite/lineage.rs index 91bb2ee5a7..1f0de1911e 100644 --- a/crates/tracedecay-code-index/tests/code_index_suite/lineage.rs +++ b/crates/tracedecay-code-index/tests/code_index_suite/lineage.rs @@ -10,13 +10,7 @@ use tracedecay_domain::{ SymbolIdentityDigest, SymbolOccurrenceId, }; -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: Debug, -{ - T::try_from(value.to_owned()).expect("valid fixture identity") -} +use tracedecay_domain::test_fixtures::id; fn digest(byte: char) -> T where diff --git a/crates/tracedecay-code-index/tests/code_index_suite/projection_receipts.rs b/crates/tracedecay-code-index/tests/code_index_suite/projection_receipts.rs index db49ec491a..a60729fd61 100644 --- a/crates/tracedecay-code-index/tests/code_index_suite/projection_receipts.rs +++ b/crates/tracedecay-code-index/tests/code_index_suite/projection_receipts.rs @@ -12,13 +12,7 @@ use tracedecay_domain::{ ProjectionKindV1, ProjectionOperationV1, ProjectionOutcomeV1, ProjectionReplayReasonV1, }; -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: Debug, -{ - T::try_from(value.to_owned()).expect("valid fixture identity") -} +use tracedecay_domain::test_fixtures::id; fn digest(byte: char) -> T where diff --git a/crates/tracedecay-code-index/tests/code_index_suite/support.rs b/crates/tracedecay-code-index/tests/code_index_suite/support.rs index e7b74f6e1c..8bb913b670 100644 --- a/crates/tracedecay-code-index/tests/code_index_suite/support.rs +++ b/crates/tracedecay-code-index/tests/code_index_suite/support.rs @@ -1,5 +1,3 @@ -use std::fmt::Debug; - use tracedecay_code_index::chunks::content_digest; use tracedecay_code_index::intake::{CodeIndexIntake, ReceiptBoundCodeFileV1, SanitizedCodeIntake}; use tracedecay_code_index::languages::{LanguageRegistry, StaticLanguageRegistry}; @@ -11,13 +9,7 @@ use tracedecay_domain::{ pub const RUST_SOURCE: &str = "//! Module documentation.\n\nuse std::collections::HashMap;\n\n/// Increment a value.\npub fn alpha(value: u32) -> u32 {\n value + 1\n}\n\npub struct Holder {\n map: HashMap,\n}\n\nimpl Holder {\n pub fn get(&self, key: u32) -> Option {\n self.map.get(&key).copied()\n }\n}\n\n// trailing window text\n"; -pub fn id(value: &str) -> T -where - T: TryFrom, - >::Error: Debug, -{ - T::try_from(value.to_owned()).expect("valid fixture identity") -} +pub use tracedecay_domain::test_fixtures::id; pub use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-configuration/src/config/analyzer.rs b/crates/tracedecay-configuration/src/config/analyzer.rs index f96df691a6..de8c5cf885 100644 --- a/crates/tracedecay-configuration/src/config/analyzer.rs +++ b/crates/tracedecay-configuration/src/config/analyzer.rs @@ -79,13 +79,7 @@ mod tests { use super::*; - fn id(value: &str) -> T - where - T: TryFrom, - >::Error: std::fmt::Debug, - { - T::try_from(value.to_owned()).expect("fixture id is canonical") - } + use tracedecay_domain::test_fixtures::id; fn analyzer_key() -> SettingKey { SettingKey::new(ANALYZER_SETTINGS_SETTING_KEY).unwrap() diff --git a/crates/tracedecay-configuration/src/config/scope_control.rs b/crates/tracedecay-configuration/src/config/scope_control.rs index 967128d6d6..cbea9f5902 100644 --- a/crates/tracedecay-configuration/src/config/scope_control.rs +++ b/crates/tracedecay-configuration/src/config/scope_control.rs @@ -157,13 +157,7 @@ mod tests { }; use tracedecay_domain::{LocatorDigest, ProjectId}; - fn id(value: &str) -> T - where - T: TryFrom, - >::Error: std::fmt::Debug, - { - T::try_from(value.to_owned()).unwrap() - } + use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-configuration/src/config/topology.rs b/crates/tracedecay-configuration/src/config/topology.rs index 8196a7ae28..804f5e15f2 100644 --- a/crates/tracedecay-configuration/src/config/topology.rs +++ b/crates/tracedecay-configuration/src/config/topology.rs @@ -63,13 +63,7 @@ mod tests { use super::*; - fn id(value: &str) -> T - where - T: TryFrom, - >::Error: std::fmt::Debug, - { - T::try_from(value.to_owned()).expect("fixture id is canonical") - } + use tracedecay_domain::test_fixtures::id; fn topology_key() -> SettingKey { SettingKey::new(WORK_TOPOLOGY_POLICY_SETTING_KEY).unwrap() diff --git a/crates/tracedecay-configuration/src/configuration/authorization.rs b/crates/tracedecay-configuration/src/configuration/authorization.rs index 2707559795..cb5d69ca0f 100644 --- a/crates/tracedecay-configuration/src/configuration/authorization.rs +++ b/crates/tracedecay-configuration/src/configuration/authorization.rs @@ -155,13 +155,7 @@ mod tests { } } - fn id(value: &str) -> T - where - T: TryFrom, - >::Error: std::fmt::Debug, - { - T::try_from(value.to_owned()).unwrap() - } + use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-configuration/src/configuration/operations.rs b/crates/tracedecay-configuration/src/configuration/operations.rs index 7de552cf6d..398b746c40 100644 --- a/crates/tracedecay-configuration/src/configuration/operations.rs +++ b/crates/tracedecay-configuration/src/configuration/operations.rs @@ -627,13 +627,7 @@ mod tests { AccessPolicyDigest::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() } - fn id(value: &str) -> T - where - T: TryFrom, - >::Error: std::fmt::Debug, - { - T::try_from(value.to_owned()).unwrap() - } + use tracedecay_domain::test_fixtures::id; struct Store { current: ConfigurationCurrentStateV1, diff --git a/crates/tracedecay-contracts/src/git/tests.rs b/crates/tracedecay-contracts/src/git/tests.rs index 8fcf5589cf..e43e4d51b7 100644 --- a/crates/tracedecay-contracts/src/git/tests.rs +++ b/crates/tracedecay-contracts/src/git/tests.rs @@ -21,13 +21,7 @@ use crate::{ RequestContext, RequestId, ResolvedScope, }; -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).expect("fixture id") -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-contracts/src/remote/credential_admission.rs b/crates/tracedecay-contracts/src/remote/credential_admission.rs index 1810c1a0e4..0aae85b18f 100644 --- a/crates/tracedecay-contracts/src/remote/credential_admission.rs +++ b/crates/tracedecay-contracts/src/remote/credential_admission.rs @@ -745,13 +745,7 @@ mod tests { } } - fn id(value: &str) -> T - where - T: TryFrom, - T::Error: std::fmt::Debug, - { - T::try_from(value.to_owned()).unwrap() - } + use tracedecay_domain::test_fixtures::id; fn scope() -> RemoteRepositoryScopeV1 { RemoteRepositoryScopeV1 { diff --git a/crates/tracedecay-contracts/src/retained_surfaces/service.rs b/crates/tracedecay-contracts/src/retained_surfaces/service.rs index 187acf3c5f..6e73974de5 100644 --- a/crates/tracedecay-contracts/src/retained_surfaces/service.rs +++ b/crates/tracedecay-contracts/src/retained_surfaces/service.rs @@ -799,13 +799,7 @@ mod tests { } } - fn id(value: &str) -> T - where - T: TryFrom, - T::Error: std::fmt::Debug, - { - T::try_from(value.to_owned()).expect("fixture identity is valid") - } + use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-contracts/src/work_duplicate_adjudication.rs b/crates/tracedecay-contracts/src/work_duplicate_adjudication.rs index d96b988eb4..fb1355e2ce 100644 --- a/crates/tracedecay-contracts/src/work_duplicate_adjudication.rs +++ b/crates/tracedecay-contracts/src/work_duplicate_adjudication.rs @@ -465,13 +465,7 @@ mod tests { WorkTopologyGenerationRefV1, }; - fn id(value: &str) -> T - where - T: TryFrom, - T::Error: std::fmt::Debug, - { - T::try_from(value.to_owned()).unwrap() - } + use tracedecay_domain::test_fixtures::id; fn attempt(name: &str) -> WorkAttemptIdentityV1 { WorkAttemptIdentityV1::new( diff --git a/crates/tracedecay-contracts/src/work_evidence/tests.rs b/crates/tracedecay-contracts/src/work_evidence/tests.rs index da52035fdf..60972a9319 100644 --- a/crates/tracedecay-contracts/src/work_evidence/tests.rs +++ b/crates/tracedecay-contracts/src/work_evidence/tests.rs @@ -22,13 +22,7 @@ use crate::{ DisclosureClass, RequestId, ResolvedScope, WorkAttemptProviderOutcomeV1, }; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-contracts/tests/contracts_suite/common/mod.rs b/crates/tracedecay-contracts/tests/contracts_suite/common/mod.rs index dfe55961f3..1f70b5b5a9 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/common/mod.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/common/mod.rs @@ -4,7 +4,6 @@ mod work_product_attempt_support; use std::cell::RefCell; use std::collections::{BTreeMap, BTreeSet, VecDeque}; -use std::fmt; use std::sync::{Arc, Mutex}; use tracedecay_contracts::{ @@ -57,13 +56,7 @@ pub const SHA256_B: &str = const SOURCE_AUTHORIZATION_TRUTH_TABLES: &str = include_str!("../../../../tracedecay-policy/tests/fixtures/source_authorization/core.json"); -pub fn id(value: &str) -> T -where - T: TryFrom, - >::Error: fmt::Debug, -{ - T::try_from(value.to_owned()).expect("fixture identity is canonical") -} +pub use tracedecay_domain::test_fixtures::id; pub fn digest(value: &str) -> ManifestDigest { ManifestDigest::new(value).expect("fixture digest is canonical") diff --git a/crates/tracedecay-contracts/tests/contracts_suite/execution_topology_metrics.rs b/crates/tracedecay-contracts/tests/contracts_suite/execution_topology_metrics.rs index 1744e523ca..eefca511f0 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/execution_topology_metrics.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/execution_topology_metrics.rs @@ -31,13 +31,7 @@ use tracedecay_domain::{ }; use tracedecay_tool_catalog::{CapabilityId, UseCaseId}; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; fn context() -> RequestContext { context_with( diff --git a/crates/tracedecay-contracts/tests/contracts_suite/execution_topology_rollup.rs b/crates/tracedecay-contracts/tests/contracts_suite/execution_topology_rollup.rs index b07f96a820..4878b6b2eb 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/execution_topology_rollup.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/execution_topology_rollup.rs @@ -31,13 +31,7 @@ mod stack_drift; const DAY_MICROS: i64 = 86_400_000_000; const SCOPE: &str = "project.execution-topology-rollup"; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; fn rollup_context() -> RequestContext { let scope = ResolvedScope::new( id::(SCOPE), diff --git a/crates/tracedecay-contracts/tests/contracts_suite/multi_root_query.rs b/crates/tracedecay-contracts/tests/contracts_suite/multi_root_query.rs index e1fb25b682..ad3993a764 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/multi_root_query.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/multi_root_query.rs @@ -1,5 +1,4 @@ use std::collections::BTreeSet; -use std::fmt; use std::sync::Mutex; use schemars::schema_for; @@ -19,13 +18,7 @@ use tracedecay_tool_catalog::{CapabilityId, UseCaseId}; const CAPABILITY: &str = "capability.multi-root.query"; const USE_CASE: &str = "use-case.multi-root.query"; -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-contracts/tests/contracts_suite/multi_root_scope_set.rs b/crates/tracedecay-contracts/tests/contracts_suite/multi_root_scope_set.rs index dd0a17ff7c..d876d9d366 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/multi_root_scope_set.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/multi_root_scope_set.rs @@ -1,7 +1,6 @@ use crate::common; use std::collections::BTreeSet; -use std::fmt; use serde_json::json; use tracedecay_contracts::{ @@ -19,13 +18,7 @@ use tracedecay_tool_catalog::{CapabilityId, UseCaseId}; const CAPABILITY: &str = "capability.multi-root.query"; const USE_CASE: &str = "use-case.multi-root.query"; -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-contracts/tests/contracts_suite/policy_composition.rs b/crates/tracedecay-contracts/tests/contracts_suite/policy_composition.rs index 7a9cbcf53c..8a5f662f36 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/policy_composition.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/policy_composition.rs @@ -17,13 +17,7 @@ use tracedecay_policy::routing::{ }; use tracedecay_tool_catalog::{CapabilityId, UseCaseId}; -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-contracts/tests/contracts_suite/work_artifact_hydration_service.rs b/crates/tracedecay-contracts/tests/contracts_suite/work_artifact_hydration_service.rs index 44669bfe43..2982fabf72 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/work_artifact_hydration_service.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/work_artifact_hydration_service.rs @@ -20,13 +20,7 @@ use tracedecay_domain::{ }; use tracedecay_tool_catalog::{CapabilityId, UseCaseId}; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-contracts/tests/contracts_suite/work_placement_service.rs b/crates/tracedecay-contracts/tests/contracts_suite/work_placement_service.rs index 9df8f07264..d8c744b159 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/work_placement_service.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/work_placement_service.rs @@ -31,13 +31,7 @@ use tracedecay_domain::{ }; use tracedecay_tool_catalog::{CapabilityId, UseCaseId}; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-contracts/tests/contracts_suite/work_product_application.rs b/crates/tracedecay-contracts/tests/contracts_suite/work_product_application.rs index d95e5cedf8..674b40c01b 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/work_product_application.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/work_product_application.rs @@ -35,13 +35,7 @@ use tracedecay_domain::{ }; use tracedecay_tool_catalog::{CapabilityId, UseCaseId}; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-contracts/tests/contracts_suite/work_proposal_planner.rs b/crates/tracedecay-contracts/tests/contracts_suite/work_proposal_planner.rs index 64a17bada9..17efabfdc4 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/work_proposal_planner.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/work_proposal_planner.rs @@ -44,13 +44,7 @@ const CREATED_AT: UtcMicros = UtcMicros(10); /// incomparable and never enter the calibration cohort. const EVALUATED_AT: UtcMicros = UtcMicros(50); -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-contracts/tests/contracts_suite/work_run_control_service.rs b/crates/tracedecay-contracts/tests/contracts_suite/work_run_control_service.rs index afde1bc710..622fae4494 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/work_run_control_service.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/work_run_control_service.rs @@ -32,13 +32,7 @@ use tracedecay_tool_catalog::{CapabilityId, UseCaseId}; const ADMITTED_DEADLINE: UtcMicros = UtcMicros(10_000); -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-contracts/tests/contracts_suite/work_synthesis_service.rs b/crates/tracedecay-contracts/tests/contracts_suite/work_synthesis_service.rs index 25af595912..dfe050a65c 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/work_synthesis_service.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/work_synthesis_service.rs @@ -34,13 +34,7 @@ use tracedecay_domain::{ }; use tracedecay_tool_catalog::{CapabilityId, UseCaseId}; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-contracts/tests/contracts_suite/work_topology_view.rs b/crates/tracedecay-contracts/tests/contracts_suite/work_topology_view.rs index 80cd27d79a..c47efc2084 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/work_topology_view.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/work_topology_view.rs @@ -32,13 +32,7 @@ use tracedecay_domain::{ }; use tracedecay_tool_catalog::{CapabilityId, UseCaseId}; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-contracts/tests/contracts_suite/workflow_coordination.rs b/crates/tracedecay-contracts/tests/contracts_suite/workflow_coordination.rs index f05ccb9001..51a42d1616 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/workflow_coordination.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/workflow_coordination.rs @@ -20,13 +20,7 @@ use tracedecay_domain::{ }; use tracedecay_tool_catalog::{CapabilityId, UseCaseId}; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-contracts/tests/contracts_suite/workflow_dag_execution.rs b/crates/tracedecay-contracts/tests/contracts_suite/workflow_dag_execution.rs index cf5ffff201..e1c29de4fc 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/workflow_dag_execution.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/workflow_dag_execution.rs @@ -17,13 +17,7 @@ use tracedecay_domain::{ WorkflowStepId, WorkflowStepOutput, }; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-contracts/tests/contracts_suite/workflow_fan_out_census.rs b/crates/tracedecay-contracts/tests/contracts_suite/workflow_fan_out_census.rs index edcfc0c9d0..470b301347 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/workflow_fan_out_census.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/workflow_fan_out_census.rs @@ -31,13 +31,7 @@ use tracedecay_domain::{ WorkflowRunEventContext, WorkflowRunProjection, WorkflowStep, WorkflowStepId, WorktreeId, }; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; fn digest(byte: char) -> ManifestDigest { let hex = format!("{:02x}", u32::from(byte) & 0xff); diff --git a/crates/tracedecay-contracts/tests/contracts_suite/workflow_provider_registry.rs b/crates/tracedecay-contracts/tests/contracts_suite/workflow_provider_registry.rs index aab1a63811..0dc7658876 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/workflow_provider_registry.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/workflow_provider_registry.rs @@ -8,13 +8,7 @@ use tracedecay_domain::{ WorkflowStepId, }; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-contracts/tests/contracts_suite/workflow_runtime.rs b/crates/tracedecay-contracts/tests/contracts_suite/workflow_runtime.rs index 31cc8b0305..3f1ac19263 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/workflow_runtime.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/workflow_runtime.rs @@ -20,13 +20,7 @@ use tracedecay_domain::{ WorkflowStepId, WorktreeId, }; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-daemon-service/src/context_scout_lifecycle/tests.rs b/crates/tracedecay-daemon-service/src/context_scout_lifecycle/tests.rs index 0774879391..494dcc4b8c 100644 --- a/crates/tracedecay-daemon-service/src/context_scout_lifecycle/tests.rs +++ b/crates/tracedecay-daemon-service/src/context_scout_lifecycle/tests.rs @@ -18,12 +18,7 @@ use super::*; use tracedecay_project::test_support::host_admission::HostAdmissionTestRuntimeV1; use tracedecay_sessions::admission::HostAdmissionScope; -fn id>(value: &str) -> T -where - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; fn complete_native_observation() -> CanonicalObservationEnvelopeV1 { let relations = CanonicalObservationRelationsV1::new(id::("session.native.codex")) diff --git a/crates/tracedecay-daemon-service/src/invocation/work_attempt_exec/tests.rs b/crates/tracedecay-daemon-service/src/invocation/work_attempt_exec/tests.rs index 89eb73864b..8d0e48a8f0 100644 --- a/crates/tracedecay-daemon-service/src/invocation/work_attempt_exec/tests.rs +++ b/crates/tracedecay-daemon-service/src/invocation/work_attempt_exec/tests.rs @@ -73,13 +73,7 @@ const CODEX_EXEC_JSON_ARGV: [&str; 3] = ["exec", "--json", "-"]; // In-memory attempt authority // --------------------------------------------------------------------------- -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-daemon-service/src/query_mcp_admission.rs b/crates/tracedecay-daemon-service/src/query_mcp_admission.rs index a3144169b1..f524ab410d 100644 --- a/crates/tracedecay-daemon-service/src/query_mcp_admission.rs +++ b/crates/tracedecay-daemon-service/src/query_mcp_admission.rs @@ -327,13 +327,7 @@ mod tests { use super::{QueryMcpAdmissionUnavailableV1, admit_query_mcp_read_at}; - fn id(value: &str) -> T - where - T: TryFrom, - T::Error: std::fmt::Debug, - { - T::try_from(value.to_owned()).expect("typed fixture id") - } + use tracedecay_domain::test_fixtures::id; fn scoped( project: &str, diff --git a/crates/tracedecay-domain/src/code_intelligence/index.rs b/crates/tracedecay-domain/src/code_intelligence/index.rs index 65ad2f76b1..3bcadbe98e 100644 --- a/crates/tracedecay-domain/src/code_intelligence/index.rs +++ b/crates/tracedecay-domain/src/code_intelligence/index.rs @@ -518,13 +518,7 @@ fn validate_language_revisions( mod tests { use super::*; - fn id(value: &str) -> T - where - T: TryFrom, - >::Error: std::fmt::Debug, - { - T::try_from(value.to_owned()).expect("valid fixture identity") - } + use crate::test_fixtures::id; fn digest(byte: char) -> String { format!("sha256:{}", byte.to_string().repeat(64)) diff --git a/crates/tracedecay-domain/src/code_intelligence/language.rs b/crates/tracedecay-domain/src/code_intelligence/language.rs index fc67883850..eef9d0cbc3 100644 --- a/crates/tracedecay-domain/src/code_intelligence/language.rs +++ b/crates/tracedecay-domain/src/code_intelligence/language.rs @@ -155,13 +155,7 @@ impl EdgeAuthorityV1 { mod tests { use super::*; - fn id(value: &str) -> T - where - T: TryFrom, - >::Error: std::fmt::Debug, - { - T::try_from(value.to_owned()).expect("valid fixture identity") - } + use crate::test_fixtures::id; fn descriptor() -> LanguageDescriptorV1 { LanguageDescriptorV1 { diff --git a/crates/tracedecay-domain/src/code_intelligence/search.rs b/crates/tracedecay-domain/src/code_intelligence/search.rs index 6a0d9b3fb0..dd1689c36c 100644 --- a/crates/tracedecay-domain/src/code_intelligence/search.rs +++ b/crates/tracedecay-domain/src/code_intelligence/search.rs @@ -1581,13 +1581,7 @@ mod tests { use crate::code_intelligence::language::EdgeAuthorityV1; use crate::research::id::{PrivacyDomainId, SanitizationReceiptId}; - fn id(value: &str) -> T - where - T: TryFrom, - >::Error: std::fmt::Debug, - { - T::try_from(value.to_owned()).expect("valid fixture identity") - } + use crate::test_fixtures::id; fn digest(byte: char) -> String { format!("sha256:{}", byte.to_string().repeat(64)) diff --git a/crates/tracedecay-domain/src/diagnostics.rs b/crates/tracedecay-domain/src/diagnostics.rs index db10a5097c..effa6ba8ce 100644 --- a/crates/tracedecay-domain/src/diagnostics.rs +++ b/crates/tracedecay-domain/src/diagnostics.rs @@ -347,13 +347,7 @@ fn validate_sanitized_message(message: &str) -> Result<(), DomainError> { mod tests { use super::*; - fn id(value: &str) -> T - where - T: TryFrom, - >::Error: std::fmt::Debug, - { - T::try_from(value.to_owned()).expect("valid fixture identity") - } + use crate::test_fixtures::id; fn digest(byte: char) -> String { format!("sha256:{}", byte.to_string().repeat(64)) diff --git a/crates/tracedecay-domain/src/feedback/mod.rs b/crates/tracedecay-domain/src/feedback/mod.rs index 7e3046d207..57a3920b2e 100644 --- a/crates/tracedecay-domain/src/feedback/mod.rs +++ b/crates/tracedecay-domain/src/feedback/mod.rs @@ -1589,13 +1589,7 @@ mod tests { use crate::test_fixtures::digest; - fn id(value: &str) -> T - where - T: TryFrom, - >::Error: fmt::Debug, - { - T::try_from(value.to_owned()).unwrap() - } + use crate::test_fixtures::id; fn request(content: FeedbackContentIdentityV1) -> FeedbackCycleRequestV1 { FeedbackCycleRequestV1::new( diff --git a/crates/tracedecay-domain/src/memory/fact_tests.rs b/crates/tracedecay-domain/src/memory/fact_tests.rs index d32846f7a8..c9c5b9c051 100644 --- a/crates/tracedecay-domain/src/memory/fact_tests.rs +++ b/crates/tracedecay-domain/src/memory/fact_tests.rs @@ -3,9 +3,7 @@ use crate::observation::{SanitizerDispositionV1, SensitivityV1}; use crate::research::SanitizationReceiptRefV1; use serde_json::json; -fn id>(value: &str) -> T { - T::try_from(value.to_owned()).unwrap() -} +use crate::test_fixtures::id; fn fact_id(owner: FactOwnerV1, operation: &str) -> FactId { FactId::derive( diff --git a/crates/tracedecay-domain/src/memory/lineage.rs b/crates/tracedecay-domain/src/memory/lineage.rs index 0a8aa7d13d..9eab522703 100644 --- a/crates/tracedecay-domain/src/memory/lineage.rs +++ b/crates/tracedecay-domain/src/memory/lineage.rs @@ -288,12 +288,7 @@ mod tests { }; use super::*; - fn id(value: &str) -> T - where - T: TryFrom, - { - T::try_from(value.to_owned()).unwrap() - } + use crate::test_fixtures::id; fn fact_id(operation: &str) -> FactId { fact_id_for(&FactOwnerV1::Profile, operation) diff --git a/crates/tracedecay-domain/src/memory/relation.rs b/crates/tracedecay-domain/src/memory/relation.rs index 8e9d35265d..58426b9f6c 100644 --- a/crates/tracedecay-domain/src/memory/relation.rs +++ b/crates/tracedecay-domain/src/memory/relation.rs @@ -301,12 +301,7 @@ use crate::research::{ }; #[cfg(test)] -fn id(value: &str) -> T -where - T: TryFrom, -{ - T::try_from(value.to_owned()).unwrap() -} +use crate::test_fixtures::id; #[cfg(test)] pub(in crate::memory) fn fact_id_for(owner: &FactOwnerV1, operation: &str) -> FactId { diff --git a/crates/tracedecay-domain/src/repository.rs b/crates/tracedecay-domain/src/repository.rs index e016af7844..ad8c4d9bc9 100644 --- a/crates/tracedecay-domain/src/repository.rs +++ b/crates/tracedecay-domain/src/repository.rs @@ -489,12 +489,7 @@ mod tests { const COMMIT: &str = "0123456789abcdef0123456789abcdef01234567"; const TREE: &str = "89abcdef0123456789abcdef0123456789abcdef"; - fn id(value: &str) -> T - where - T: TryFrom, - { - T::try_from(value.to_owned()).expect("valid fixture identity") - } + use crate::test_fixtures::id; fn evidence() -> RepositoryEvidenceV1 { RepositoryEvidenceV1::new( diff --git a/crates/tracedecay-domain/src/research/mod.rs b/crates/tracedecay-domain/src/research/mod.rs index ac0ccd61ef..dc2f5e14cc 100644 --- a/crates/tracedecay-domain/src/research/mod.rs +++ b/crates/tracedecay-domain/src/research/mod.rs @@ -47,12 +47,7 @@ mod tests { use super::*; - fn id(value: &str) -> T - where - T: TryFrom, - { - T::try_from(value.to_owned()).expect("valid fixture identity") - } + use crate::test_fixtures::id; #[test] fn ids_reject_invalid_deserialized_values() { diff --git a/crates/tracedecay-domain/src/retrieval.rs b/crates/tracedecay-domain/src/retrieval.rs index 26baba2a11..571939fd72 100644 --- a/crates/tracedecay-domain/src/retrieval.rs +++ b/crates/tracedecay-domain/src/retrieval.rs @@ -1281,13 +1281,7 @@ mod tests { const ONE_DIGEST: &str = "sha256:1111111111111111111111111111111111111111111111111111111111111111"; - fn id(value: &str) -> T - where - T: TryFrom, - >::Error: fmt::Debug, - { - T::try_from(value.to_owned()).expect("valid fixture identity") - } + use crate::test_fixtures::id; fn freshness() -> SourceFreshness { SourceFreshness { diff --git a/crates/tracedecay-domain/src/test_fixtures.rs b/crates/tracedecay-domain/src/test_fixtures.rs index f17f9fb09e..7b81d2e2df 100644 --- a/crates/tracedecay-domain/src/test_fixtures.rs +++ b/crates/tracedecay-domain/src/test_fixtures.rs @@ -2,7 +2,9 @@ //! on this one. //! //! The values are not hashes of anything. Suites used to each re-roll the same -//! `sha256:` spelling. +//! `sha256:` spelling and the same `TryFrom` identity parse. + +use std::fmt::Debug; use crate::ManifestDigest; @@ -22,9 +24,18 @@ pub fn digest(digit: char) -> ManifestDigest { ManifestDigest::new(repeated_sha256_text(digit)).expect("fixture digest is canonical") } +/// Parses a fixture identity. The value must already be canonical for `T`. +pub fn id(value: &str) -> T +where + T: TryFrom, + T::Error: Debug, +{ + T::try_from(value.to_owned()).expect("fixture id is canonical") +} + #[cfg(test)] mod tests { - use super::{digest, repeated_sha256_text}; + use super::{digest, id, repeated_sha256_text}; #[test] fn repeated_hex_digest_matches_the_shared_spelling() { @@ -36,4 +47,10 @@ mod tests { crate::ManifestDigest::zero().unwrap().as_str() ); } + + #[test] + fn fixture_id_accepts_a_canonical_project_id() { + let project: crate::ProjectId = id("project.fixture"); + assert_eq!(project.as_str(), "project.fixture"); + } } diff --git a/crates/tracedecay-domain/tests/domain_suite/code_search_contract.rs b/crates/tracedecay-domain/tests/domain_suite/code_search_contract.rs index 7c8a9c5577..10efffa9aa 100644 --- a/crates/tracedecay-domain/tests/domain_suite/code_search_contract.rs +++ b/crates/tracedecay-domain/tests/domain_suite/code_search_contract.rs @@ -1,5 +1,4 @@ use std::collections::BTreeMap; -use std::fmt; use tracedecay_domain::{ CandidateContribution, CompactCandidate, EvidenceRole, ExactAdmissionProof, @@ -11,13 +10,7 @@ use tracedecay_domain::{ const ZERO_DIGEST: &str = "sha256:0000000000000000000000000000000000000000000000000000000000000000"; -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: fmt::Debug, -{ - T::try_from(value.to_owned()).expect("valid fixture identity") -} +use tracedecay_domain::test_fixtures::id; fn freshness() -> SourceFreshness { SourceFreshness { diff --git a/crates/tracedecay-domain/tests/domain_suite/configuration_contract.rs b/crates/tracedecay-domain/tests/domain_suite/configuration_contract.rs index 6d42bd27e4..e99ee1972a 100644 --- a/crates/tracedecay-domain/tests/domain_suite/configuration_contract.rs +++ b/crates/tracedecay-domain/tests/domain_suite/configuration_contract.rs @@ -12,13 +12,7 @@ use tracedecay_domain::{ AccessPolicyDigest, ActorId, CapabilityId, LocatorDigest, ProjectId, UtcMicros, }; -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).expect("fixture id is canonical") -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-domain/tests/domain_suite/feedback_contract.rs b/crates/tracedecay-domain/tests/domain_suite/feedback_contract.rs index be31df8798..d7c547723a 100644 --- a/crates/tracedecay-domain/tests/domain_suite/feedback_contract.rs +++ b/crates/tracedecay-domain/tests/domain_suite/feedback_contract.rs @@ -12,13 +12,7 @@ use tracedecay_domain::{ RepositoryId, RetrievalAnchorId, SessionId, UtcMicros, WorktreeId, }; -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).expect("fixture id is canonical") -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-domain/tests/domain_suite/git_index_transaction_contract.rs b/crates/tracedecay-domain/tests/domain_suite/git_index_transaction_contract.rs index 09a99a678d..6054ec7d94 100644 --- a/crates/tracedecay-domain/tests/domain_suite/git_index_transaction_contract.rs +++ b/crates/tracedecay-domain/tests/domain_suite/git_index_transaction_contract.rs @@ -13,13 +13,7 @@ use tracedecay_domain::{ ManifestDigest, ProjectId, RepositoryId, UtcMicros, WorktreeId, }; -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).expect("fixture id is canonical") -} +use tracedecay_domain::test_fixtures::id; fn oid(byte: char) -> GitOidV1 { GitOidV1::new(byte.to_string().repeat(40)).expect("fixture oid is canonical") diff --git a/crates/tracedecay-domain/tests/domain_suite/git_topology_anchor_contract.rs b/crates/tracedecay-domain/tests/domain_suite/git_topology_anchor_contract.rs index d50ca533e6..85e8afa664 100644 --- a/crates/tracedecay-domain/tests/domain_suite/git_topology_anchor_contract.rs +++ b/crates/tracedecay-domain/tests/domain_suite/git_topology_anchor_contract.rs @@ -27,13 +27,7 @@ use tracedecay_domain::{ canonical_sha256, derive_git_topology_anchor_id, }; -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).expect("fixture id is canonical") -} +use tracedecay_domain::test_fixtures::id; fn oid(byte: char) -> GitOidV1 { GitOidV1::new(byte.to_string().repeat(40)).expect("fixture oid is canonical") diff --git a/crates/tracedecay-domain/tests/domain_suite/repository_state_contract.rs b/crates/tracedecay-domain/tests/domain_suite/repository_state_contract.rs index 04ef1fd777..2970fe7e85 100644 --- a/crates/tracedecay-domain/tests/domain_suite/repository_state_contract.rs +++ b/crates/tracedecay-domain/tests/domain_suite/repository_state_contract.rs @@ -7,13 +7,7 @@ use tracedecay_domain::{ RepositoryId, UtcMicros, WorktreeId, }; -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).expect("fixture id is canonical") -} +use tracedecay_domain::test_fixtures::id; fn oid(byte: char) -> GitOidV1 { GitOidV1::new(byte.to_string().repeat(40)).expect("fixture oid is canonical") diff --git a/crates/tracedecay-domain/tests/domain_suite/work_duplicate_adjudication_contract.rs b/crates/tracedecay-domain/tests/domain_suite/work_duplicate_adjudication_contract.rs index f1bc1b5120..edc161c718 100644 --- a/crates/tracedecay-domain/tests/domain_suite/work_duplicate_adjudication_contract.rs +++ b/crates/tracedecay-domain/tests/domain_suite/work_duplicate_adjudication_contract.rs @@ -7,13 +7,7 @@ use tracedecay_domain::{ WorkDuplicateAdjudicationRevisionV1, WorkTopologyGenerationRefV1, WorktreeId, }; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; fn attempt(task: &str, run: &str, attempt: &str) -> WorkAttemptIdentityV1 { WorkAttemptIdentityV1::new( diff --git a/crates/tracedecay-domain/tests/domain_suite/work_execution_snapshot_contract.rs b/crates/tracedecay-domain/tests/domain_suite/work_execution_snapshot_contract.rs index d7a43a4847..4104df6535 100644 --- a/crates/tracedecay-domain/tests/domain_suite/work_execution_snapshot_contract.rs +++ b/crates/tracedecay-domain/tests/domain_suite/work_execution_snapshot_contract.rs @@ -9,13 +9,7 @@ use tracedecay_domain::{ WorkSandboxPolicy, WorktreeCleanlinessRequirementV1, safe_work_topology_policy_v1, }; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-domain/tests/domain_suite/work_product_contract.rs b/crates/tracedecay-domain/tests/domain_suite/work_product_contract.rs index 2904432905..5182d96967 100644 --- a/crates/tracedecay-domain/tests/domain_suite/work_product_contract.rs +++ b/crates/tracedecay-domain/tests/domain_suite/work_product_contract.rs @@ -18,13 +18,7 @@ use tracedecay_domain::{ WorkTimelineLaneV1, canonical_json_bytes, }; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-domain/tests/domain_suite/work_runtime_contract.rs b/crates/tracedecay-domain/tests/domain_suite/work_runtime_contract.rs index ab0658b2b3..4690558d9a 100644 --- a/crates/tracedecay-domain/tests/domain_suite/work_runtime_contract.rs +++ b/crates/tracedecay-domain/tests/domain_suite/work_runtime_contract.rs @@ -19,13 +19,7 @@ use tracedecay_domain::{ WorkflowOperationRef, WorktreeId, safe_work_topology_policy_v1, }; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-domain/tests/domain_suite/workflow_definition_contract.rs b/crates/tracedecay-domain/tests/domain_suite/workflow_definition_contract.rs index 6aea6e511c..f5617d590d 100644 --- a/crates/tracedecay-domain/tests/domain_suite/workflow_definition_contract.rs +++ b/crates/tracedecay-domain/tests/domain_suite/workflow_definition_contract.rs @@ -14,13 +14,7 @@ use tracedecay_domain::{ WorkflowStepId, WorkflowStepOutput, WorkflowStepStatus, }; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-global-db/src/configuration/resolver.rs b/crates/tracedecay-global-db/src/configuration/resolver.rs index 5899e7b975..7f158a9864 100644 --- a/crates/tracedecay-global-db/src/configuration/resolver.rs +++ b/crates/tracedecay-global-db/src/configuration/resolver.rs @@ -183,13 +183,7 @@ mod tests { ConfigurationValueV1, UserProfileId, }; - fn id(value: &str) -> T - where - T: TryFrom, - >::Error: std::fmt::Debug, - { - T::try_from(value.to_owned()).unwrap() - } + use tracedecay_domain::test_fixtures::id; #[test] fn same_value_in_a_higher_layer_changes_provenance_not_behavior() { diff --git a/crates/tracedecay-global-db/src/configuration/store/tests/mod.rs b/crates/tracedecay-global-db/src/configuration/store/tests/mod.rs index 6674ebc6bb..8484c6bf39 100644 --- a/crates/tracedecay-global-db/src/configuration/store/tests/mod.rs +++ b/crates/tracedecay-global-db/src/configuration/store/tests/mod.rs @@ -188,13 +188,7 @@ async fn count( rows.next().await.unwrap().unwrap().get::(0).unwrap() } -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; fn root_revision() -> ConfigurationRevisionRecordV1 { let snapshot = resolve_configuration(&ConfigurationRegistry::core().unwrap(), &[]) diff --git a/crates/tracedecay-global-db/src/git_index_transactions/tests.rs b/crates/tracedecay-global-db/src/git_index_transactions/tests.rs index e0f64bcdb1..8de103fe9c 100644 --- a/crates/tracedecay-global-db/src/git_index_transactions/tests.rs +++ b/crates/tracedecay-global-db/src/git_index_transactions/tests.rs @@ -52,13 +52,7 @@ async fn repositories_page( } } -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).expect("fixture identity") -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-global-db/tests/session_relation_graph.rs b/crates/tracedecay-global-db/tests/session_relation_graph.rs index f342b7347a..12121c9935 100644 --- a/crates/tracedecay-global-db/tests/session_relation_graph.rs +++ b/crates/tracedecay-global-db/tests/session_relation_graph.rs @@ -35,13 +35,7 @@ impl GraphCancellation for TestCancellation { } } -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).expect("valid identity") -} +use tracedecay_domain::test_fixtures::id; fn occurrence_id(seed: &str) -> MessageOccurrenceIdV1 { let digest = Sha256::digest(seed.as_bytes()); diff --git a/crates/tracedecay-graph-query/src/context/read_modes.rs b/crates/tracedecay-graph-query/src/context/read_modes.rs index 36e98d0d32..ffe005cc35 100644 --- a/crates/tracedecay-graph-query/src/context/read_modes.rs +++ b/crates/tracedecay-graph-query/src/context/read_modes.rs @@ -301,13 +301,7 @@ mod tests { const FILE: &str = "src/main.rs"; - fn id(value: &str) -> T - where - T: TryFrom, - >::Error: Debug, - { - T::try_from(value.to_owned()).expect("valid fixture identity") - } + use tracedecay_domain::test_fixtures::id; fn digest(byte: char) -> T where diff --git a/crates/tracedecay-policy/src/configuration.rs b/crates/tracedecay-policy/src/configuration.rs index 2d7676f637..38587fea6a 100644 --- a/crates/tracedecay-policy/src/configuration.rs +++ b/crates/tracedecay-policy/src/configuration.rs @@ -156,13 +156,7 @@ mod tests { use super::*; use tracedecay_domain::configuration::ConfigurationGrantReceiptId; - fn id(value: &str) -> T - where - T: TryFrom, - >::Error: std::fmt::Debug, - { - T::try_from(value.to_owned()).unwrap() - } + use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-policy/tests/policy_suite/routing_admission.rs b/crates/tracedecay-policy/tests/policy_suite/routing_admission.rs index d43d3d4695..b9543fc125 100644 --- a/crates/tracedecay-policy/tests/policy_suite/routing_admission.rs +++ b/crates/tracedecay-policy/tests/policy_suite/routing_admission.rs @@ -26,13 +26,7 @@ use tracedecay_policy::routing::{ TruthFreshnessRequirementV1, TruthSourceStateV1, }; -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).expect("valid fixture identifier") -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-project/src/project/lifecycle/mod.rs b/crates/tracedecay-project/src/project/lifecycle/mod.rs index 5812e816f5..989753c1b5 100644 --- a/crates/tracedecay-project/src/project/lifecycle/mod.rs +++ b/crates/tracedecay-project/src/project/lifecycle/mod.rs @@ -896,12 +896,7 @@ mod tests { tracedecay_agent_hosts::agents::context_scout::ports::ContextScoutLifecycleAddressV1, tracedecay_contracts::context_scout::ContextScoutAddressV1, ) { - fn id>(value: &str) -> T - where - T::Error: std::fmt::Debug, - { - T::try_from(value.to_owned()).unwrap() - } + use tracedecay_domain::test_fixtures::id; let observed_at = UtcMicros(10); let project_id = id::("project.scout.fixture"); diff --git a/crates/tracedecay-query/assets/runtime-root/tests/fixtures/search_quality/corpus/crates/tracedecay-domain/src/repository.rs b/crates/tracedecay-query/assets/runtime-root/tests/fixtures/search_quality/corpus/crates/tracedecay-domain/src/repository.rs index 816ce1a55f..5fc420ee66 100644 --- a/crates/tracedecay-query/assets/runtime-root/tests/fixtures/search_quality/corpus/crates/tracedecay-domain/src/repository.rs +++ b/crates/tracedecay-query/assets/runtime-root/tests/fixtures/search_quality/corpus/crates/tracedecay-domain/src/repository.rs @@ -501,12 +501,7 @@ mod tests { const COMMIT: &str = "0123456789abcdef0123456789abcdef01234567"; const TREE: &str = "89abcdef0123456789abcdef0123456789abcdef"; - fn id(value: &str) -> T - where - T: TryFrom, - { - T::try_from(value.to_owned()).expect("valid fixture identity") - } + use crate::test_fixtures::id; fn evidence() -> RepositoryEvidenceV1 { RepositoryEvidenceV1::new( diff --git a/crates/tracedecay-query/src/retrieval/diversity.rs b/crates/tracedecay-query/src/retrieval/diversity.rs index 42b3429ae9..fda12b1686 100644 --- a/crates/tracedecay-query/src/retrieval/diversity.rs +++ b/crates/tracedecay-query/src/retrieval/diversity.rs @@ -280,13 +280,7 @@ mod cap_key_tests { FreshnessCompatibilityV1, RetrievalAnchorId, SourceFreshness, UtcMicros, }; - fn id(value: &str) -> T - where - T: TryFrom, - >::Error: std::fmt::Debug, - { - T::try_from(value.to_owned()).expect("valid fixture identity") - } + use tracedecay_domain::test_fixtures::id; fn occurrence(name: &str, file: &str) -> OccurrenceProvenance { OccurrenceProvenance { diff --git a/crates/tracedecay-query/src/retrieval/exact/tests.rs b/crates/tracedecay-query/src/retrieval/exact/tests.rs index 230fca18d8..ff260038d5 100644 --- a/crates/tracedecay-query/src/retrieval/exact/tests.rs +++ b/crates/tracedecay-query/src/retrieval/exact/tests.rs @@ -36,13 +36,7 @@ impl RetrievalExecutionControl for ActiveControl { } } -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: fmt::Debug, -{ - T::try_from(value.to_owned()).expect("valid fixture identity") -} +use tracedecay_domain::test_fixtures::id; fn digest_id(byte: char) -> T where diff --git a/crates/tracedecay-query/src/retrieval/fusion.rs b/crates/tracedecay-query/src/retrieval/fusion.rs index 3f3d285228..622dca803a 100644 --- a/crates/tracedecay-query/src/retrieval/fusion.rs +++ b/crates/tracedecay-query/src/retrieval/fusion.rs @@ -1366,13 +1366,7 @@ mod attach_same_source_decisions_tests { use super::*; use tracedecay_domain::{EvidenceRole, FreshnessCompatibilityV1}; - fn id(value: &str) -> T - where - T: TryFrom, - >::Error: std::fmt::Debug, - { - T::try_from(value.to_owned()).expect("valid fixture identity") - } + use tracedecay_domain::test_fixtures::id; fn freshness() -> SourceFreshness { SourceFreshness { diff --git a/crates/tracedecay-query/src/retrieval/graph/tests.rs b/crates/tracedecay-query/src/retrieval/graph/tests.rs index 3726c80b50..8b19201603 100644 --- a/crates/tracedecay-query/src/retrieval/graph/tests.rs +++ b/crates/tracedecay-query/src/retrieval/graph/tests.rs @@ -59,13 +59,7 @@ fn graph_control() -> Arc { }) } -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: fmt::Debug, -{ - T::try_from(value.to_owned()).expect("valid fixture identity") -} +use tracedecay_domain::test_fixtures::id; fn digest_id(byte: char) -> T where diff --git a/crates/tracedecay-query/src/retrieval/lexical/routes/tests.rs b/crates/tracedecay-query/src/retrieval/lexical/routes/tests.rs index ccccf797bf..3a073f7f35 100644 --- a/crates/tracedecay-query/src/retrieval/lexical/routes/tests.rs +++ b/crates/tracedecay-query/src/retrieval/lexical/routes/tests.rs @@ -23,13 +23,7 @@ use crate::retrieval::lexical::{ }; use crate::retrieval::ports::{CodeCandidateBindingV1, CodeOccurrenceRefV1, RetrievalPortError}; -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: fmt::Debug, -{ - T::try_from(value.to_owned()).expect("valid fixture identity") -} +use tracedecay_domain::test_fixtures::id; fn anchors(values: &[&str]) -> Vec { values.iter().map(|value| (*value).to_owned()).collect() diff --git a/crates/tracedecay-query/src/retrieval/lexical/tests.rs b/crates/tracedecay-query/src/retrieval/lexical/tests.rs index eeda445556..061add520e 100644 --- a/crates/tracedecay-query/src/retrieval/lexical/tests.rs +++ b/crates/tracedecay-query/src/retrieval/lexical/tests.rs @@ -58,13 +58,7 @@ impl RetrievalExecutionControl for CancelledControl { static CANCELLED_CONTROL: CancelledControl = CancelledControl; -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: fmt::Debug, -{ - T::try_from(value.to_owned()).expect("valid fixture identity") -} +use tracedecay_domain::test_fixtures::id; fn digest_id(byte: char) -> T where diff --git a/crates/tracedecay-query/src/retrieval/tests.rs b/crates/tracedecay-query/src/retrieval/tests.rs index 8fecd39279..896ee00066 100644 --- a/crates/tracedecay-query/src/retrieval/tests.rs +++ b/crates/tracedecay-query/src/retrieval/tests.rs @@ -20,13 +20,7 @@ use tracedecay_domain::{ use super::fusion::CompositionLaneInput; -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: fmt::Debug, -{ - T::try_from(value.to_owned()).expect("valid fixture identity") -} +use tracedecay_domain::test_fixtures::id; fn digest_id(byte: char) -> T where diff --git a/crates/tracedecay-query/tests/canonical_execution_equivalence.rs b/crates/tracedecay-query/tests/canonical_execution_equivalence.rs index 3fa79fc2e5..e25f0d7f32 100644 --- a/crates/tracedecay-query/tests/canonical_execution_equivalence.rs +++ b/crates/tracedecay-query/tests/canonical_execution_equivalence.rs @@ -26,13 +26,7 @@ use tracedecay_query::retrieval::{ route_authenticated_prepared_query_cursor, }; -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: fmt::Debug, -{ - T::try_from(value.to_owned()).expect("valid fixture identity") -} +use tracedecay_domain::test_fixtures::id; fn digest(byte: char) -> T where diff --git a/crates/tracedecay-query/tests/retrieval_contract_spine.rs b/crates/tracedecay-query/tests/retrieval_contract_spine.rs index 9774e25bb9..b43433e077 100644 --- a/crates/tracedecay-query/tests/retrieval_contract_spine.rs +++ b/crates/tracedecay-query/tests/retrieval_contract_spine.rs @@ -25,13 +25,7 @@ impl RetrievalExecutionControl for ActiveControl { } } -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: fmt::Debug, -{ - T::try_from(value.to_owned()).expect("valid fixture identity") -} +use tracedecay_domain::test_fixtures::id; fn request_and_proof() -> (ExactLaneRequest<'static>, ExactAdmissionProof) { let scope = RetrievalScope { diff --git a/crates/tracedecay-query/tests/search_quality_suite/candidate_producers.rs b/crates/tracedecay-query/tests/search_quality_suite/candidate_producers.rs index 9009f6ecdd..1447d228f8 100644 --- a/crates/tracedecay-query/tests/search_quality_suite/candidate_producers.rs +++ b/crates/tracedecay-query/tests/search_quality_suite/candidate_producers.rs @@ -886,13 +886,7 @@ impl TestArtifactSourceStaging for CodeLexicalArtifactBuilderV1 { } } -pub(crate) fn id(value: &str) -> T -where - T: TryFrom, - >::Error: fmt::Debug, -{ - T::try_from(value.to_owned()).expect("valid fixture identity") -} +pub(crate) use tracedecay_domain::test_fixtures::id; pub(crate) fn digest_id(byte: char) -> T where diff --git a/crates/tracedecay-runtime-core/src/shard_runtime/registry/close.rs b/crates/tracedecay-runtime-core/src/shard_runtime/registry/close.rs index ed6e46a05a..b2be54ec03 100644 --- a/crates/tracedecay-runtime-core/src/shard_runtime/registry/close.rs +++ b/crates/tracedecay-runtime-core/src/shard_runtime/registry/close.rs @@ -439,13 +439,7 @@ mod tests { }; use crate::shard_runtime::shard::ShardRuntime; - fn id(value: &str) -> T - where - T: TryFrom, - >::Error: Debug, - { - T::try_from(value.to_owned()).unwrap() - } + use tracedecay_domain::test_fixtures::id; fn profile_shard() -> StoreShardIdV1 { StoreShardIdV1::profile( diff --git a/crates/tracedecay-runtime-core/src/shard_runtime/registry/destructive.rs b/crates/tracedecay-runtime-core/src/shard_runtime/registry/destructive.rs index 90df2e7fb5..48bc8fb8af 100644 --- a/crates/tracedecay-runtime-core/src/shard_runtime/registry/destructive.rs +++ b/crates/tracedecay-runtime-core/src/shard_runtime/registry/destructive.rs @@ -408,13 +408,7 @@ mod tests { }; use crate::shard_runtime::shard::ShardRuntime; - fn id(value: &str) -> T - where - T: TryFrom, - T::Error: std::fmt::Debug, - { - T::try_from(value.to_owned()).unwrap() - } + use tracedecay_domain::test_fixtures::id; fn profile_shard() -> StoreShardIdV1 { StoreShardIdV1::profile( diff --git a/crates/tracedecay-runtime-core/src/shard_runtime/registry/retirement/tests.rs b/crates/tracedecay-runtime-core/src/shard_runtime/registry/retirement/tests.rs index b89ffb9696..9878fba087 100644 --- a/crates/tracedecay-runtime-core/src/shard_runtime/registry/retirement/tests.rs +++ b/crates/tracedecay-runtime-core/src/shard_runtime/registry/retirement/tests.rs @@ -172,13 +172,7 @@ impl StoreRuntimeOwnerAttachmentRetirementReservationV1 for FailingOwnerAttachme } } -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; fn profile_shard(profile: &str) -> StoreShardIdV1 { StoreShardIdV1::profile( diff --git a/crates/tracedecay-runtime-core/src/shard_runtime/registry/tests/support.rs b/crates/tracedecay-runtime-core/src/shard_runtime/registry/tests/support.rs index f9b4a28891..f14efd5562 100644 --- a/crates/tracedecay-runtime-core/src/shard_runtime/registry/tests/support.rs +++ b/crates/tracedecay-runtime-core/src/shard_runtime/registry/tests/support.rs @@ -15,13 +15,7 @@ use tracedecay_store::{ use super::super::*; -pub(super) fn id(value: &str) -> T -where - T: TryFrom, - >::Error: Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +pub(super) use tracedecay_domain::test_fixtures::id; /// Host-absolute fixture path: store locators require `Path::is_absolute`, /// which a bare `/...` literal fails on Windows, where the same fixture is diff --git a/crates/tracedecay-runtime-core/src/shard_runtime/shard.rs b/crates/tracedecay-runtime-core/src/shard_runtime/shard.rs index 9cd35aaae1..bd23260bba 100644 --- a/crates/tracedecay-runtime-core/src/shard_runtime/shard.rs +++ b/crates/tracedecay-runtime-core/src/shard_runtime/shard.rs @@ -1087,13 +1087,7 @@ mod tests { ShardRuntimeLeaseKind::Client, ]; - fn id(value: &str) -> T - where - T: TryFrom, - >::Error: Debug, - { - T::try_from(value.to_owned()).expect("canonical fixture identity") - } + use tracedecay_domain::test_fixtures::id; fn binding() -> StoreRuntimeBindingV1 { StoreRuntimeBindingV1::new( diff --git a/crates/tracedecay-runtime-core/src/shard_runtime/telemetry.rs b/crates/tracedecay-runtime-core/src/shard_runtime/telemetry.rs index c9c0d60573..a1cb2b2f69 100644 --- a/crates/tracedecay-runtime-core/src/shard_runtime/telemetry.rs +++ b/crates/tracedecay-runtime-core/src/shard_runtime/telemetry.rs @@ -438,13 +438,7 @@ mod tests { ShardRuntime, ShardRuntimeEvictionBlocker, ShardRuntimeLeaseKind, }; - fn id(value: &str) -> T - where - T: TryFrom, - >::Error: Debug, - { - T::try_from(value.to_owned()).expect("canonical test identity") - } + use tracedecay_domain::test_fixtures::id; fn binding(project: &str, incarnation: u64, epoch: u64) -> StoreRuntimeBindingV1 { StoreRuntimeBindingV1::new( diff --git a/crates/tracedecay-rusqlite-runtime/src/checkpoint/tests.rs b/crates/tracedecay-rusqlite-runtime/src/checkpoint/tests.rs index d5a6e57fd2..4eaa9e42c5 100644 --- a/crates/tracedecay-rusqlite-runtime/src/checkpoint/tests.rs +++ b/crates/tracedecay-rusqlite-runtime/src/checkpoint/tests.rs @@ -102,13 +102,7 @@ fn controller( .expect("fake driver configures") } -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; /// Test authority that admits one externally canonical publication and issues /// a permit only from an observed clear drain. It never derives an identity diff --git a/crates/tracedecay-rusqlite-runtime/src/watermark/tests.rs b/crates/tracedecay-rusqlite-runtime/src/watermark/tests.rs index 8b055bd7ea..d163875e9e 100644 --- a/crates/tracedecay-rusqlite-runtime/src/watermark/tests.rs +++ b/crates/tracedecay-rusqlite-runtime/src/watermark/tests.rs @@ -8,13 +8,7 @@ use tracedecay_store::{ use super::*; use crate::read_consistency::{CommitWatermarkSource, WatermarkSourceState}; -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; fn binding(project: &str) -> StoreRuntimeBindingV1 { StoreRuntimeBindingV1::new( diff --git a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/handoff_open_storage.rs b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/handoff_open_storage.rs index e198bfc273..6492b245d3 100644 --- a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/handoff_open_storage.rs +++ b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/handoff_open_storage.rs @@ -37,13 +37,7 @@ impl HandoffOpenTargetPort for CurrentTarget { } } -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/multi_root_scope_set.rs b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/multi_root_scope_set.rs index ad1c63423e..ae1c354fb3 100644 --- a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/multi_root_scope_set.rs +++ b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/multi_root_scope_set.rs @@ -1,5 +1,4 @@ use std::collections::BTreeSet; -use std::fmt; use std::path::PathBuf; use rusqlite::{Connection, Savepoint}; @@ -139,13 +138,7 @@ fn registered_locator(binding: &StoreRuntimeBindingV1) -> VerifiedStoreLocatorV1 ) } -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/repository_attachment.rs b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/repository_attachment.rs index 114146e7b2..79b3777f88 100644 --- a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/repository_attachment.rs +++ b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/repository_attachment.rs @@ -1,5 +1,3 @@ -use std::fmt::Debug; - use rusqlite::Connection; use tempfile::TempDir; use tracedecay_domain::{ @@ -20,13 +18,7 @@ use tracedecay_store::{ VerifiedStoreLocatorV1, }; -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; fn binding() -> StoreRuntimeBindingV1 { serde_json::from_value(serde_json::json!({ diff --git a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_attempt_storage.rs b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_attempt_storage.rs index 871116014d..928e61da59 100644 --- a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_attempt_storage.rs +++ b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_attempt_storage.rs @@ -45,13 +45,7 @@ use common::fixture_abs_root; use tracedecay_rusqlite_runtime::workflow::install_workflow_schema; use work_registered_store::RegisteredWorkStore; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_duplicate_adjudication_storage.rs b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_duplicate_adjudication_storage.rs index 9daf8208b6..ccca0529dd 100644 --- a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_duplicate_adjudication_storage.rs +++ b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_duplicate_adjudication_storage.rs @@ -20,13 +20,7 @@ use tracedecay_domain::{ use work_registered_store::RegisteredWorkStore; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_leak_adjudication_storage.rs b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_leak_adjudication_storage.rs index 72e7ee41a8..3fa698b98c 100644 --- a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_leak_adjudication_storage.rs +++ b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_leak_adjudication_storage.rs @@ -27,13 +27,7 @@ use tracedecay_domain::{ use common::fixture_abs_root; use work_registered_store::RegisteredWorkStore; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_placement_storage.rs b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_placement_storage.rs index 6d4df4e8e5..b8eefa8b74 100644 --- a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_placement_storage.rs +++ b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_placement_storage.rs @@ -26,13 +26,7 @@ use work_registered_store::RegisteredWorkStore; static ROOT: std::sync::LazyLock = std::sync::LazyLock::new(|| fixture_abs_root("/workspace/placement-storage")); -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_product_graph_authority.rs b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_product_graph_authority.rs index 0f256e19b6..2d16d00d07 100644 --- a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_product_graph_authority.rs +++ b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_product_graph_authority.rs @@ -43,13 +43,7 @@ const REPOSITORY: &str = "repository.work-product.fixture"; /// `occurred_at`, so a projection is never asked to describe its own future. const PROJECTED_AT: UtcMicros = UtcMicros(400); -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_product_query_authority.rs b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_product_query_authority.rs index d92366dd98..048597c289 100644 --- a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_product_query_authority.rs +++ b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_product_query_authority.rs @@ -56,13 +56,7 @@ const REPOSITORY: &str = "repository.work-product-query.fixture"; const OBSERVED_AT: UtcMicros = UtcMicros(400); const TASK: &str = "task.deliver"; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_run_control_storage.rs b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_run_control_storage.rs index ac4237536e..6d4ba5f425 100644 --- a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_run_control_storage.rs +++ b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/work_run_control_storage.rs @@ -39,13 +39,7 @@ use work_registered_store::RegisteredWorkStore; const ADMITTED_DEADLINE: UtcMicros = UtcMicros(1_000_000); -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/workflow_fan_out_census_storage.rs b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/workflow_fan_out_census_storage.rs index 1f3ad60ee9..0ff22554f1 100644 --- a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/workflow_fan_out_census_storage.rs +++ b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/workflow_fan_out_census_storage.rs @@ -31,13 +31,7 @@ use crate::registered_workflow_store; use registered_workflow_store::RegisteredWorkflowStore; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; fn digest(byte: char) -> ManifestDigest { let hex = format!("{:02x}", u32::from(byte) & 0xff); diff --git a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/workflow_run_journal_storage.rs b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/workflow_run_journal_storage.rs index a5d3c9e10f..a6474202fb 100644 --- a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/workflow_run_journal_storage.rs +++ b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/workflow_run_journal_storage.rs @@ -39,13 +39,7 @@ use crate::registered_workflow_store; use registered_workflow_store::RegisteredWorkflowStore; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; /// A distinct, valid `sha256:`-tagged digest per input byte. fn digest(byte: char) -> ManifestDigest { diff --git a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/workflow_runtime_storage.rs b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/workflow_runtime_storage.rs index bd05d1dae0..d2ae0fa316 100644 --- a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/workflow_runtime_storage.rs +++ b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/workflow_runtime_storage.rs @@ -26,13 +26,7 @@ use crate::registered_workflow_store; use registered_workflow_store::RegisteredWorkflowStore; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; /// A distinct, valid `sha256:`-tagged digest per input byte. /// diff --git a/crates/tracedecay-session-memory/src/memory/tests.rs b/crates/tracedecay-session-memory/src/memory/tests.rs index 98f5395c88..9c4547cc74 100644 --- a/crates/tracedecay-session-memory/src/memory/tests.rs +++ b/crates/tracedecay-session-memory/src/memory/tests.rs @@ -564,12 +564,7 @@ fn owner() -> FactOwnerV1 { } } -fn id(value: &str) -> T -where - T: TryFrom, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; fn fact_id(owner: FactOwnerV1, operation: &str) -> FactId { FactId::derive( diff --git a/crates/tracedecay-session-temporal-store/src/relations/projection_read.rs b/crates/tracedecay-session-temporal-store/src/relations/projection_read.rs index d9f435fa6a..845f3b2a7b 100644 --- a/crates/tracedecay-session-temporal-store/src/relations/projection_read.rs +++ b/crates/tracedecay-session-temporal-store/src/relations/projection_read.rs @@ -478,13 +478,7 @@ mod tests { GraphWatermark, NeverCancelled, ProjectionReplacement, SourceGeneration, }; - fn id(value: &str) -> T - where - T: TryFrom, - T::Error: std::fmt::Debug, - { - T::try_from(value.to_owned()).expect("valid test identity") - } + use tracedecay_domain::test_fixtures::id; fn relation_projection() -> SessionRelationProjection { SessionRelationProjection { diff --git a/crates/tracedecay-store-runtime/src/store_locator_resolver.rs b/crates/tracedecay-store-runtime/src/store_locator_resolver.rs index 60e91d10c3..6f17287093 100644 --- a/crates/tracedecay-store-runtime/src/store_locator_resolver.rs +++ b/crates/tracedecay-store-runtime/src/store_locator_resolver.rs @@ -1481,13 +1481,7 @@ mod tests { use super::*; - fn id(value: &str) -> T - where - T: TryFrom, - >::Error: Debug, - { - T::try_from(value.to_owned()).expect("canonical fixture identity") - } + use tracedecay_domain::test_fixtures::id; fn incarnation() -> StoreIncarnationV1 { StoreIncarnationV1::new(1).expect("non-zero fixture incarnation") diff --git a/crates/tracedecay-store/src/memory/tests.rs b/crates/tracedecay-store/src/memory/tests.rs index 3f1673771b..bdb18c7e50 100644 --- a/crates/tracedecay-store/src/memory/tests.rs +++ b/crates/tracedecay-store/src/memory/tests.rs @@ -16,12 +16,7 @@ use super::*; mod add_material; -fn id(value: &str) -> T -where - T: TryFrom, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; fn fact_id(owner: FactOwnerV1, operation: &str) -> FactId { FactId::derive( diff --git a/crates/tracedecay-store/src/runtime/identity.rs b/crates/tracedecay-store/src/runtime/identity.rs index 44ab00934b..09f27ac3cd 100644 --- a/crates/tracedecay-store/src/runtime/identity.rs +++ b/crates/tracedecay-store/src/runtime/identity.rs @@ -537,13 +537,7 @@ mod tests { absolute_temp_root.join(name) } - fn id(value: &str) -> T - where - T: TryFrom, - >::Error: fmt::Debug, - { - T::try_from(value.to_owned()).expect("canonical fixture identity") - } + use tracedecay_domain::test_fixtures::id; #[test] fn profile_memory_has_a_distinct_mutable_wire_identity() { diff --git a/crates/tracedecay-store/tests/store_suite/configuration_contract.rs b/crates/tracedecay-store/tests/store_suite/configuration_contract.rs index accaa2d5e2..1767d8060e 100644 --- a/crates/tracedecay-store/tests/store_suite/configuration_contract.rs +++ b/crates/tracedecay-store/tests/store_suite/configuration_contract.rs @@ -7,13 +7,7 @@ use tracedecay_store::configuration::{ ConfigurationProtectedOperationV1, ConfigurationProtectedPlanRecordV1, }; -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).expect("fixture id is canonical") -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay-store/tests/store_suite/diagnostics_contract.rs b/crates/tracedecay-store/tests/store_suite/diagnostics_contract.rs index 4eecc6f9ce..db63bca347 100644 --- a/crates/tracedecay-store/tests/store_suite/diagnostics_contract.rs +++ b/crates/tracedecay-store/tests/store_suite/diagnostics_contract.rs @@ -6,13 +6,7 @@ use tracedecay_domain::{ }; use tracedecay_store::{DiagnosticStoreError, SanitizedCleanDiagnosticSnapshotV1}; -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).expect("valid fixture identity") -} +use tracedecay_domain::test_fixtures::id; fn digest(byte: char) -> String { format!("sha256:{}", byte.to_string().repeat(64)) diff --git a/crates/tracedecay-store/tests/store_suite/storage_runtime_contract.rs b/crates/tracedecay-store/tests/store_suite/storage_runtime_contract.rs index cd882c109b..240785f68c 100644 --- a/crates/tracedecay-store/tests/store_suite/storage_runtime_contract.rs +++ b/crates/tracedecay-store/tests/store_suite/storage_runtime_contract.rs @@ -13,13 +13,7 @@ use tracedecay_domain::{ }; use tracedecay_store::*; -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: Debug, -{ - T::try_from(value.to_owned()).expect("fixture id is canonical") -} +use tracedecay_domain::test_fixtures::id; fn digest(byte: char) -> CommandDigestV1 { CommandDigestV1::new(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() diff --git a/crates/tracedecay/src/daemon/invocation_tests/work_evidence_journey_tests.rs b/crates/tracedecay/src/daemon/invocation_tests/work_evidence_journey_tests.rs index da1152b5b5..22cd39094d 100644 --- a/crates/tracedecay/src/daemon/invocation_tests/work_evidence_journey_tests.rs +++ b/crates/tracedecay/src/daemon/invocation_tests/work_evidence_journey_tests.rs @@ -30,13 +30,7 @@ use tracedecay_lsp::LspSessionRegistry; use tracedecay_session_memory::context::{BranchId, ProfileId, SessionRootId, SessionStoreId}; use tracedecay_tool_catalog::{CapabilityId, UseCaseId}; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).expect("Work evidence journey identity") -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay/src/daemon/tests/code_index_hydration.rs b/crates/tracedecay/src/daemon/tests/code_index_hydration.rs index 27908f88e8..3669b4cd63 100644 --- a/crates/tracedecay/src/daemon/tests/code_index_hydration.rs +++ b/crates/tracedecay/src/daemon/tests/code_index_hydration.rs @@ -25,13 +25,7 @@ use tracedecay_query::retrieval::hydrate::{ HydrationWorkPermitV1, LateHydrationSource, }; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).expect("valid fixture identity") -} +use tracedecay_domain::test_fixtures::id; fn request(max_hydration_bytes: u64) -> RetrievalRequest { RetrievalRequest { diff --git a/crates/tracedecay/tests/daemon_suite/advanced_workflow_journey_test.rs b/crates/tracedecay/tests/daemon_suite/advanced_workflow_journey_test.rs index a767245374..de244a27e0 100644 --- a/crates/tracedecay/tests/daemon_suite/advanced_workflow_journey_test.rs +++ b/crates/tracedecay/tests/daemon_suite/advanced_workflow_journey_test.rs @@ -81,13 +81,7 @@ const PROVIDER_TRANSCRIPT_ASSISTANT_MESSAGE_ID: &str = "message.advanced-workflo const PROVIDER_TRANSCRIPT_REFRESH_MESSAGE_ID: &str = "message.advanced-workflow-provider-participant-refresh"; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).expect("advanced workflow identity") -} +use tracedecay_domain::test_fixtures::id; fn run(command: &mut Command, operation: &str) -> Vec { let output = command diff --git a/crates/tracedecay/tests/daemon_suite/workflow_handoff_test.rs b/crates/tracedecay/tests/daemon_suite/workflow_handoff_test.rs index c92c3493f0..bf94accf02 100644 --- a/crates/tracedecay/tests/daemon_suite/workflow_handoff_test.rs +++ b/crates/tracedecay/tests/daemon_suite/workflow_handoff_test.rs @@ -19,13 +19,7 @@ use tracedecay_domain::{ WorkflowOutputName, WorkflowStep, WorkflowStepId, WorktreeId, canonical_sha256, }; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; /// A distinct, valid `sha256:`-tagged digest per input byte. /// diff --git a/crates/tracedecay/tests/product_surface_suite/api_application_parity.rs b/crates/tracedecay/tests/product_surface_suite/api_application_parity.rs index 9dc2e08a53..383bd019bf 100644 --- a/crates/tracedecay/tests/product_surface_suite/api_application_parity.rs +++ b/crates/tracedecay/tests/product_surface_suite/api_application_parity.rs @@ -803,13 +803,7 @@ fn git_requests() -> (ApplicationSurfaceRequest, ApplicationSurfaceRequest) { ) } -fn id(value: &str) -> T -where - T: TryFrom, - >::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).expect("fixture identity") -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay/tests/runtime_acceptance_suite/daemon_runtime_acceptance.rs b/crates/tracedecay/tests/runtime_acceptance_suite/daemon_runtime_acceptance.rs index b47025edff..2354a935a3 100644 --- a/crates/tracedecay/tests/runtime_acceptance_suite/daemon_runtime_acceptance.rs +++ b/crates/tracedecay/tests/runtime_acceptance_suite/daemon_runtime_acceptance.rs @@ -36,13 +36,7 @@ use tracedecay_tool_catalog::BindingSurface; use crate::common; -fn id(value: &str) -> T -where - T: TryFrom, - T::Error: std::fmt::Debug, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; use tracedecay_domain::test_fixtures::digest; diff --git a/crates/tracedecay/tests/session_suite/anchor_tombstone_expiry.rs b/crates/tracedecay/tests/session_suite/anchor_tombstone_expiry.rs index 08a64f868c..f680db7daf 100644 --- a/crates/tracedecay/tests/session_suite/anchor_tombstone_expiry.rs +++ b/crates/tracedecay/tests/session_suite/anchor_tombstone_expiry.rs @@ -31,12 +31,7 @@ use crate::common::open_graph_db_from_template; const DIGEST_A: &str = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; const DIGEST_B: &str = "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; -fn id(value: &str) -> T -where - T: TryFrom, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; fn owner() -> FactOwnerV1 { FactOwnerV1::Profile diff --git a/crates/tracedecay/tests/storage_suite/fact_merge_hydration_test.rs b/crates/tracedecay/tests/storage_suite/fact_merge_hydration_test.rs index 6c1dcd4f58..b3b3793c29 100644 --- a/crates/tracedecay/tests/storage_suite/fact_merge_hydration_test.rs +++ b/crates/tracedecay/tests/storage_suite/fact_merge_hydration_test.rs @@ -55,12 +55,7 @@ async fn setup_db() -> TestDb { TestDb { db, _dir: dir } } -fn id(value: &str) -> T -where - T: TryFrom, -{ - T::try_from(value.to_owned()).unwrap() -} +use tracedecay_domain::test_fixtures::id; fn sqlite_text_literal(value: &str) -> String { format!("'{}'", value.replace('\'', "''")) From 3a01dba248d88d5e2e5953936a296d8b230d2f3b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:45:46 +0000 Subject: [PATCH 152/182] refactor(pass-5/5): drop dashboard hooks reexport The module only forwarded tracedecay-hooks and the readiness installer. The composition root calls the analytics bridge directly. Co-authored-by: Zack Jackson --- Cargo.lock | 1 - crates/tracedecay-dashboard-api/Cargo.toml | 1 - crates/tracedecay-dashboard-api/src/hooks.rs | 7 ------- crates/tracedecay-dashboard-api/src/lib.rs | 1 - crates/tracedecay/src/hooks.rs | 10 ++++++---- 5 files changed, 6 insertions(+), 14 deletions(-) delete mode 100644 crates/tracedecay-dashboard-api/src/hooks.rs diff --git a/Cargo.lock b/Cargo.lock index b6be9ab4b9..1a7993aa8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5961,7 +5961,6 @@ dependencies = [ "tracedecay-global-db", "tracedecay-graph-db", "tracedecay-graph-query", - "tracedecay-hooks", "tracedecay-lsp", "tracedecay-privacy", "tracedecay-runtime-core", diff --git a/crates/tracedecay-dashboard-api/Cargo.toml b/crates/tracedecay-dashboard-api/Cargo.toml index c85f9419f5..042a8f1e64 100644 --- a/crates/tracedecay-dashboard-api/Cargo.toml +++ b/crates/tracedecay-dashboard-api/Cargo.toml @@ -32,7 +32,6 @@ tracedecay-domain = { path = "../tracedecay-domain", version = "0.1.0" } tracedecay-global-db = { path = "../tracedecay-global-db", version = "0.1.0" } tracedecay-graph-db = { path = "../tracedecay-graph-db", version = "0.1.0" } tracedecay-graph-query = { path = "../tracedecay-graph-query", version = "0.1.0" } -tracedecay-hooks = { path = "../tracedecay-hooks", version = "0.1.0" } tracedecay-lsp = { path = "../tracedecay-lsp", version = "0.1.0" } tracedecay-privacy = { path = "../tracedecay-privacy", version = "0.1.0" } tracedecay-runtime-core = { path = "../tracedecay-runtime-core", version = "0.1.0" } diff --git a/crates/tracedecay-dashboard-api/src/hooks.rs b/crates/tracedecay-dashboard-api/src/hooks.rs deleted file mode 100644 index 8c0ecb08e1..0000000000 --- a/crates/tracedecay-dashboard-api/src/hooks.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! Hook contracts plus the readiness projection installed by the composition root. - -pub use tracedecay_application::analytics_bridge::{ - HookReadinessProjectionPort, aggregate_hook_completed_readiness, - install_hook_readiness_projection, -}; -pub use tracedecay_hooks::*; diff --git a/crates/tracedecay-dashboard-api/src/lib.rs b/crates/tracedecay-dashboard-api/src/lib.rs index 4aaf17458e..9b77f7c611 100644 --- a/crates/tracedecay-dashboard-api/src/lib.rs +++ b/crates/tracedecay-dashboard-api/src/lib.rs @@ -145,7 +145,6 @@ pub mod feedback_api; mod graph_api; mod graph_service; mod graph_structure_api; -pub mod hooks; mod lcm_api; mod remote_status_api; pub use lcm_api::{ diff --git a/crates/tracedecay/src/hooks.rs b/crates/tracedecay/src/hooks.rs index 7d3932ccd0..cc19068413 100644 --- a/crates/tracedecay/src/hooks.rs +++ b/crates/tracedecay/src/hooks.rs @@ -7,7 +7,9 @@ use serde_json::Value; struct RootHookReadinessProjection; -impl tracedecay_dashboard_api::hooks::HookReadinessProjectionPort for RootHookReadinessProjection { +impl tracedecay_application::analytics_bridge::HookReadinessProjectionPort + for RootHookReadinessProjection +{ #[hotpath::measure(label = "hints.hook_aggregate")] fn aggregate_hook_completed_readiness(&self, rows: &[Value]) -> Value { let distribution = tracedecay_agent_hosts::hooks::aggregate_hook_completed_readiness(rows); @@ -39,9 +41,9 @@ pub(crate) fn install_dashboard_hook_readiness_projection() -> tracedecay_domain { static INSTALLATION: std::sync::LazyLock> = std::sync::LazyLock::new(|| { - tracedecay_dashboard_api::hooks::install_hook_readiness_projection(std::sync::Arc::new( - RootHookReadinessProjection, - )) + tracedecay_application::analytics_bridge::install_hook_readiness_projection( + std::sync::Arc::new(RootHookReadinessProjection), + ) .map_err(|_| "dashboard hook readiness projection is already installed".to_owned()) }); INSTALLATION From fd3e445117ba7a7124c439deef82fdfeb95d500d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:47:41 +0000 Subject: [PATCH 153/182] simplify(pass-5/5): share control-character folding Control characters become spaces in one helper. The fold does not trim, so leading and trailing spaces stay for the caller to decide. Co-authored-by: Zack Jackson --- .../src/diagnostics_publication.rs | 11 +------ .../tracedecay-contracts/src/doctor/report.rs | 11 +------ .../src/doctor/sources.rs | 11 +------ .../src/invocation/source_edit.rs | 11 +------ crates/tracedecay-domain/src/lib.rs | 3 +- crates/tracedecay-domain/src/text.rs | 31 ++++++++++++++++++- .../src/retention/diagnostics.rs | 5 +-- 7 files changed, 37 insertions(+), 46 deletions(-) diff --git a/crates/tracedecay-application/src/diagnostics_publication.rs b/crates/tracedecay-application/src/diagnostics_publication.rs index 09416efb3e..7c872a85ff 100644 --- a/crates/tracedecay-application/src/diagnostics_publication.rs +++ b/crates/tracedecay-application/src/diagnostics_publication.rs @@ -915,16 +915,7 @@ pub async fn publish_compiler_diagnostics_through_code_index_v1( /// a producer's text always satisfies `validate_sanitized_message`. #[must_use] pub fn bounded_notice(message: &str) -> String { - let collapsed: String = message - .chars() - .map(|character| { - if character.is_control() { - ' ' - } else { - character - } - }) - .collect(); + let collapsed = tracedecay_domain::fold_control_characters(message); let collapsed = collapsed.trim(); if collapsed.is_empty() { return "diagnostic reported without a message".to_owned(); diff --git a/crates/tracedecay-contracts/src/doctor/report.rs b/crates/tracedecay-contracts/src/doctor/report.rs index 314882fae8..2b22453cee 100644 --- a/crates/tracedecay-contracts/src/doctor/report.rs +++ b/crates/tracedecay-contracts/src/doctor/report.rs @@ -911,16 +911,7 @@ fn storage_unavailable( /// statement: control characters folded to spaces, trimmed, and bounded so the /// composed statement stays inside the statement contract. fn sanitized_detail(detail: &str) -> Option { - let folded: String = detail - .chars() - .map(|character| { - if character.is_control() { - ' ' - } else { - character - } - }) - .collect(); + let folded = tracedecay_domain::fold_control_characters(detail); let bounded = truncate_at_char_boundary(folded.trim(), PLACEHOLDER_DETAIL_MAX_BYTES); let bounded = bounded.trim(); if bounded.is_empty() { diff --git a/crates/tracedecay-contracts/src/doctor/sources.rs b/crates/tracedecay-contracts/src/doctor/sources.rs index 0570d75081..51b2d8a80d 100644 --- a/crates/tracedecay-contracts/src/doctor/sources.rs +++ b/crates/tracedecay-contracts/src/doctor/sources.rs @@ -77,16 +77,7 @@ fn source_finding( fn bounded_statement(statement: &str) -> String { const STATEMENT_LIMIT_BYTES: usize = 512; const TRUNCATION_MARK: &str = "…"; - let sanitized = statement - .chars() - .map(|character| { - if character.is_control() { - ' ' - } else { - character - } - }) - .collect::(); + let sanitized = tracedecay_domain::fold_control_characters(statement); let sanitized = sanitized.trim(); if sanitized.len() <= STATEMENT_LIMIT_BYTES { return sanitized.to_owned(); diff --git a/crates/tracedecay-daemon-service/src/invocation/source_edit.rs b/crates/tracedecay-daemon-service/src/invocation/source_edit.rs index 5c46f55268..1ac4031c8b 100644 --- a/crates/tracedecay-daemon-service/src/invocation/source_edit.rs +++ b/crates/tracedecay-daemon-service/src/invocation/source_edit.rs @@ -217,16 +217,7 @@ const SOURCE_EDIT_SYMBOL_EVIDENCE_UNAVAILABLE: &str = "source-edit-symbol-eviden const SOURCE_EDIT_DIAGNOSTICS_UNAVAILABLE: &str = "source_edit_diagnostics_unavailable"; fn sanitize_safe_diagnostic_text(value: &str, limit: usize) -> String { - let collapsed: String = value - .chars() - .map(|character| { - if character.is_control() { - ' ' - } else { - character - } - }) - .collect(); + let collapsed = tracedecay_domain::fold_control_characters(value); let trimmed = collapsed.trim(); if trimmed.is_empty() { return String::new(); diff --git a/crates/tracedecay-domain/src/lib.rs b/crates/tracedecay-domain/src/lib.rs index f746d5c7af..e936a21d1b 100644 --- a/crates/tracedecay-domain/src/lib.rs +++ b/crates/tracedecay-domain/src/lib.rs @@ -383,7 +383,8 @@ pub use session_derived::{ }; pub use source_path_policy::{GENERATED_DIR_SEGMENTS, is_generated_dir_segment}; pub use text::{ - collapse_whitespace, forward_slash_path, forward_slash_text, utf8_prefix_at_or_before, + collapse_whitespace, fold_control_characters, forward_slash_path, forward_slash_text, + utf8_prefix_at_or_before, }; pub use work::{RuntimeEvidenceRef, WorkAuthority, WorkContractError, WorkVersion}; pub use work_duplicate_adjudication::{ diff --git a/crates/tracedecay-domain/src/text.rs b/crates/tracedecay-domain/src/text.rs index d6c23705ee..a28a487b35 100644 --- a/crates/tracedecay-domain/src/text.rs +++ b/crates/tracedecay-domain/src/text.rs @@ -6,6 +6,25 @@ //! stays empty. Callers that trim, mark truncation, or refuse a mid-character //! budget still do that themselves. +/// Replace Unicode control characters with an ASCII space. +/// +/// Newline, carriage return, and tab are controls, so they become spaces. +/// Non-controls, including ordinary spaces and `/`, are unchanged. An empty +/// string stays empty. This does not trim. +#[must_use] +pub fn fold_control_characters(value: &str) -> String { + value + .chars() + .map(|character| { + if character.is_control() { + ' ' + } else { + character + } + }) + .collect() +} + /// Replace `\` with `/`. /// /// Trailing separators stay. `\` becomes `/`, `foo\` becomes `foo/`, and @@ -49,7 +68,17 @@ pub fn utf8_prefix_at_or_before(text: &str, max_bytes: usize) -> &str { #[cfg(test)] mod tests { - use super::{collapse_whitespace, forward_slash_text, utf8_prefix_at_or_before}; + use super::{ + collapse_whitespace, fold_control_characters, forward_slash_text, utf8_prefix_at_or_before, + }; + + #[test] + fn control_characters_become_spaces_without_trimming() { + assert_eq!(fold_control_characters(""), ""); + assert_eq!(fold_control_characters("a\nb\tc\r"), "a b c "); + assert_eq!(fold_control_characters(" src/ "), " src/ "); + assert_eq!(fold_control_characters("é"), "é"); + } #[test] fn forward_slashes_keep_trailing_separators_and_an_empty_string() { diff --git a/crates/tracedecay-maintenance/src/retention/diagnostics.rs b/crates/tracedecay-maintenance/src/retention/diagnostics.rs index 4516d3d07c..f4ca8f20f1 100644 --- a/crates/tracedecay-maintenance/src/retention/diagnostics.rs +++ b/crates/tracedecay-maintenance/src/retention/diagnostics.rs @@ -257,10 +257,7 @@ fn permits_synchronous_exhaustive_scan(root: &Path) -> bool { } fn bounded_statement(statement: &str) -> String { - let cleaned: String = statement - .chars() - .map(|ch| if ch.is_control() { ' ' } else { ch }) - .collect(); + let cleaned = tracedecay_domain::fold_control_characters(statement); let cleaned = cleaned.trim(); if cleaned.len() <= DOCTOR_TEXT_LIMIT { return cleaned.to_string(); From 957c20da25b17adae0f33dc2c92ca29e36118f73 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:47:44 +0000 Subject: [PATCH 154/182] test(fixtures): share repeating sha256 text String digest fixtures used the same sixty-four-digit spelling. They now alias the shared text constructor. Co-authored-by: Zack Jackson --- crates/tracedecay-application/src/diagnostics_publication.rs | 4 +--- crates/tracedecay-application/src/diagnostics_query.rs | 4 +--- crates/tracedecay-application/src/diagnostics_store.rs | 4 +--- .../src/lsp_runtime/diagnostic_admission_tests.rs | 4 +--- .../src/lsp_runtime/projection_identity_tests.rs | 4 +--- .../src/primitives/production/managed_test_scope_tests.rs | 4 +--- crates/tracedecay-code-index/src/chunks.rs | 4 +--- crates/tracedecay-domain/src/code_intelligence/index.rs | 4 +--- crates/tracedecay-domain/src/code_intelligence/search.rs | 4 +--- crates/tracedecay-domain/src/diagnostics.rs | 4 +--- .../src/repository/graph_publication/support.rs | 4 +--- .../src/repository/graph_publication/tests.rs | 4 +--- .../tests/rusqlite_suite/runtime_actor/support.rs | 4 +--- .../tests/rusqlite_suite/transactional_inbox.rs | 4 +--- crates/tracedecay-session-temporal-store/src/hydration.rs | 4 +--- .../src/hydration/file_stream_tests.rs | 4 +--- .../src/participant_freeze.rs | 4 +--- .../src/retrieval/graph_relation_tests.rs | 4 +--- .../tracedecay-session-temporal-store/src/retrieval/tests.rs | 4 +--- .../tracedecay-store/src/runtime/graph_publication/tests.rs | 4 +--- .../tests/store_suite/diagnostics_contract.rs | 4 +--- crates/tracedecay-temporal-query/src/cursor.rs | 4 +--- crates/tracedecay-temporal-query/src/hydration.rs | 4 +--- crates/tracedecay-temporal-query/src/ports/tests.rs | 4 +--- crates/tracedecay-temporal-query/src/tests.rs | 4 +--- 25 files changed, 25 insertions(+), 75 deletions(-) diff --git a/crates/tracedecay-application/src/diagnostics_publication.rs b/crates/tracedecay-application/src/diagnostics_publication.rs index 02fb885f69..20e35824b1 100644 --- a/crates/tracedecay-application/src/diagnostics_publication.rs +++ b/crates/tracedecay-application/src/diagnostics_publication.rs @@ -939,9 +939,7 @@ mod tests { use tracedecay_domain::test_fixtures::id; - fn digest(byte: char) -> String { - format!("sha256:{}", byte.to_string().repeat(64)) - } + use tracedecay_domain::test_fixtures::repeated_sha256_text as digest; pub(crate) fn scope(generation: &str) -> CleanGenerationDiagnosticScopeV1 { CleanGenerationDiagnosticScopeV1 { diff --git a/crates/tracedecay-application/src/diagnostics_query.rs b/crates/tracedecay-application/src/diagnostics_query.rs index 71c147a988..8488c21f52 100644 --- a/crates/tracedecay-application/src/diagnostics_query.rs +++ b/crates/tracedecay-application/src/diagnostics_query.rs @@ -912,9 +912,7 @@ mod tests { use tracedecay_domain::test_fixtures::id; - fn digest(byte: char) -> String { - format!("sha256:{}", byte.to_string().repeat(64)) - } + use tracedecay_domain::test_fixtures::repeated_sha256_text as digest; fn fixture_record(generation: &str, anchor: &str) -> GenerationDiagnosticV1 { let mut record = GenerationDiagnosticV1 { diff --git a/crates/tracedecay-application/src/diagnostics_store.rs b/crates/tracedecay-application/src/diagnostics_store.rs index 4a6eeaf937..6ed3251101 100644 --- a/crates/tracedecay-application/src/diagnostics_store.rs +++ b/crates/tracedecay-application/src/diagnostics_store.rs @@ -1620,9 +1620,7 @@ mod tests { use tracedecay_domain::test_fixtures::id; - fn digest(byte: char) -> String { - format!("sha256:{}", byte.to_string().repeat(64)) - } + use tracedecay_domain::test_fixtures::repeated_sha256_text as digest; fn fixture_record(generation: &str, anchor: &str) -> GenerationDiagnosticV1 { let mut record = GenerationDiagnosticV1 { diff --git a/crates/tracedecay-application/src/lsp_runtime/diagnostic_admission_tests.rs b/crates/tracedecay-application/src/lsp_runtime/diagnostic_admission_tests.rs index 7b14e1ca3d..4775dc26f4 100644 --- a/crates/tracedecay-application/src/lsp_runtime/diagnostic_admission_tests.rs +++ b/crates/tracedecay-application/src/lsp_runtime/diagnostic_admission_tests.rs @@ -10,9 +10,7 @@ use tracedecay_domain::{ use tracedecay_domain::test_fixtures::id; -fn digest(byte: char) -> String { - format!("sha256:{}", byte.to_string().repeat(64)) -} +use tracedecay_domain::test_fixtures::repeated_sha256_text as digest; /// Builds a record through the real production publication builder so the /// admission rules are exercised against records shaped exactly like the diff --git a/crates/tracedecay-application/src/lsp_runtime/projection_identity_tests.rs b/crates/tracedecay-application/src/lsp_runtime/projection_identity_tests.rs index d1b7c36489..06d62c45d4 100644 --- a/crates/tracedecay-application/src/lsp_runtime/projection_identity_tests.rs +++ b/crates/tracedecay-application/src/lsp_runtime/projection_identity_tests.rs @@ -6,9 +6,7 @@ use tracedecay_domain::{ }; use tracedecay_domain::test_fixtures::id; -fn digest(byte: char) -> String { - format!("sha256:{}", byte.to_string().repeat(64)) -} +use tracedecay_domain::test_fixtures::repeated_sha256_text as digest; fn scope() -> ResolvedScope { ResolvedScope::new( diff --git a/crates/tracedecay-application/src/primitives/production/managed_test_scope_tests.rs b/crates/tracedecay-application/src/primitives/production/managed_test_scope_tests.rs index 317bccdfc3..3704abf56a 100644 --- a/crates/tracedecay-application/src/primitives/production/managed_test_scope_tests.rs +++ b/crates/tracedecay-application/src/primitives/production/managed_test_scope_tests.rs @@ -11,9 +11,7 @@ use crate::lsp_runtime::{LspCodeIndexProjectionIdentity, LspCodeIndexProjectionI use tracedecay_domain::test_fixtures::id; -fn digest(byte: char) -> String { - format!("sha256:{}", byte.to_string().repeat(64)) -} +use tracedecay_domain::test_fixtures::repeated_sha256_text as digest; struct SealedIdentity; diff --git a/crates/tracedecay-code-index/src/chunks.rs b/crates/tracedecay-code-index/src/chunks.rs index 19960d8de3..c039ff4f90 100644 --- a/crates/tracedecay-code-index/src/chunks.rs +++ b/crates/tracedecay-code-index/src/chunks.rs @@ -2761,9 +2761,7 @@ mod tests { use tracedecay_domain::test_fixtures::id; - fn digest(byte: char) -> String { - format!("sha256:{}", byte.to_string().repeat(64)) - } + use tracedecay_domain::test_fixtures::repeated_sha256_text as digest; fn fixture_function_row( source: &str, diff --git a/crates/tracedecay-domain/src/code_intelligence/index.rs b/crates/tracedecay-domain/src/code_intelligence/index.rs index 3bcadbe98e..bed3d0a1bd 100644 --- a/crates/tracedecay-domain/src/code_intelligence/index.rs +++ b/crates/tracedecay-domain/src/code_intelligence/index.rs @@ -520,9 +520,7 @@ mod tests { use crate::test_fixtures::id; - fn digest(byte: char) -> String { - format!("sha256:{}", byte.to_string().repeat(64)) - } + use crate::test_fixtures::repeated_sha256_text as digest; fn snapshot() -> SanitizedCodeSnapshotV1 { SanitizedCodeSnapshotV1 { diff --git a/crates/tracedecay-domain/src/code_intelligence/search.rs b/crates/tracedecay-domain/src/code_intelligence/search.rs index dd1689c36c..4a1cda24ed 100644 --- a/crates/tracedecay-domain/src/code_intelligence/search.rs +++ b/crates/tracedecay-domain/src/code_intelligence/search.rs @@ -1583,9 +1583,7 @@ mod tests { use crate::test_fixtures::id; - fn digest(byte: char) -> String { - format!("sha256:{}", byte.to_string().repeat(64)) - } + use crate::test_fixtures::repeated_sha256_text as digest; #[test] fn ephemeral_query_view_is_bounded_and_redacts_its_text() { diff --git a/crates/tracedecay-domain/src/diagnostics.rs b/crates/tracedecay-domain/src/diagnostics.rs index effa6ba8ce..9dd29e8597 100644 --- a/crates/tracedecay-domain/src/diagnostics.rs +++ b/crates/tracedecay-domain/src/diagnostics.rs @@ -349,9 +349,7 @@ mod tests { use crate::test_fixtures::id; - fn digest(byte: char) -> String { - format!("sha256:{}", byte.to_string().repeat(64)) - } + use crate::test_fixtures::repeated_sha256_text as digest; fn fixture_record() -> GenerationDiagnosticV1 { let mut record = GenerationDiagnosticV1 { diff --git a/crates/tracedecay-rusqlite-runtime/src/repository/graph_publication/support.rs b/crates/tracedecay-rusqlite-runtime/src/repository/graph_publication/support.rs index 502d2252bd..361cc6b1a6 100644 --- a/crates/tracedecay-rusqlite-runtime/src/repository/graph_publication/support.rs +++ b/crates/tracedecay-rusqlite-runtime/src/repository/graph_publication/support.rs @@ -1509,9 +1509,7 @@ mod dependency_batch_tests { GraphPublicationOperationContextV1::new(&control, probe).unwrap() } - fn digest(byte: char) -> String { - format!("sha256:{}", byte.to_string().repeat(64)) - } + use tracedecay_domain::test_fixtures::repeated_sha256_text as digest; /// A valid hex digit character for `digest()`, cycling over `index`. /// Unlike a plain ASCII letter range, every value this returns is diff --git a/crates/tracedecay-rusqlite-runtime/src/repository/graph_publication/tests.rs b/crates/tracedecay-rusqlite-runtime/src/repository/graph_publication/tests.rs index 959e3cbb39..59f47ebe69 100644 --- a/crates/tracedecay-rusqlite-runtime/src/repository/graph_publication/tests.rs +++ b/crates/tracedecay-rusqlite-runtime/src/repository/graph_publication/tests.rs @@ -232,9 +232,7 @@ fn control_and_probe( ) } -fn digest(byte: char) -> String { - format!("sha256:{}", byte.to_string().repeat(64)) -} +use tracedecay_domain::test_fixtures::repeated_sha256_text as digest; fn projection(name: &str) -> GraphProjectionIdentityV1 { projection_for_project("project.fixture", name) diff --git a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/runtime_actor/support.rs b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/runtime_actor/support.rs index 2be587dbd5..0649a484be 100644 --- a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/runtime_actor/support.rs +++ b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/runtime_actor/support.rs @@ -55,9 +55,7 @@ impl TestBinding { } } -fn digest(byte: char) -> String { - format!("sha256:{}", byte.to_string().repeat(64)) -} +use tracedecay_domain::test_fixtures::repeated_sha256_text as digest; fn priority_name(priority: OperationPriorityV1) -> &'static str { match priority { diff --git a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/transactional_inbox.rs b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/transactional_inbox.rs index 5878f59d46..4a7c470d44 100644 --- a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/transactional_inbox.rs +++ b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/transactional_inbox.rs @@ -60,9 +60,7 @@ impl TestDatabase { } } -fn digest(byte: char) -> String { - format!("sha256:{}", byte.to_string().repeat(64)) -} +use tracedecay_domain::test_fixtures::repeated_sha256_text as digest; /// A real (non-mock) `StorageOperationExecutor`: it performs a genuine SQL /// insert against the savepoint `apply_inbox` hands it, keyed by the applied diff --git a/crates/tracedecay-session-temporal-store/src/hydration.rs b/crates/tracedecay-session-temporal-store/src/hydration.rs index 36447df97d..a2f44405f9 100644 --- a/crates/tracedecay-session-temporal-store/src/hydration.rs +++ b/crates/tracedecay-session-temporal-store/src/hydration.rs @@ -2150,9 +2150,7 @@ mod tests { RetrievalAnchorId::new("anchor-1").expect("anchor") } - fn digest(byte: char) -> String { - format!("sha256:{}", byte.to_string().repeat(64)) - } + use tracedecay_domain::test_fixtures::repeated_sha256_text as digest; fn snapshot(control: ExecutionControl) -> TemporalExecutionSnapshot { TemporalExecutionSnapshot::new_authorized( diff --git a/crates/tracedecay-session-temporal-store/src/hydration/file_stream_tests.rs b/crates/tracedecay-session-temporal-store/src/hydration/file_stream_tests.rs index 717e4d2c4e..15c8371552 100644 --- a/crates/tracedecay-session-temporal-store/src/hydration/file_stream_tests.rs +++ b/crates/tracedecay-session-temporal-store/src/hydration/file_stream_tests.rs @@ -252,9 +252,7 @@ impl TemporalHydrationBackend for ExternalPayloadBackend<'_> { } } -fn digest(byte: char) -> String { - format!("sha256:{}", byte.to_string().repeat(64)) -} +use tracedecay_domain::test_fixtures::repeated_sha256_text as digest; fn snapshot(control: ExecutionControl) -> TemporalExecutionSnapshot { let limits = ExecutionLimits::default(); diff --git a/crates/tracedecay-session-temporal-store/src/participant_freeze.rs b/crates/tracedecay-session-temporal-store/src/participant_freeze.rs index 7d5bd8ae3d..18c85fde4a 100644 --- a/crates/tracedecay-session-temporal-store/src/participant_freeze.rs +++ b/crates/tracedecay-session-temporal-store/src/participant_freeze.rs @@ -539,9 +539,7 @@ mod tests { .expect("valid authorized root") } - fn digest(byte: char) -> String { - format!("sha256:{}", byte.to_string().repeat(64)) - } + use tracedecay_domain::test_fixtures::repeated_sha256_text as digest; fn execution_request() -> AuthorizedTemporalExecutionRequest { let snapshot = TemporalSnapshotRequest::new( diff --git a/crates/tracedecay-session-temporal-store/src/retrieval/graph_relation_tests.rs b/crates/tracedecay-session-temporal-store/src/retrieval/graph_relation_tests.rs index 3ac93f8698..bd4f217013 100644 --- a/crates/tracedecay-session-temporal-store/src/retrieval/graph_relation_tests.rs +++ b/crates/tracedecay-session-temporal-store/src/retrieval/graph_relation_tests.rs @@ -23,9 +23,7 @@ use crate::relations::{ WorkflowAgentMembership, }; -fn digest(byte: char) -> String { - format!("sha256:{}", byte.to_string().repeat(64)) -} +use tracedecay_domain::test_fixtures::repeated_sha256_text as digest; fn project() -> ProjectId { ProjectId::new("project-retrieval").expect("project") diff --git a/crates/tracedecay-session-temporal-store/src/retrieval/tests.rs b/crates/tracedecay-session-temporal-store/src/retrieval/tests.rs index 7fc90ceb71..a31eee6b9b 100644 --- a/crates/tracedecay-session-temporal-store/src/retrieval/tests.rs +++ b/crates/tracedecay-session-temporal-store/src/retrieval/tests.rs @@ -52,9 +52,7 @@ fn normalize_plan_detail(detail: &str) -> String { .to_ascii_uppercase() } -fn digest(byte: char) -> String { - format!("sha256:{}", byte.to_string().repeat(64)) -} +use tracedecay_domain::test_fixtures::repeated_sha256_text as digest; fn snapshot(generation: u64) -> TemporalExecutionSnapshot { TemporalExecutionSnapshot::new_authorized( diff --git a/crates/tracedecay-store/src/runtime/graph_publication/tests.rs b/crates/tracedecay-store/src/runtime/graph_publication/tests.rs index a77b556305..b767cbd607 100644 --- a/crates/tracedecay-store/src/runtime/graph_publication/tests.rs +++ b/crates/tracedecay-store/src/runtime/graph_publication/tests.rs @@ -13,9 +13,7 @@ fn projection(project: &str) -> GraphProjectionIdentityV1 { } } -fn digest(byte: char) -> String { - format!("sha256:{}", byte.to_string().repeat(64)) -} +use tracedecay_domain::test_fixtures::repeated_sha256_text as digest; #[test] fn replay_payload_and_digests_are_closed_and_validated() { diff --git a/crates/tracedecay-store/tests/store_suite/diagnostics_contract.rs b/crates/tracedecay-store/tests/store_suite/diagnostics_contract.rs index db63bca347..2342cd64a0 100644 --- a/crates/tracedecay-store/tests/store_suite/diagnostics_contract.rs +++ b/crates/tracedecay-store/tests/store_suite/diagnostics_contract.rs @@ -8,9 +8,7 @@ use tracedecay_store::{DiagnosticStoreError, SanitizedCleanDiagnosticSnapshotV1} use tracedecay_domain::test_fixtures::id; -fn digest(byte: char) -> String { - format!("sha256:{}", byte.to_string().repeat(64)) -} +use tracedecay_domain::test_fixtures::repeated_sha256_text as digest; fn fixture_record(generation: &str, anchor: &str) -> GenerationDiagnosticV1 { let mut record = GenerationDiagnosticV1 { diff --git a/crates/tracedecay-temporal-query/src/cursor.rs b/crates/tracedecay-temporal-query/src/cursor.rs index 1107e844b6..5bf069e64f 100644 --- a/crates/tracedecay-temporal-query/src/cursor.rs +++ b/crates/tracedecay-temporal-query/src/cursor.rs @@ -525,9 +525,7 @@ mod tests { } } - fn digest(byte: char) -> String { - format!("sha256:{}", byte.to_string().repeat(64)) - } + use tracedecay_domain::test_fixtures::repeated_sha256_text as digest; fn snapshot_for(session: &str, access: char, projection: u64) -> TemporalExecutionSnapshot { snapshot_for_key(session, access, projection, "key-1", 1) diff --git a/crates/tracedecay-temporal-query/src/hydration.rs b/crates/tracedecay-temporal-query/src/hydration.rs index c247fa624b..6e9c3bdb91 100644 --- a/crates/tracedecay-temporal-query/src/hydration.rs +++ b/crates/tracedecay-temporal-query/src/hydration.rs @@ -327,9 +327,7 @@ mod tests { serde_json::from_str(&format!("\"{value}\"")).expect("valid anchor") } - fn digest(byte: char) -> String { - format!("sha256:{}", byte.to_string().repeat(64)) - } + use tracedecay_domain::test_fixtures::repeated_sha256_text as digest; fn snapshot() -> TemporalExecutionSnapshot { snapshot_with_limits(ExecutionLimits::default()) diff --git a/crates/tracedecay-temporal-query/src/ports/tests.rs b/crates/tracedecay-temporal-query/src/ports/tests.rs index 9a3a587d1e..82382e738f 100644 --- a/crates/tracedecay-temporal-query/src/ports/tests.rs +++ b/crates/tracedecay-temporal-query/src/ports/tests.rs @@ -23,9 +23,7 @@ fn session_id() -> SessionId { serde_json::from_str("\"session-1\"").expect("valid session id") } -fn digest(byte: char) -> String { - format!("sha256:{}", byte.to_string().repeat(64)) -} +use tracedecay_domain::test_fixtures::repeated_sha256_text as digest; fn participant(session: &str, source: &str, generation: u64) -> TemporalParticipantGeneration { TemporalParticipantGeneration::new( diff --git a/crates/tracedecay-temporal-query/src/tests.rs b/crates/tracedecay-temporal-query/src/tests.rs index ddf20a4d79..01ba6b2c06 100644 --- a/crates/tracedecay-temporal-query/src/tests.rs +++ b/crates/tracedecay-temporal-query/src/tests.rs @@ -229,9 +229,7 @@ impl VersionedTokenEstimator for Words { } } -fn digest(byte: char) -> String { - format!("sha256:{}", byte.to_string().repeat(64)) -} +use tracedecay_domain::test_fixtures::repeated_sha256_text as digest; fn anchor(value: &str) -> RetrievalAnchorId { RetrievalAnchorId::new(value).expect("valid anchor") From f5af7875e95d4c3e609d5bfa31949bd4a6bcb6a4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:48:41 +0000 Subject: [PATCH 155/182] simplify(global-db): drop uncalled orphan-relink apply Co-authored-by: Zack Jackson --- .../src/profile_registry_maintenance.rs | 14 - .../src/registry_maintenance.rs | 500 +----------------- .../src/stack_delivery.rs | 5 - .../profile_registry_test_support.rs | 30 -- 4 files changed, 2 insertions(+), 547 deletions(-) diff --git a/crates/tracedecay-global-db/src/profile_registry_maintenance.rs b/crates/tracedecay-global-db/src/profile_registry_maintenance.rs index 1d7a4cf163..fcf85b277b 100644 --- a/crates/tracedecay-global-db/src/profile_registry_maintenance.rs +++ b/crates/tracedecay-global-db/src/profile_registry_maintenance.rs @@ -10,8 +10,6 @@ use std::path::{Component, Path, PathBuf}; use crate::{ ProjectRegistryContext, RegisteredGlobalDb, RegisteredGlobalDbLeaseV1, registry_maintenance::ForgetRegistryProjectRows, registry_maintenance::RegistryGcReport, - registry_maintenance::RegistryOrphanRelinkApplyReport, - registry_maintenance::RegistryOrphanRelinkReport, registry_maintenance::forget_registry_project, }; @@ -246,18 +244,6 @@ impl ProfileRegistryMaintenanceRuntime { }) } - #[hotpath::measure(label = "daemon.profile_registry.apply_orphan_relink", future = true)] - pub async fn apply_orphan_relink( - &self, - report: &RegistryOrphanRelinkReport, - ) -> std::result::Result> { - crate::registry_maintenance::apply_registry_orphan_relink_report( - self.profile_database.as_ref(), - report, - ) - .await - } - #[hotpath::measure(label = "daemon.profile_registry.gc", future = true)] pub async fn registry_gc( &self, diff --git a/crates/tracedecay-global-db/src/registry_maintenance.rs b/crates/tracedecay-global-db/src/registry_maintenance.rs index 360e007303..a0665d2685 100644 --- a/crates/tracedecay-global-db/src/registry_maintenance.rs +++ b/crates/tracedecay-global-db/src/registry_maintenance.rs @@ -1,4 +1,4 @@ -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::BTreeSet; use std::fs; use std::path::{Component, Path, PathBuf}; @@ -9,7 +9,6 @@ use crate::{ RegisteredGlobalDbWriteTransaction, StoreArtifactUpsert, StoreInstanceUpsert, }; use tracedecay_runtime_core::branch_meta; -use tracedecay_runtime_core::db::engine::{Executor, IntoParams, QueryExecutor, params}; use tracedecay_runtime_core::storage::{ STORE_MANIFEST_FILENAME, STORE_MANIFEST_SCHEMA_VERSION, StorageMode, StoreKind, read_legacy_enrollment_marker, read_repository_identity_marker, read_store_manifest, @@ -56,15 +55,6 @@ pub struct RegistryOrphanRelinkReport { pub issues: Vec, } -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] -pub struct RegistryOrphanRelinkApplyReport { - pub projects: usize, - pub aliases: usize, - pub stores: usize, - pub graph_scopes: usize, - pub artifacts: usize, -} - /// Canonical read-only plan returned by the daemon and consumed by the /// `registry-gc` CLI. Apply fills only the deletion counters after executing /// the same plan under the active database mutation authority. @@ -95,320 +85,11 @@ impl RegistryGcReport { } } -fn encode_registry_identity( - value: &T, - label: impl std::fmt::Display, -) -> std::result::Result { - serde_json::to_string(value).map_err(|error| format!("could not encode {label}: {error}")) -} - -#[hotpath::measure(future = true, label = "global_db.registry_maintenance.persist")] -pub async fn apply_registry_orphan_relink_report( - db: &RegisteredGlobalDb, - report: &RegistryOrphanRelinkReport, -) -> std::result::Result> { - let transaction = db.begin_write_transaction().await.map_err(|error| { - vec![format!( - "could not start atomic registry orphan relink: {error}" - )] - })?; - let issues = preflight_registry_orphan_relink(&transaction, report).await; - if !issues.is_empty() { - return Err(issues); - } - let applied = apply_registry_orphan_relink_rows(&transaction, report) - .await - .map_err(|issue| vec![issue])?; - transaction.commit().await.map_err(|error| { - vec![format!( - "could not commit atomic registry orphan relink: {error}" - )] - })?; - Ok(applied) -} - #[cfg(test)] #[allow(clippy::unwrap_used, clippy::expect_used)] mod tests; -pub async fn apply_single_registry_orphan_relink_report( - db: &RegisteredGlobalDb, - report: &RegistryOrphanRelinkReport, -) -> std::result::Result> { - let [plan] = report.plans.as_slice() else { - return Err(vec![format!( - "migration cutover requires exactly one registry orphan relink plan, found {}", - report.plans.len() - )]); - }; - if plan.status != RegistryOrphanRelinkStatus::Eligible { - return Err(vec![format!( - "migration cutover registry orphan relink plan for '{}' is {:?}: {}", - plan.project.project_id, - plan.status, - plan.status_reason.as_deref().unwrap_or("not eligible") - )]); - } - apply_registry_orphan_relink_report(db, report).await -} - -async fn preflight_registry_orphan_relink( - conn: &Q, - report: &RegistryOrphanRelinkReport, -) -> Vec -where - Q: QueryExecutor + ?Sized, -{ - let mut issues = report.issues.clone(); - let mut project_roots = BTreeMap::::new(); - let mut aliases = BTreeMap::::new(); - let mut stores = BTreeMap::::new(); - let mut store_paths = BTreeMap::::new(); - let mut scopes = BTreeMap::::new(); - let mut scope_paths = BTreeMap::::new(); - - for plan in &report.plans { - match plan.status { - RegistryOrphanRelinkStatus::Eligible => {} - RegistryOrphanRelinkStatus::Stale | RegistryOrphanRelinkStatus::Retired => { - continue; - } - RegistryOrphanRelinkStatus::Blocked => { - issues.push(format!( - "{} reconstruction plan for '{}' is blocked: {}", - plan.manifest_path.display(), - plan.project.project_id, - plan.status_reason.as_deref().unwrap_or("not eligible") - )); - continue; - } - } - let project = &plan.project; - let root = RegisteredGlobalDb::canonical_project_key(&project.project_root); - let root_alias = RegisteredGlobalDb::project_path_alias_key(&project.project_root); - record_batch_owner( - &mut project_roots, - &root_alias, - &project.project_id, - "canonical project root", - &mut issues, - ); - match query_optional_text( - conn, - "SELECT canonical_root FROM code_projects WHERE project_id=?1", - params![project.project_id.as_str()], - ) - .await - { - Ok(Some(existing)) if existing != root => issues.push(format!( - "project '{}' already owns canonical root '{}' instead of '{}'", - project.project_id, existing, root - )), - Err(error) => issues.push(error), - _ => {} - } - match query_all_text( - conn, - "SELECT project_id FROM project_aliases WHERE alias_path=?1", - params![root_alias.as_str()], - ) - .await - { - Ok(owners) => { - for owner in owners { - if owner != project.project_id { - issues.push(format!( - "canonical root '{root}' is already owned by project '{owner}'" - )); - } - } - } - Err(error) => issues.push(error), - } - for alias in &project.aliases { - let alias = RegisteredGlobalDb::project_path_alias_key(alias); - record_batch_owner( - &mut aliases, - &alias, - &project.project_id, - "project alias", - &mut issues, - ); - match query_optional_text( - conn, - "SELECT project_id FROM project_aliases WHERE alias_path=?1", - params![alias.as_str()], - ) - .await - { - Ok(Some(owner)) if owner != project.project_id => issues.push(format!( - "alias '{alias}' is already owned by project '{owner}'" - )), - Err(error) => issues.push(error), - _ => {} - } - } - - let store_identity = match encode_registry_identity( - &( - &plan.store.project_id, - &plan.store.store_kind, - &plan.store.storage_mode, - &plan.store.store_relpath, - &plan.store.manifest_relpath, - ), - format!("store '{}'", plan.store.store_id), - ) { - Ok(identity) => identity, - Err(error) => { - issues.push(error); - continue; - } - }; - record_batch_owner( - &mut stores, - &plan.store.store_id, - &store_identity, - "store id", - &mut issues, - ); - match query_optional_text( - conn, - "SELECT json_array(project_id, store_kind, storage_mode, store_relpath, manifest_relpath) - FROM store_instances WHERE store_id=?1", - params![plan.store.store_id.as_str()], - ) - .await - { - Ok(Some(existing)) if existing != store_identity => issues.push(format!( - "store '{}' already has conflicting ownership or location", - plan.store.store_id - )), - Err(error) => issues.push(error), - _ => {} - } - for physical_path in std::iter::once(plan.store.store_relpath.as_str()) - .chain(plan.store.manifest_relpath.as_deref()) - { - record_batch_owner( - &mut store_paths, - physical_path, - &plan.store.store_id, - "physical store path", - &mut issues, - ); - match query_all_text( - conn, - "SELECT store_id FROM store_instances - WHERE store_relpath=?1 OR manifest_relpath=?1", - params![physical_path], - ) - .await - { - Ok(owners) => { - for owner in owners { - if owner != plan.store.store_id { - issues.push(format!( - "physical store path '{physical_path}' is already owned by store '{owner}'" - )); - } - } - } - Err(error) => issues.push(error), - } - } - - for scope in &plan.graph_scopes { - let scope_identity = match encode_registry_identity( - &( - &scope.project_id, - &scope.store_id, - &scope.branch_name, - &scope.db_relpath, - &scope.parent_scope_id, - ), - format!("graph scope '{}'", scope.graph_scope_id), - ) { - Ok(identity) => identity, - Err(error) => { - issues.push(error); - continue; - } - }; - record_batch_owner( - &mut scopes, - &scope.graph_scope_id, - &scope_identity, - "graph scope id", - &mut issues, - ); - match query_optional_text( - conn, - "SELECT json_array(project_id, store_id, branch_name, db_relpath, parent_scope_id) - FROM graph_scopes WHERE graph_scope_id=?1", - params![scope.graph_scope_id.as_str()], - ) - .await - { - Ok(Some(existing)) - if existing != scope_identity - && !graph_scope_location_drift_is_repairable(&existing, scope) => - { - issues.push(format!( - "graph scope '{}' already has conflicting ownership", - scope.graph_scope_id - )); - } - Err(error) => issues.push(error), - _ => {} - } - record_batch_owner( - &mut scope_paths, - &scope.db_relpath, - &scope.graph_scope_id, - "physical graph database path", - &mut issues, - ); - match query_all_text( - conn, - "SELECT graph_scope_id FROM graph_scopes WHERE db_relpath=?1", - params![scope.db_relpath.as_str()], - ) - .await - { - Ok(owners) => { - for owner in owners { - if owner != scope.graph_scope_id { - issues.push(format!( - "physical graph database path '{}' is already owned by scope '{}'", - scope.db_relpath, owner - )); - } - } - } - Err(error) => issues.push(error), - } - } - } - issues -} - -fn record_batch_owner( - owners: &mut BTreeMap, - key: &str, - owner: &str, - label: &str, - issues: &mut Vec, -) { - if let Some(existing) = owners.insert(key.to_string(), owner.to_string()) - && existing != owner - { - issues.push(format!( - "{label} '{key}' has conflicting batch owners '{existing}' and '{owner}'" - )); - } -} - +#[cfg(test)] fn graph_scope_location_drift_is_repairable(existing: &str, expected: &GraphScopeUpsert) -> bool { serde_json::from_str::<(String, String, String, String, Option)>(existing).is_ok_and( |(project_id, store_id, branch_name, _, _)| { @@ -418,180 +99,3 @@ fn graph_scope_location_drift_is_repairable(existing: &str, expected: &GraphScop }, ) } - -async fn query_optional_text( - conn: &Q, - sql: &str, - params: P, -) -> std::result::Result, String> -where - Q: QueryExecutor + ?Sized, - P: IntoParams, -{ - let mut rows = conn - .query(sql, params) - .await - .map_err(|error| format!("registry orphan relink preflight query failed: {error}"))?; - rows.next() - .await - .map_err(|error| format!("registry orphan relink preflight row failed: {error}"))? - .map(|row| { - row.get::(0) - .map_err(|error| format!("registry orphan relink preflight value failed: {error}")) - }) - .transpose() -} - -async fn query_all_text( - conn: &Q, - sql: &str, - params: P, -) -> std::result::Result, String> -where - Q: QueryExecutor + ?Sized, - P: IntoParams, -{ - let mut rows = conn - .query(sql, params) - .await - .map_err(|error| format!("registry orphan relink preflight query failed: {error}"))?; - let mut values = Vec::new(); - while let Some(row) = rows - .next() - .await - .map_err(|error| format!("registry orphan relink preflight row failed: {error}"))? - { - values.push( - row.get::(0).map_err(|error| { - format!("registry orphan relink preflight value failed: {error}") - })?, - ); - } - Ok(values) -} - -async fn apply_registry_orphan_relink_rows( - conn: &E, - report: &RegistryOrphanRelinkReport, -) -> std::result::Result -where - E: Executor + ?Sized, -{ - let mut applied = RegistryOrphanRelinkApplyReport::default(); - let now = tracedecay_runtime_core::tracedecay::current_timestamp(); - for plan in &report.plans { - if plan.status != RegistryOrphanRelinkStatus::Eligible { - continue; - } - let project = &plan.project; - let canonical_root = RegisteredGlobalDb::canonical_project_key(&project.project_root); - applied.projects += usize::try_from( - conn.execute( - "INSERT OR IGNORE INTO code_projects( - project_id, canonical_root, display_root, git_common_dir, git_remote_url, - default_branch, created_at, last_seen_at - ) VALUES(?1, ?2, ?3, NULL, NULL, ?4, ?5, ?5)", - params![ - project.project_id.as_str(), - canonical_root, - project.project_root.to_string_lossy().to_string(), - project.default_branch.as_deref(), - now, - ], - ) - .await - .map_err(|error| format!("failed to insert code project: {error}"))?, - ) - .unwrap_or(usize::MAX); - for alias in &project.aliases { - applied.aliases += usize::try_from( - conn.execute( - "INSERT OR IGNORE INTO project_aliases(alias_path, project_id, last_seen_at) - VALUES(?1, ?2, ?3)", - params![ - RegisteredGlobalDb::project_path_alias_key(alias), - project.project_id.as_str(), - now, - ], - ) - .await - .map_err(|error| format!("failed to insert project alias: {error}"))?, - ) - .unwrap_or(usize::MAX); - } - applied.stores += usize::try_from( - conn.execute( - "INSERT OR IGNORE INTO store_instances( - store_id, project_id, store_kind, storage_mode, store_relpath, - manifest_relpath, created_at, last_verified_at, last_write_at - ) VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", - params![ - plan.store.store_id.as_str(), - plan.store.project_id.as_str(), - plan.store.store_kind.as_str(), - plan.store.storage_mode.as_str(), - plan.store.store_relpath.as_str(), - plan.store.manifest_relpath.as_deref(), - now, - plan.store.last_verified_at, - plan.store.last_write_at, - ], - ) - .await - .map_err(|error| format!("failed to insert store instance: {error}"))?, - ) - .unwrap_or(usize::MAX); - for scope in &plan.graph_scopes { - applied.graph_scopes += usize::try_from( - conn.execute( - "INSERT INTO graph_scopes( - graph_scope_id, project_id, store_id, branch_name, db_relpath, - parent_scope_id, last_synced_at, writable - ) VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8) - ON CONFLICT(graph_scope_id) DO UPDATE SET - project_id = excluded.project_id, - store_id = excluded.store_id, - branch_name = excluded.branch_name, - db_relpath = excluded.db_relpath, - parent_scope_id = excluded.parent_scope_id, - last_synced_at = excluded.last_synced_at, - writable = excluded.writable", - params![ - scope.graph_scope_id.as_str(), - scope.project_id.as_str(), - scope.store_id.as_str(), - scope.branch_name.as_str(), - scope.db_relpath.as_str(), - scope.parent_scope_id.as_deref(), - scope.last_synced_at, - i64::from(scope.writable), - ], - ) - .await - .map_err(|error| format!("failed to insert graph scope: {error}"))?, - ) - .unwrap_or(usize::MAX); - } - for artifact in &plan.artifacts { - applied.artifacts += usize::try_from( - conn.execute( - "INSERT OR IGNORE INTO store_artifacts( - store_id, artifact_kind, relpath, size_bytes, schema_version, updated_at - ) VALUES(?1, ?2, ?3, ?4, ?5, ?6)", - params![ - artifact.store_id.as_str(), - artifact.artifact_kind.as_str(), - artifact.relpath.as_str(), - artifact.size_bytes, - artifact.schema_version.as_deref(), - artifact.updated_at, - ], - ) - .await - .map_err(|error| format!("failed to insert store artifact: {error}"))?, - ) - .unwrap_or(usize::MAX); - } - } - Ok(applied) -} diff --git a/crates/tracedecay-global-db/src/stack_delivery.rs b/crates/tracedecay-global-db/src/stack_delivery.rs index ffedfbeb15..1e0df42ad0 100644 --- a/crates/tracedecay-global-db/src/stack_delivery.rs +++ b/crates/tracedecay-global-db/src/stack_delivery.rs @@ -118,11 +118,6 @@ pub enum GitHubStackSignalAppendOutcomeV1 { } impl GitHubStackSignalAppendOutcomeV1 { - #[hotpath::skip] - pub const fn is_saturated(&self) -> bool { - matches!(self, Self::Saturated { .. }) - } - #[hotpath::skip] pub const fn pending_count(&self) -> usize { match self { diff --git a/crates/tracedecay-project/src/test_support/host_admission/profile_registry_test_support.rs b/crates/tracedecay-project/src/test_support/host_admission/profile_registry_test_support.rs index 688dedc544..8fed6e5cc8 100644 --- a/crates/tracedecay-project/src/test_support/host_admission/profile_registry_test_support.rs +++ b/crates/tracedecay-project/src/test_support/host_admission/profile_registry_test_support.rs @@ -217,36 +217,6 @@ impl HostAdmissionTestRuntimeV1 { self.profile_database.apply_registry_reap(&plan).await } - #[doc(hidden)] - pub async fn apply_registry_orphan_relink_report( - &self, - report: &tracedecay_global_db::registry_maintenance::RegistryOrphanRelinkReport, - ) -> std::result::Result< - tracedecay_global_db::registry_maintenance::RegistryOrphanRelinkApplyReport, - Vec, - > { - tracedecay_global_db::registry_maintenance::apply_registry_orphan_relink_report( - self.profile_database.as_ref(), - report, - ) - .await - } - - #[doc(hidden)] - pub async fn apply_single_registry_orphan_relink_report( - &self, - report: &tracedecay_global_db::registry_maintenance::RegistryOrphanRelinkReport, - ) -> std::result::Result< - tracedecay_global_db::registry_maintenance::RegistryOrphanRelinkApplyReport, - Vec, - > { - tracedecay_global_db::registry_maintenance::apply_single_registry_orphan_relink_report( - self.profile_database.as_ref(), - report, - ) - .await - } - #[doc(hidden)] pub async fn upsert_graph_scope( &self, From 07527f8b355e8920245cef690969428a2728f611 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:49:05 +0000 Subject: [PATCH 156/182] refactor(daemon): reuse protocol connect helpers The restart grace, response bound, and connect-failure advice already lived on the daemon protocol. The client now uses those instead of a second copy. Co-authored-by: Zack Jackson --- crates/tracedecay/src/daemon/core_client.rs | 63 +++------------------ 1 file changed, 7 insertions(+), 56 deletions(-) diff --git a/crates/tracedecay/src/daemon/core_client.rs b/crates/tracedecay/src/daemon/core_client.rs index efefbe4471..8533f51a5b 100644 --- a/crates/tracedecay/src/daemon/core_client.rs +++ b/crates/tracedecay/src/daemon/core_client.rs @@ -15,8 +15,13 @@ use tracedecay_daemon_identity::current_daemon_connection; use tracedecay_daemon_identity::{ResolvedDaemonConnection, client_connection}; use tracedecay_framing::{BoundedLineReader, WIRE_RECORD_TOO_LARGE, is_wire_oversized_io_error}; +pub(crate) use tracedecay_daemon_protocol::connection::{ + DAEMON_RESTART_GRACE, DAEMON_RESTART_POLL_INTERVAL, daemon_connect_failure_advice, + is_transient_daemon_connect_error, +}; pub(crate) use tracedecay_daemon_protocol::DAEMON_TOOL_LIVENESS_POLL_INTERVAL; -use tracedecay_daemon_protocol::{DAEMON_TOOL_RESPONSE_GRACE, tool_request_deadline}; +pub use tracedecay_daemon_protocol::daemon_tool_response_bound; +use tracedecay_daemon_protocol::tool_request_deadline; #[cfg(unix)] use super::unavailable_error; @@ -26,28 +31,6 @@ use super::{ TraceDecayError, error_is_project_open_retryable, tool_call_transport_error_is_retryable, }; -/// Bounded grace a client keeps reading for *after* the caller's request -/// deadline has elapsed. -/// -/// The request deadline belongs to the daemon: it is what admission measures -/// and what the retained owners settle against, and its whole purpose is to -/// produce a typed terminal, a `PartialEffect` carrying a committed receipt, a -/// typed timeout, rather than silence. Bounding the client's *read* by that -/// same instant made every one of those terminals unobservable through this -/// transport: the client abandoned the connection moments before the envelope -/// it had asked for arrived and reported "outcome may be unknown" while the -/// outcome was already on the wire. The read bound must therefore outlive the -/// request deadline; this is by how much. It bounds only a dead or wedged -/// daemon, never the request. -/// The local read bound for a request whose caller deadline is `request_deadline`. -pub fn daemon_tool_response_bound(request_deadline: Instant) -> Result { - request_deadline - .checked_add(DAEMON_TOOL_RESPONSE_GRACE) - .ok_or_else(|| TraceDecayError::Config { - message: "daemon tool response bound exceeds the supported monotonic range".to_string(), - }) -} - /// The caller's request deadline as an absolute wall-clock instant, for the /// wire. /// @@ -65,17 +48,6 @@ fn wire_request_deadline_micros(request_deadline: Instant) -> tracedecay_domain: ) } -/// How long daemon clients keep retrying a failed connect before giving up. -/// -/// An explicit restart, or an update of a service that was already running, -/// briefly unlinks the socket before the replacement binds it. Connects in -/// that bounded window fail with `NotFound` or `ConnectionRefused`. Long-lived -/// MCP sessions (Cursor's `tracedecay serve` stdio proxy) reconnect per request -/// so a live session can ride out replacement without surfacing a hard -/// JSON-RPC error. This grace does not start an intentionally held service. -pub(crate) const DAEMON_RESTART_GRACE: Duration = Duration::from_secs(8); -pub(crate) const DAEMON_RESTART_POLL_INTERVAL: Duration = Duration::from_millis(200); - /// How long a liveness probe waits for the daemon endpoint to accept a /// connection before the in-flight request is declared unreachable. const DAEMON_TOOL_HEALTH_CONNECT_TIMEOUT: Duration = Duration::from_secs(1); @@ -191,27 +163,6 @@ pub(crate) fn default_available_socket_path() -> Result { } } -pub(crate) fn is_transient_daemon_connect_error(kind: std::io::ErrorKind) -> bool { - matches!( - kind, - std::io::ErrorKind::NotFound - | std::io::ErrorKind::ConnectionRefused - | std::io::ErrorKind::WouldBlock - ) -} - -pub(crate) fn is_saturated_daemon_connect_error(kind: std::io::ErrorKind) -> bool { - kind == std::io::ErrorKind::WouldBlock -} - -pub(crate) fn daemon_connect_failure_advice(kind: std::io::ErrorKind) -> &'static str { - if is_saturated_daemon_connect_error(kind) { - "The daemon is up but not accepting connections, likely overloaded. Retry shortly, or check `tracedecay daemon status`." - } else { - "The daemon may be restarting (e.g. after `tracedecay update`). Retry shortly, or check `tracedecay daemon status`." - } -} - pub(crate) async fn connect_to_current_daemon_within( socket_path: &Path, client_deadline: Option, @@ -442,7 +393,7 @@ pub async fn call_tool( /// Calls a daemon tool with `deadline` as the *caller's request deadline*. /// /// The deadline is sent to the daemon, which enforces it; the local read runs -/// on that deadline plus [`DAEMON_TOOL_RESPONSE_GRACE`] so a deadline-elapsed +/// on that deadline plus [`tracedecay_daemon_protocol::DAEMON_TOOL_RESPONSE_GRACE`] so a deadline-elapsed /// typed terminal is read rather than raced. pub async fn call_tool_within( socket_path: &Path, From b9fa4375c36534840a5f1e8d1a5c2622fb060cf9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:49:16 +0000 Subject: [PATCH 157/182] refactor(daemon): share the handshake preamble writer Auth preface plus handshake line is one protocol write. The composition client keeps its connection type and calls that writer. Co-authored-by: Zack Jackson --- .../src/connection.rs | 24 +++++++++++++++---- crates/tracedecay-daemon-protocol/src/lib.rs | 2 +- crates/tracedecay/src/daemon/core_client.rs | 21 +++++++--------- 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/crates/tracedecay-daemon-protocol/src/connection.rs b/crates/tracedecay-daemon-protocol/src/connection.rs index 3ef6739f83..a48210b860 100644 --- a/crates/tracedecay-daemon-protocol/src/connection.rs +++ b/crates/tracedecay-daemon-protocol/src/connection.rs @@ -209,13 +209,18 @@ where } } -#[hotpath::measure(label = "daemon_protocol.client.preamble", future = true)] -pub async fn write_daemon_preamble( - writer: &mut tokio::io::WriteHalf, - connection: &DaemonConnection, +/// Writes the optional auth preface and the handshake line. +/// +/// Callers that already hold a [`DaemonConnection`] use +/// [`write_daemon_preamble`]. The composition-root client uses this directly +/// because its connection type is the authority record, not the protocol +/// connection. +pub async fn write_daemon_handshake_preamble( + writer: &mut (impl tokio::io::AsyncWrite + Unpin), + auth_token: Option<&str>, handshake: &DaemonHandshake, ) -> Result<()> { - if let Some(token) = connection.auth_token.as_deref() { + if let Some(token) = auth_token { writer .write_all(DaemonAuthPreface::new(token).to_line()?.as_bytes()) .await?; @@ -226,6 +231,15 @@ pub async fn write_daemon_preamble( Ok(()) } +#[hotpath::measure(label = "daemon_protocol.client.preamble", future = true)] +pub async fn write_daemon_preamble( + writer: &mut tokio::io::WriteHalf, + connection: &DaemonConnection, + handshake: &DaemonHandshake, +) -> Result<()> { + write_daemon_handshake_preamble(writer, connection.auth_token.as_deref(), handshake).await +} + pub fn is_transient_daemon_connect_error(kind: std::io::ErrorKind) -> bool { matches!( kind, diff --git a/crates/tracedecay-daemon-protocol/src/lib.rs b/crates/tracedecay-daemon-protocol/src/lib.rs index 10b4fe309b..dd4a4c682b 100644 --- a/crates/tracedecay-daemon-protocol/src/lib.rs +++ b/crates/tracedecay-daemon-protocol/src/lib.rs @@ -81,7 +81,7 @@ pub use connection::{ DaemonLivenessProbe, MAX_TOOL_REQUEST_DEADLINE, TOOL_REQUEST_DEADLINE_ENV, connect_to_daemon_connection, daemon_connect_failure, daemon_response_stalled, daemon_response_stalled_during, daemon_tool_response_bound, next_daemon_response_line, - tool_request_deadline, write_daemon_preamble, + tool_request_deadline, write_daemon_handshake_preamble, write_daemon_preamble, }; pub use contract::{ DAEMON_INVOCATION_PROTOCOL, DAEMON_INVOCATION_REVISION, DAEMON_SHUTDOWN_METHOD, diff --git a/crates/tracedecay/src/daemon/core_client.rs b/crates/tracedecay/src/daemon/core_client.rs index 8533f51a5b..4dd992010e 100644 --- a/crates/tracedecay/src/daemon/core_client.rs +++ b/crates/tracedecay/src/daemon/core_client.rs @@ -15,19 +15,19 @@ use tracedecay_daemon_identity::current_daemon_connection; use tracedecay_daemon_identity::{ResolvedDaemonConnection, client_connection}; use tracedecay_framing::{BoundedLineReader, WIRE_RECORD_TOO_LARGE, is_wire_oversized_io_error}; +pub(crate) use tracedecay_daemon_protocol::DAEMON_TOOL_LIVENESS_POLL_INTERVAL; pub(crate) use tracedecay_daemon_protocol::connection::{ DAEMON_RESTART_GRACE, DAEMON_RESTART_POLL_INTERVAL, daemon_connect_failure_advice, is_transient_daemon_connect_error, }; -pub(crate) use tracedecay_daemon_protocol::DAEMON_TOOL_LIVENESS_POLL_INTERVAL; pub use tracedecay_daemon_protocol::daemon_tool_response_bound; use tracedecay_daemon_protocol::tool_request_deadline; #[cfg(unix)] use super::unavailable_error; use super::{ - BrokerStream, DaemonAuthPreface, DaemonClientDeadline, DaemonHandshake, JsonRpcError, - JsonRpcRequest, JsonRpcResponse, PROJECT_OPEN_RETRY_GRACE, PROJECT_OPEN_RETRY_INTERVAL, Result, + BrokerStream, DaemonClientDeadline, DaemonHandshake, JsonRpcError, JsonRpcRequest, + JsonRpcResponse, PROJECT_OPEN_RETRY_GRACE, PROJECT_OPEN_RETRY_INTERVAL, Result, TraceDecayError, error_is_project_open_retryable, tool_call_transport_error_is_retryable, }; @@ -135,15 +135,12 @@ pub(crate) async fn write_daemon_preamble( connection: &ResolvedDaemonConnection, handshake: &DaemonHandshake, ) -> Result<()> { - if let Some(token) = connection.auth_token.as_deref() { - writer - .write_all(DaemonAuthPreface::new(token).to_line()?.as_bytes()) - .await?; - writer.write_all(b"\n").await?; - } - writer.write_all(handshake.to_line()?.as_bytes()).await?; - writer.write_all(b"\n").await?; - Ok(()) + tracedecay_daemon_protocol::write_daemon_handshake_preamble( + writer, + connection.auth_token.as_deref(), + handshake, + ) + .await } pub(crate) fn default_available_socket_path() -> Result { From c0019bd8aaed7ca59c7b30493210167e956e6775 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:49:40 +0000 Subject: [PATCH 158/182] refactor(daemon): share bounded response reads The protocol and composition clients polled liveness around the same bounded line read. One reader takes the liveness check; each caller keeps its own probe and measurement label. Co-authored-by: Zack Jackson --- .../src/connection.rs | 30 ++++++++++++++--- crates/tracedecay-daemon-protocol/src/lib.rs | 3 +- crates/tracedecay/src/daemon/core_client.rs | 32 ++++--------------- 3 files changed, 35 insertions(+), 30 deletions(-) diff --git a/crates/tracedecay-daemon-protocol/src/connection.rs b/crates/tracedecay-daemon-protocol/src/connection.rs index a48210b860..0f1556d9f2 100644 --- a/crates/tracedecay-daemon-protocol/src/connection.rs +++ b/crates/tracedecay-daemon-protocol/src/connection.rs @@ -176,15 +176,21 @@ pub async fn ensure_daemon_connection_live( Ok(()) } -#[hotpath::measure(label = "daemon_protocol.client.response.wait", future = true)] -pub async fn next_daemon_response_line( +/// Reads the next daemon frame, polling `ensure_live` whenever the read is +/// still pending. +/// +/// The reader stays held across polls. `read_mcp_line` is dropped when the +/// poll wins `select!`; the accumulator lives on the line reader. +pub async fn poll_daemon_response_line( reader: &mut R, - connection: &DaemonConnection, request_label: &str, liveness_poll_interval: Duration, + mut ensure_live: F, ) -> Result> where R: tokio::io::AsyncBufRead + Unpin, + F: FnMut() -> Fut, + Fut: std::future::Future>, { let mut line_reader = BoundedLineReader::new(reader); loop { @@ -203,12 +209,28 @@ where }; } () = tokio::time::sleep(liveness_poll_interval) => { - ensure_daemon_connection_live(connection, request_label).await?; + ensure_live().await?; } } } } +#[hotpath::measure(label = "daemon_protocol.client.response.wait", future = true)] +pub async fn next_daemon_response_line( + reader: &mut R, + connection: &DaemonConnection, + request_label: &str, + liveness_poll_interval: Duration, +) -> Result> +where + R: tokio::io::AsyncBufRead + Unpin, +{ + poll_daemon_response_line(reader, request_label, liveness_poll_interval, || { + ensure_daemon_connection_live(connection, request_label) + }) + .await +} + /// Writes the optional auth preface and the handshake line. /// /// Callers that already hold a [`DaemonConnection`] use diff --git a/crates/tracedecay-daemon-protocol/src/lib.rs b/crates/tracedecay-daemon-protocol/src/lib.rs index dd4a4c682b..eed99da37a 100644 --- a/crates/tracedecay-daemon-protocol/src/lib.rs +++ b/crates/tracedecay-daemon-protocol/src/lib.rs @@ -81,7 +81,8 @@ pub use connection::{ DaemonLivenessProbe, MAX_TOOL_REQUEST_DEADLINE, TOOL_REQUEST_DEADLINE_ENV, connect_to_daemon_connection, daemon_connect_failure, daemon_response_stalled, daemon_response_stalled_during, daemon_tool_response_bound, next_daemon_response_line, - tool_request_deadline, write_daemon_handshake_preamble, write_daemon_preamble, + poll_daemon_response_line, tool_request_deadline, write_daemon_handshake_preamble, + write_daemon_preamble, }; pub use contract::{ DAEMON_INVOCATION_PROTOCOL, DAEMON_INVOCATION_REVISION, DAEMON_SHUTDOWN_METHOD, diff --git a/crates/tracedecay/src/daemon/core_client.rs b/crates/tracedecay/src/daemon/core_client.rs index 4dd992010e..6cbd946672 100644 --- a/crates/tracedecay/src/daemon/core_client.rs +++ b/crates/tracedecay/src/daemon/core_client.rs @@ -13,8 +13,6 @@ use tracedecay_daemon_control::default_socket_path; #[cfg(not(unix))] use tracedecay_daemon_identity::current_daemon_connection; use tracedecay_daemon_identity::{ResolvedDaemonConnection, client_connection}; -use tracedecay_framing::{BoundedLineReader, WIRE_RECORD_TOO_LARGE, is_wire_oversized_io_error}; - pub(crate) use tracedecay_daemon_protocol::DAEMON_TOOL_LIVENESS_POLL_INTERVAL; pub(crate) use tracedecay_daemon_protocol::connection::{ DAEMON_RESTART_GRACE, DAEMON_RESTART_POLL_INTERVAL, daemon_connect_failure_advice, @@ -105,29 +103,13 @@ pub(crate) async fn next_daemon_response_line( where R: tokio::io::AsyncBufRead + Unpin, { - // Hold the reader across liveness polls. `read_mcp_line` is dropped when - // the poll wins `select!`; the accumulator lives on `line_reader`. - let mut line_reader = BoundedLineReader::new(reader); - loop { - tokio::select! { - result = line_reader.read_mcp_line() => { - return match result { - Ok(line) => Ok(line), - Err(error) if is_wire_oversized_io_error(&error) => { - Err(TraceDecayError::Config { - message: format!( - "daemon {request_label} response exceeded wire message bound ({WIRE_RECORD_TOO_LARGE})" - ), - }) - } - Err(error) => Err(error.into()), - }; - } - () = tokio::time::sleep(liveness_poll_interval) => { - ensure_daemon_connection_live(connection, request_label).await?; - } - } - } + tracedecay_daemon_protocol::poll_daemon_response_line( + reader, + request_label, + liveness_poll_interval, + || ensure_daemon_connection_live(connection, request_label), + ) + .await } pub(crate) async fn write_daemon_preamble( From 5566f21cd6ebf34d8eb88d0b40fc327dd40a894e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:49:40 +0000 Subject: [PATCH 159/182] refactor(catalog): share the public route projection Executable bindings expose one public-route accessor. SDK projection and registered HTTP mounts use it instead of repeating the exposure match. Co-authored-by: Zack Jackson --- .../tracedecay-contracts/src/sdk_catalog.rs | 8 +--- .../application_surface/registered_http.rs | 8 ++-- .../tracedecay-tool-catalog/src/executable.rs | 44 ++++++++++++------- 3 files changed, 34 insertions(+), 26 deletions(-) diff --git a/crates/tracedecay-contracts/src/sdk_catalog.rs b/crates/tracedecay-contracts/src/sdk_catalog.rs index c7b3b9376d..00bd3f1c89 100644 --- a/crates/tracedecay-contracts/src/sdk_catalog.rs +++ b/crates/tracedecay-contracts/src/sdk_catalog.rs @@ -228,11 +228,7 @@ fn project_http_binding( disposition: unavailable_disposition(availability), }); }; - let RouteExposureV1::Public { - binding_id, - route_path, - } = executable.exposure() - else { + let Some((binding_id, route_path)) = executable.public_route() else { return Ok(SdkExecutableBindingAvailabilityV1::Unavailable { operation_id: executable.operation_id().clone(), disposition: ExecutableUnavailableDispositionV1::RouteUnavailable, @@ -244,7 +240,7 @@ fn project_http_binding( binding_id.clone(), sdk_method, SdkTransportBindingV1::Http { - route_path: route_path.clone(), + route_path: route_path.to_owned(), }, )?; Ok(SdkExecutableBindingAvailabilityV1::available(binding)) diff --git a/crates/tracedecay-daemon-service/src/application_surface/registered_http.rs b/crates/tracedecay-daemon-service/src/application_surface/registered_http.rs index 596f93f10b..cc7fae76b3 100644 --- a/crates/tracedecay-daemon-service/src/application_surface/registered_http.rs +++ b/crates/tracedecay-daemon-service/src/application_surface/registered_http.rs @@ -1,3 +1,4 @@ +use super::problems::{application_contract_error_response, registered_adapter_unavailable}; use axum::response::Response; use serde::Serialize; use tracedecay_api::{ @@ -11,9 +12,6 @@ use tracedecay_contracts::{ use tracedecay_daemon_protocol::{ ApplicationSurfaceAdapterError, DaemonInvocationError, InvocationCancellationPolicy, }; -use tracedecay_tool_catalog::RouteExposureV1; - -use super::problems::{application_contract_error_response, registered_adapter_unavailable}; pub(crate) trait RegisteredHttpOperation: Copy { fn operation_id(self) -> String; @@ -248,7 +246,7 @@ where &format!("The {family} operation is not advertised by this build"), ); }; - let RouteExposureV1::Public { binding_id, .. } = binding.exposure() else { + let Some((binding_id, _)) = binding.public_route() else { return registered_adapter_unavailable( request_id, &problem_code("route_unavailable"), @@ -332,7 +330,7 @@ where &format!("The {family} operation is not advertised by this build"), ); }; - let RouteExposureV1::Public { binding_id, .. } = binding.exposure() else { + let Some((binding_id, _)) = binding.public_route() else { return registered_adapter_unavailable( request_id, &problem_code("route_unavailable"), diff --git a/crates/tracedecay-tool-catalog/src/executable.rs b/crates/tracedecay-tool-catalog/src/executable.rs index d5d4c23fa1..164b7d938c 100644 --- a/crates/tracedecay-tool-catalog/src/executable.rs +++ b/crates/tracedecay-tool-catalog/src/executable.rs @@ -364,6 +364,19 @@ impl ExecutableBindingV1 { &self.exposure } + /// The catalog route when this binding is public. Internal composition + /// returns `None` so a missing route and a private binding stay indistinguishable + /// to adapters. + pub fn public_route(&self) -> Option<(&BindingId, &str)> { + match &self.exposure { + RouteExposureV1::Public { + binding_id, + route_path, + } => Some((binding_id, route_path.as_str())), + RouteExposureV1::Internal => None, + } + } + pub const fn effect(&self) -> EffectClass { self.effect } @@ -547,27 +560,28 @@ impl SdkExecutableBindingV1 { transport: SdkTransportBindingV1, ) -> Result { transport.validate()?; - match (&transport, executable.exposure()) { - ( - SdkTransportBindingV1::Http { route_path }, - RouteExposureV1::Public { - binding_id: executable_binding_id, - route_path: executable_route_path, - }, - ) if binding_id == *executable_binding_id && route_path == executable_route_path => {} - (SdkTransportBindingV1::Http { .. }, _) => { - return Err(CatalogValidationError::InvalidValue { - field: "SDK HTTP binding", - reason: "must exactly match the executable public route", - }); + match &transport { + SdkTransportBindingV1::Http { route_path } => { + let matches_public = executable.public_route().is_some_and( + |(executable_binding_id, executable_route_path)| { + &binding_id == executable_binding_id + && route_path.as_str() == executable_route_path + }, + ); + if !matches_public { + return Err(CatalogValidationError::InvalidValue { + field: "SDK HTTP binding", + reason: "must exactly match the executable public route", + }); + } } - (SdkTransportBindingV1::McpTool { .. }, RouteExposureV1::Internal) => {} - (SdkTransportBindingV1::McpTool { .. }, RouteExposureV1::Public { .. }) => { + SdkTransportBindingV1::McpTool { .. } if executable.public_route().is_some() => { return Err(CatalogValidationError::InvalidValue { field: "SDK MCP binding", reason: "must not alias an HTTP executable route", }); } + SdkTransportBindingV1::McpTool { .. } => {} } Ok(Self { executable, From 4e6d7eacc9126882020cba50b2ea2068fc515d22 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:49:41 +0000 Subject: [PATCH 160/182] refactor(wire): share request keys and envelopes MCP connection request keys, outcome payloads, and problem summaries each had a private copy. Callers now use the contract helpers. Co-authored-by: Zack Jackson --- crates/tracedecay-cli/src/remote_command.rs | 9 +++--- crates/tracedecay-cli/src/work_cli.rs | 12 ++------ .../src/request_identity.rs | 9 ++++++ .../src/result/envelope.rs | 17 +++++++++++ .../tracedecay-mcp/src/server/connection.rs | 6 +--- .../src/daemon/connection_serving.rs | 16 ++-------- crates/tracedecay/src/doctor.rs | 2 +- crates/tracedecay/src/mcp/server.rs | 29 ++----------------- 8 files changed, 41 insertions(+), 59 deletions(-) diff --git a/crates/tracedecay-cli/src/remote_command.rs b/crates/tracedecay-cli/src/remote_command.rs index 614814735b..4f19838feb 100644 --- a/crates/tracedecay-cli/src/remote_command.rs +++ b/crates/tracedecay-cli/src/remote_command.rs @@ -296,8 +296,9 @@ fn protocol_exit_status(response: &RemoteProtocolResponseV1) -> Result<()> Ok(_) => Ok(()), Err(problem) => Err(TraceDecayError::Config { message: format!( - "Remote Brain request {} failed: {}: {}", - response.request_id, problem.problem.code, problem.problem.message + "Remote Brain request {} failed: {}", + response.request_id, + problem.problem.summary() ), }), } @@ -483,7 +484,7 @@ Recovery required: {}\n", fn render_protocol_response(response: &RemoteProtocolResponseV1) -> String { let outcome = match &response.result { Ok(_) => "ok".to_owned(), - Err(problem) => format!("{}: {}", problem.problem.code, problem.problem.message), + Err(problem) => problem.problem.summary(), }; format!( "Request: {}\nAuthority: {}\nOutcome: {}\n", @@ -628,7 +629,7 @@ mod tests { let Err(problem) = &response.result else { panic!("expected typed protocol problem"); }; - let outcome = format!("{}: {}", problem.problem.code, problem.problem.message); + let outcome = problem.problem.summary(); assert_eq!( render_protocol_response(&response), format!("Request: request.cli.remote.7\nAuthority: unavailable\nOutcome: {outcome}\n") diff --git a/crates/tracedecay-cli/src/work_cli.rs b/crates/tracedecay-cli/src/work_cli.rs index cdd3dc967e..5fac738e77 100644 --- a/crates/tracedecay-cli/src/work_cli.rs +++ b/crates/tracedecay-cli/src/work_cli.rs @@ -454,10 +454,10 @@ fn work_delivery_is_eligible(operation: WorkOperation, outcome: &WorkApplication (WorkOperation::StartAttempt, WorkApplicationOutcomeV1::StartAttempt(outcome)) | (WorkOperation::AttemptStatus, WorkApplicationOutcomeV1::AttemptStatus(outcome)) | (WorkOperation::CancelAttempt, WorkApplicationOutcomeV1::CancelAttempt(outcome)) => { - application_outcome_payload(outcome).is_some() + outcome.payload().is_some() } (WorkOperation::HydrateArtifacts, WorkApplicationOutcomeV1::HydrateArtifacts(outcome)) => { - application_outcome_payload(outcome).is_some_and(|hydration| { + outcome.payload().is_some_and(|hydration| { matches!( hydration, tracedecay_contracts::WorkArtifactHydrationV1::Hydrated { attempts, .. } @@ -469,14 +469,6 @@ fn work_delivery_is_eligible(operation: WorkOperation, outcome: &WorkApplication } } -fn application_outcome_payload(outcome: &ApplicationOutcome) -> Option<&T> { - match outcome { - ApplicationOutcome::Evidence(result) => result.payload.as_ref(), - ApplicationOutcome::Preview(result) => result.payload.as_ref(), - ApplicationOutcome::Effect(result) => result.payload.as_ref(), - } -} - fn erase_work_outcome(outcome: WorkApplicationOutcomeV1) -> Result> { let outcome = match outcome { WorkApplicationOutcomeV1::GenerateProposal(outcome) => serde_json::to_value(outcome), diff --git a/crates/tracedecay-contracts/src/request_identity.rs b/crates/tracedecay-contracts/src/request_identity.rs index b68a4ea378..a89e3deba2 100644 --- a/crates/tracedecay-contracts/src/request_identity.rs +++ b/crates/tracedecay-contracts/src/request_identity.rs @@ -317,6 +317,11 @@ pub fn mcp_connection_request_id(id: &Value, connection_scope: &str) -> Option Option { + mcp_connection_request_id(id, connection_scope).map(|request_id| request_id.as_str().to_owned()) +} + pub struct McpConnectionIdentityAuthority { instance_id: Option, next_connection: AtomicU64, @@ -468,6 +473,10 @@ mod tests { "request.mcp.connection.6b86b273ff34fce19d6b804eff5a3f57" ); assert!(mcp_connection_request_id(&Value::Null, "connection").is_none()); + assert_eq!( + mcp_connection_request_key(&json!(1), "connection").as_deref(), + Some(numeric.as_str()) + ); } #[test] diff --git a/crates/tracedecay-contracts/src/result/envelope.rs b/crates/tracedecay-contracts/src/result/envelope.rs index 9377fc40a8..033e460bf0 100644 --- a/crates/tracedecay-contracts/src/result/envelope.rs +++ b/crates/tracedecay-contracts/src/result/envelope.rs @@ -93,6 +93,16 @@ pub enum ApplicationOutcome { Effect(EffectResult), } +impl ApplicationOutcome { + pub fn payload(&self) -> Option<&T> { + match self { + Self::Evidence(result) => result.payload.as_ref(), + Self::Preview(result) => result.payload.as_ref(), + Self::Effect(result) => result.payload.as_ref(), + } + } +} + /// Successful application result with a stable contract, request, and scope. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(deny_unknown_fields)] @@ -183,6 +193,13 @@ pub struct ApplicationProblemRecord { source: ApplicationProblem, } +impl ApplicationProblemRecord { + /// `code: message`, the single line adapters print for a refusal. + pub fn summary(&self) -> String { + format!("{}: {}", self.code, self.message) + } +} + impl<'de> Deserialize<'de> for ApplicationProblemRecord { fn deserialize(deserializer: D) -> Result where diff --git a/crates/tracedecay-mcp/src/server/connection.rs b/crates/tracedecay-mcp/src/server/connection.rs index fbcca9de68..2215675f5f 100644 --- a/crates/tracedecay-mcp/src/server/connection.rs +++ b/crates/tracedecay-mcp/src/server/connection.rs @@ -12,6 +12,7 @@ use serde_json::Value; use crate::lifecycle::{McpConnectionLifecyclePort, McpRequestActivity}; use crate::transport::{McpTransport, write_wire_oversized_rejection}; use crate::{ErrorCode, JsonRpcRequest, JsonRpcResponse, serialize_response_line}; +use tracedecay_contracts::request_identity::mcp_connection_request_key as application_surface_request_id; use tracedecay_domain::errors::{Result, TraceDecayError}; use tracedecay_framing::is_wire_oversized_io_error; @@ -306,11 +307,6 @@ where request.id.clone() } -fn application_surface_request_id(id: &Value, connection_scope: &str) -> Option { - tracedecay_contracts::request_identity::mcp_connection_request_id(id, connection_scope) - .map(|request_id| request_id.as_str().to_owned()) -} - fn queued_cancellable_request_key( pending_lines: &VecDeque, request_id: &Value, diff --git a/crates/tracedecay/src/daemon/connection_serving.rs b/crates/tracedecay/src/daemon/connection_serving.rs index 4d3d087f86..f2f731b87d 100644 --- a/crates/tracedecay/src/daemon/connection_serving.rs +++ b/crates/tracedecay/src/daemon/connection_serving.rs @@ -478,14 +478,14 @@ impl DaemonWorkDeliveryDescriptorV1 { | WorkApplicationOutcomeV1::CancelAttempt(outcome), .. }, - ) => application_outcome_payload(outcome).is_some(), + ) => outcome.payload().is_some(), ( DaemonWorkDeliveryKindV1::ArtifactPage, DaemonInvocationOutcome::WorkApplication { outcome: WorkApplicationOutcomeV1::HydrateArtifacts(outcome), .. }, - ) => application_outcome_payload(outcome).is_some_and(|hydration| { + ) => outcome.payload().is_some_and(|hydration| { matches!( hydration, tracedecay_contracts::WorkArtifactHydrationV1::Hydrated { attempts, .. } @@ -566,7 +566,7 @@ impl DaemonWorkDeliveryDescriptorV1 { return Vec::new(); }; let Some(tracedecay_contracts::WorkArtifactHydrationV1::Hydrated { attempts, .. }) = - application_outcome_payload(outcome) + outcome.payload() else { return Vec::new(); }; @@ -577,16 +577,6 @@ impl DaemonWorkDeliveryDescriptorV1 { } } -fn application_outcome_payload( - outcome: &tracedecay_contracts::ApplicationOutcome, -) -> Option<&T> { - match outcome { - tracedecay_contracts::ApplicationOutcome::Evidence(result) => result.payload.as_ref(), - tracedecay_contracts::ApplicationOutcome::Preview(result) => result.payload.as_ref(), - tracedecay_contracts::ApplicationOutcome::Effect(result) => result.payload.as_ref(), - } -} - fn offer_daemon_work_delivery( recorder: Option< &Arc, diff --git a/crates/tracedecay/src/doctor.rs b/crates/tracedecay/src/doctor.rs index 635f1ff8a7..e3413d8d77 100644 --- a/crates/tracedecay/src/doctor.rs +++ b/crates/tracedecay/src/doctor.rs @@ -819,7 +819,7 @@ async fn configured_upload_enabled(project_path: &Path) -> tracedecay_domain::er let envelope = result.result.map_err( |problem| tracedecay_domain::errors::TraceDecayError::Config { - message: format!("{}: {}", problem.problem.code, problem.problem.message), + message: problem.problem.summary(), }, )?; let ApplicationOutcome::Evidence(evidence) = envelope.outcome else { diff --git a/crates/tracedecay/src/mcp/server.rs b/crates/tracedecay/src/mcp/server.rs index dc12bdfb59..972b9b990f 100644 --- a/crates/tracedecay/src/mcp/server.rs +++ b/crates/tracedecay/src/mcp/server.rs @@ -19,7 +19,9 @@ pub(crate) use tracedecay_code_index_runtime::code_index_scheduler::{ use tracedecay_contracts::code_index_freshness::{ CODE_INDEX_PUBLICATION_AUTHORITY_CORRUPT, CodeIndexConvergenceParkedV1, }; -use tracedecay_contracts::request_identity::McpConnectionIdentityAuthority; +use tracedecay_contracts::request_identity::{ + McpConnectionIdentityAuthority, mcp_connection_request_key as application_surface_request_id, +}; use tracedecay_domain::errors::{Result, TraceDecayError}; use tracedecay_global_db::RegisteredGlobalDbLeaseV1; use tracedecay_host_admission::TerminalReason; @@ -1499,34 +1501,9 @@ fn json_rpc_request_id_string(id: &Value) -> Option { } } -fn application_surface_request_id(id: &Value, connection_scope: &str) -> Option { - tracedecay_contracts::request_identity::mcp_connection_request_id(id, connection_scope) - .map(|request_id| request_id.as_str().to_owned()) -} - #[cfg(test)] mod cancel_candidate_journey; -#[cfg(test)] -mod application_surface_request_id_tests { - use serde_json::json; - - use super::application_surface_request_id; - - #[test] - fn request_id_hash_preserves_json_rpc_id_type() { - let numeric = application_surface_request_id(&json!(1), "connection").unwrap(); - let string = application_surface_request_id(&json!("1"), "connection").unwrap(); - - assert_ne!(numeric, string); - assert_eq!( - numeric, - application_surface_request_id(&json!(1), "connection").unwrap() - ); - assert!(application_surface_request_id(&json!(null), "connection").is_none()); - } -} - #[cfg(test)] #[allow(clippy::unwrap_used, clippy::expect_used)] mod work_evidence_mount_tests; From b32c04fa2a4dbe5c13d56db5477be03c0362e881 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:50:15 +0000 Subject: [PATCH 161/182] simplify(sessions): drop uncalled store forwarders Co-authored-by: Zack Jackson --- .../src/automation/session_reflector.rs | 4 +-- .../src/registered_lcm.rs | 29 --------------- crates/tracedecay-global-db/src/transcript.rs | 17 +-------- .../src/memory/trust.rs | 18 ---------- .../src/runtime/hosts/cursor_composer.rs | 18 +--------- .../src/runtime/store_access/lcm.rs | 36 ------------------- .../src/runtime/store_access/transcript.rs | 21 ----------- 7 files changed, 4 insertions(+), 139 deletions(-) diff --git a/crates/tracedecay-automation-runtime/src/automation/session_reflector.rs b/crates/tracedecay-automation-runtime/src/automation/session_reflector.rs index 735f5fa528..b3cf56a682 100644 --- a/crates/tracedecay-automation-runtime/src/automation/session_reflector.rs +++ b/crates/tracedecay-automation-runtime/src/automation/session_reflector.rs @@ -446,8 +446,8 @@ fn session_fact_category(category: &str) -> Option { /// Accepts numeric trust in `[0, 1]` plus the `low`/`medium`/`high` bucket /// labels models frequently emit despite the numeric prompt instruction. -/// Buckets map to the representative scores defined next to -/// [`tracedecay_session_memory::memory::trust::trust_bucket`], so they cannot drift out of their +/// Buckets map to the representative scores in +/// [`tracedecay_session_memory::memory::trust`], so they cannot drift out of their /// documented ranges. /// /// Deliberate decision: the prompt forbids string labels, but they are diff --git a/crates/tracedecay-global-db/src/registered_lcm.rs b/crates/tracedecay-global-db/src/registered_lcm.rs index d19382697a..982d5484cf 100644 --- a/crates/tracedecay-global-db/src/registered_lcm.rs +++ b/crates/tracedecay-global-db/src/registered_lcm.rs @@ -497,35 +497,6 @@ impl RegisteredGlobalDb { Ok((work, has_more)) } - #[hotpath::skip] - pub async fn lcm_payload_health_detail( - &self, - storage_root: &Path, - provider: &str, - session_id: Option<&str>, - deep: bool, - sample_limit: usize, - cfg: &LcmGcConfig, - ) -> Result { - SessionStoreAccess::new(self) - .lcm_payload_health_detail(storage_root, provider, session_id, deep, sample_limit, cfg) - .await - } - - #[hotpath::skip] - pub async fn lcm_preview_payload_gc( - &self, - storage_root: &Path, - provider: &str, - session_id: Option<&str>, - cfg: &LcmGcConfig, - now: i64, - ) -> Result { - SessionStoreAccess::new(self) - .lcm_preview_payload_gc(storage_root, provider, session_id, cfg, now) - .await - } - #[hotpath::skip] pub async fn lcm_run_payload_gc_apply( &self, diff --git a/crates/tracedecay-global-db/src/transcript.rs b/crates/tracedecay-global-db/src/transcript.rs index 811eb42268..53e1b6f633 100644 --- a/crates/tracedecay-global-db/src/transcript.rs +++ b/crates/tracedecay-global-db/src/transcript.rs @@ -1,4 +1,4 @@ -use super::{ParseOffset, RegisteredGlobalDb, TranscriptBatch}; +use super::{ParseOffset, RegisteredGlobalDb}; use tracedecay_sessions::runtime::{ SessionMessageRecord, SessionRecord, SessionStoreAccess, TranscriptGitEvidence, TranscriptPersistenceError, @@ -102,21 +102,6 @@ impl RegisteredGlobalDb { .await } - #[hotpath::measure( - future = true, - label = "global_db.transcript.upsert_projection_batches" - )] - pub async fn upsert_transcript_projection_batches( - &self, - batches: &[TranscriptBatch], - parse_offset_path: &str, - parse_offset: ParseOffset, - ) -> Result<(), String> { - SessionStoreAccess::new(self) - .upsert_transcript_projection_batches(batches, parse_offset_path, parse_offset) - .await - } - #[hotpath::measure(future = true, label = "global_db.transcript.get_parse_offset")] pub async fn get_parse_offset(&self, path: &str) -> Option { SessionStoreAccess::new(self).get_parse_offset(path).await diff --git a/crates/tracedecay-session-memory/src/memory/trust.rs b/crates/tracedecay-session-memory/src/memory/trust.rs index 0930dc15ee..110b5dc47f 100644 --- a/crates/tracedecay-session-memory/src/memory/trust.rs +++ b/crates/tracedecay-session-memory/src/memory/trust.rs @@ -4,26 +4,8 @@ pub const TRUST_MIN: f64 = 0.0; pub const TRUST_MAX: f64 = 1.0; pub const DEFAULT_TRUST: f64 = 0.5; pub const DEFAULT_MIN_TRUST: f64 = 0.3; -/// Lower bound of the "high" bucket in [`trust_bucket`]; scores in -/// `[DEFAULT_MIN_TRUST, HIGH_TRUST_THRESHOLD)` are "medium". -pub(crate) const HIGH_TRUST_THRESHOLD: f64 = 0.75; /// Representative score for a "low" trust label, inside the low bucket. pub const LOW_TRUST_REPRESENTATIVE: f64 = 0.15; /// Representative score for a "high" trust label, inside the high bucket. /// `DEFAULT_TRUST` is the representative for "medium". pub const HIGH_TRUST_REPRESENTATIVE: f64 = 0.85; - -pub fn clamp_trust(score: f64) -> f64 { - score.clamp(TRUST_MIN, TRUST_MAX) -} - -pub fn trust_bucket(score: f64) -> &'static str { - let clamped = clamp_trust(score); - if clamped < DEFAULT_MIN_TRUST { - "low" - } else if clamped < HIGH_TRUST_THRESHOLD { - "medium" - } else { - "high" - } -} diff --git a/crates/tracedecay-sessions/src/runtime/hosts/cursor_composer.rs b/crates/tracedecay-sessions/src/runtime/hosts/cursor_composer.rs index f1585afec6..f683ebfa08 100644 --- a/crates/tracedecay-sessions/src/runtime/hosts/cursor_composer.rs +++ b/crates/tracedecay-sessions/src/runtime/hosts/cursor_composer.rs @@ -111,23 +111,7 @@ impl CursorComposerSource { .await } - #[hotpath::skip] - pub async fn ingest_user( - &self, - admission: &dyn crate::admission::HostAdmission, - registered_roots: &[std::path::PathBuf], - envelope_cap: usize, - ) -> CursorComposerSweepResult { - self.ingest_user_capped( - admission, - registered_roots, - envelope_cap, - Some(sqlite::DEFAULT_COMPOSER_SWEEP_BYTES), - ) - .await - } - - /// [`Self::ingest_user`] with an aggregate serialized-payload byte budget. + /// User-scope ingest with an aggregate serialized-payload byte budget. #[hotpath::skip] pub async fn ingest_user_capped( &self, diff --git a/crates/tracedecay-sessions/src/runtime/store_access/lcm.rs b/crates/tracedecay-sessions/src/runtime/store_access/lcm.rs index 0890e5275f..9abf793d50 100644 --- a/crates/tracedecay-sessions/src/runtime/store_access/lcm.rs +++ b/crates/tracedecay-sessions/src/runtime/store_access/lcm.rs @@ -320,42 +320,6 @@ impl<'a, D: SessionRegisteredDb + Sync> SessionStoreAccess<'a, D> { compression::preflight(&snapshot, request).await } - #[hotpath::skip] - pub async fn lcm_payload_health_detail( - &self, - storage_root: &Path, - provider: &str, - session_id: Option<&str>, - deep: bool, - sample_limit: usize, - cfg: &LcmGcConfig, - ) -> Result { - let snapshot = self.lcm_read_snapshot().await?; - query::payload_health_detail( - &snapshot, - storage_root, - provider, - session_id, - deep, - sample_limit, - cfg, - ) - .await - } - - #[hotpath::skip] - pub async fn lcm_preview_payload_gc( - &self, - storage_root: &Path, - provider: &str, - session_id: Option<&str>, - cfg: &LcmGcConfig, - now: i64, - ) -> Result { - let snapshot = self.lcm_read_snapshot().await?; - gc::run_payload_gc(&snapshot, storage_root, provider, session_id, cfg, now).await - } - #[hotpath::skip] pub async fn lcm_run_payload_gc_apply( &self, diff --git a/crates/tracedecay-sessions/src/runtime/store_access/transcript.rs b/crates/tracedecay-sessions/src/runtime/store_access/transcript.rs index ec37288060..31372f64f5 100644 --- a/crates/tracedecay-sessions/src/runtime/store_access/transcript.rs +++ b/crates/tracedecay-sessions/src/runtime/store_access/transcript.rs @@ -595,27 +595,6 @@ impl SessionStoreAccess<'_, D> { .map_err(|error| TranscriptPersistenceError::storage("commit transcript batch", error)) } - /// Atomically upserts several transcript sessions (and their messages), - /// writing only the searchable `session_messages` projection, never - /// `lcm_raw_messages`, and then advances one shared parse cursor. - #[hotpath::skip] - pub async fn upsert_transcript_projection_batches( - &self, - batches: &[TranscriptBatch], - parse_offset_path: &str, - parse_offset: ParseOffset, - ) -> Result<(), String> { - self.upsert_transcript_batches_inner( - batches, - parse_offset_path, - parse_offset, - TranscriptWritePolicy::ProjectionOnly, - None, - ) - .await - .map_err(|error| error.to_string()) - } - #[hotpath::measure(label = "sessions.store.transcript.write_batches", future = true)] async fn upsert_transcript_batches_inner( &self, From 30c81d49c25faa62ba388ae8a2021ad97c2f35f6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:51:20 +0000 Subject: [PATCH 162/182] test(fixtures): share absolute fixture roots Windows and Unix suites each spelled the drive-prefixed fixture root. One helper now decides the host spelling. Co-authored-by: Zack Jackson --- .../tests/contracts_suite/common/mod.rs | 10 +--------- crates/tracedecay-domain/src/test_fixtures.rs | 16 ++++++++++++++++ crates/tracedecay-domain/src/work_placement.rs | 12 +----------- .../tests/domain_suite/work_runtime_contract.rs | 11 +---------- .../src/analyzer/host_ownership.rs | 6 +----- .../src/shard_runtime/registry/close.rs | 1 - .../shard_runtime/registry/retirement/tests.rs | 1 - .../src/shard_runtime/registry/tests/support.rs | 10 ++-------- .../src/shard_runtime/shard.rs | 1 - .../src/shard_runtime/telemetry.rs | 2 -- .../tests/rusqlite_suite/common/mod.rs | 10 +--------- 11 files changed, 23 insertions(+), 57 deletions(-) diff --git a/crates/tracedecay-contracts/tests/contracts_suite/common/mod.rs b/crates/tracedecay-contracts/tests/contracts_suite/common/mod.rs index 1f70b5b5a9..af2fa312d3 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/common/mod.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/common/mod.rs @@ -62,15 +62,7 @@ pub fn digest(value: &str) -> ManifestDigest { ManifestDigest::new(value).expect("fixture digest is canonical") } -/// Platform-absolute fixture root: the work and registered-root contracts -/// require `Path::is_absolute`, which a bare `/...` literal fails on Windows. -pub fn fixture_abs_root(posix: &str) -> String { - if cfg!(windows) { - format!("C:{}", posix.replace('/', "\\")) - } else { - posix.to_owned() - } -} +pub use tracedecay_domain::test_fixtures::fixture_abs_root; /// Canonical digest fixture for the Work-attempt product journey. pub fn work_digest(value: char) -> ManifestDigest { diff --git a/crates/tracedecay-domain/src/test_fixtures.rs b/crates/tracedecay-domain/src/test_fixtures.rs index 7b81d2e2df..eb5eb15cc9 100644 --- a/crates/tracedecay-domain/src/test_fixtures.rs +++ b/crates/tracedecay-domain/src/test_fixtures.rs @@ -24,6 +24,16 @@ pub fn digest(digit: char) -> ManifestDigest { ManifestDigest::new(repeated_sha256_text(digit)).expect("fixture digest is canonical") } +/// Absolute fixture root. Work and registry contracts require +/// `Path::is_absolute`, and a bare `/...` literal is not absolute on Windows. +pub fn fixture_abs_root(posix: &str) -> String { + if cfg!(windows) { + format!("C:{}", posix.replace('/', "\\")) + } else { + posix.to_owned() + } +} + /// Parses a fixture identity. The value must already be canonical for `T`. pub fn id(value: &str) -> T where @@ -53,4 +63,10 @@ mod tests { let project: crate::ProjectId = id("project.fixture"); assert_eq!(project.as_str(), "project.fixture"); } + + #[test] + fn fixture_abs_root_is_absolute_on_this_host() { + let root = super::fixture_abs_root("/workspace/fixture"); + assert!(std::path::Path::new(&root).is_absolute(), "{root}"); + } } diff --git a/crates/tracedecay-domain/src/work_placement.rs b/crates/tracedecay-domain/src/work_placement.rs index 71cc566d64..f6af579cc7 100644 --- a/crates/tracedecay-domain/src/work_placement.rs +++ b/crates/tracedecay-domain/src/work_placement.rs @@ -564,17 +564,7 @@ mod tests { } } - /// `Path::is_absolute` is host-specific: a bare `/workspace/...` literal - /// is not absolute on Windows, where an absolute path needs a drive or a - /// UNC prefix. The fixture must name a root the running host agrees is - /// absolute, or the contract rejects it as `InvalidTargetRoot`. - fn absolute_root(posix: &str) -> String { - if cfg!(windows) { - format!("C:{}", posix.replace('/', "\\")) - } else { - posix.to_owned() - } - } + use crate::test_fixtures::fixture_abs_root as absolute_root; fn linked() -> WorkPlacementTargetV1 { WorkPlacementTargetV1::new( diff --git a/crates/tracedecay-domain/tests/domain_suite/work_runtime_contract.rs b/crates/tracedecay-domain/tests/domain_suite/work_runtime_contract.rs index 4690558d9a..a93980f14e 100644 --- a/crates/tracedecay-domain/tests/domain_suite/work_runtime_contract.rs +++ b/crates/tracedecay-domain/tests/domain_suite/work_runtime_contract.rs @@ -99,16 +99,7 @@ fn execution_snapshot() -> WorkExecutionSnapshot { .unwrap() } -/// `WorkExecutionEnvelopeV1` requires `Path::is_absolute`, which is -/// host-specific: a bare `/tmp/...` literal is not absolute on Windows, where -/// an absolute path needs a drive or a UNC prefix. -fn absolute_root(posix: &str) -> String { - if cfg!(windows) { - format!("C:{}", posix.replace('/', "\\")) - } else { - posix.to_owned() - } -} +use tracedecay_domain::test_fixtures::fixture_abs_root as absolute_root; fn execution( attempt_identity: WorkAttemptIdentityV1, diff --git a/crates/tracedecay-lsp/src/analyzer/host_ownership.rs b/crates/tracedecay-lsp/src/analyzer/host_ownership.rs index 54dcb9b100..19f5f13f18 100644 --- a/crates/tracedecay-lsp/src/analyzer/host_ownership.rs +++ b/crates/tracedecay-lsp/src/analyzer/host_ownership.rs @@ -303,11 +303,7 @@ mod tests { /// Host-absolute fixture path: `$XDG_CONFIG_HOME` only wins when it is /// absolute, and a bare `/xdg/...` literal is not absolute on Windows. fn absolute_fixture_path(posix: &str) -> PathBuf { - if cfg!(windows) { - PathBuf::from(format!("C:{}", posix.replace('/', "\\"))) - } else { - PathBuf::from(posix) - } + PathBuf::from(tracedecay_domain::test_fixtures::fixture_abs_root(posix)) } #[test] diff --git a/crates/tracedecay-runtime-core/src/shard_runtime/registry/close.rs b/crates/tracedecay-runtime-core/src/shard_runtime/registry/close.rs index b2be54ec03..952051b72b 100644 --- a/crates/tracedecay-runtime-core/src/shard_runtime/registry/close.rs +++ b/crates/tracedecay-runtime-core/src/shard_runtime/registry/close.rs @@ -414,7 +414,6 @@ impl StoreRuntimeRegistry { #[cfg(test)] mod tests { - use std::fmt::Debug; use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; diff --git a/crates/tracedecay-runtime-core/src/shard_runtime/registry/retirement/tests.rs b/crates/tracedecay-runtime-core/src/shard_runtime/registry/retirement/tests.rs index 9878fba087..41dc98fbeb 100644 --- a/crates/tracedecay-runtime-core/src/shard_runtime/registry/retirement/tests.rs +++ b/crates/tracedecay-runtime-core/src/shard_runtime/registry/retirement/tests.rs @@ -1,4 +1,3 @@ -use std::fmt::Debug; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; diff --git a/crates/tracedecay-runtime-core/src/shard_runtime/registry/tests/support.rs b/crates/tracedecay-runtime-core/src/shard_runtime/registry/tests/support.rs index f14efd5562..b199433e19 100644 --- a/crates/tracedecay-runtime-core/src/shard_runtime/registry/tests/support.rs +++ b/crates/tracedecay-runtime-core/src/shard_runtime/registry/tests/support.rs @@ -1,4 +1,3 @@ -use std::fmt::Debug; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; @@ -18,14 +17,9 @@ use super::super::*; pub(super) use tracedecay_domain::test_fixtures::id; /// Host-absolute fixture path: store locators require `Path::is_absolute`, -/// which a bare `/...` literal fails on Windows, where the same fixture is -/// spelled `C:\...`. +/// which a bare `/...` literal fails on Windows. pub(super) fn absolute_fixture_path(posix: &str) -> PathBuf { - if cfg!(windows) { - PathBuf::from(format!("C:{}", posix.replace('/', "\\"))) - } else { - PathBuf::from(posix) - } + PathBuf::from(tracedecay_domain::test_fixtures::fixture_abs_root(posix)) } pub(super) fn incarnation() -> StoreIncarnationV1 { diff --git a/crates/tracedecay-runtime-core/src/shard_runtime/shard.rs b/crates/tracedecay-runtime-core/src/shard_runtime/shard.rs index bd23260bba..b4d418f1f1 100644 --- a/crates/tracedecay-runtime-core/src/shard_runtime/shard.rs +++ b/crates/tracedecay-runtime-core/src/shard_runtime/shard.rs @@ -1043,7 +1043,6 @@ impl Drop for ShardRuntimeQueuedWork<'_> { #[cfg(test)] mod tests { - use std::fmt::Debug; use std::sync::Barrier; use tracedecay_domain::{BrainId, ProjectId, UserProfileId}; diff --git a/crates/tracedecay-runtime-core/src/shard_runtime/telemetry.rs b/crates/tracedecay-runtime-core/src/shard_runtime/telemetry.rs index a1cb2b2f69..9422748826 100644 --- a/crates/tracedecay-runtime-core/src/shard_runtime/telemetry.rs +++ b/crates/tracedecay-runtime-core/src/shard_runtime/telemetry.rs @@ -428,8 +428,6 @@ const fn count_if(value: bool) -> u32 { #[cfg(test)] mod tests { - use std::fmt::Debug; - use tracedecay_domain::{BrainId, ProjectId, UserProfileId}; use tracedecay_store::{StoreAuthorityEpochV1, StoreIncarnationV1, StoreShardIdV1}; diff --git a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/common/mod.rs b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/common/mod.rs index 8b1ac310bc..ba4e206faf 100644 --- a/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/common/mod.rs +++ b/crates/tracedecay-rusqlite-runtime/tests/rusqlite_suite/common/mod.rs @@ -1,9 +1 @@ -/// Platform-absolute fixture root: the work and registered-root contracts -/// require `Path::is_absolute`, which a bare `/...` literal fails on Windows. -pub fn fixture_abs_root(posix: &str) -> String { - if cfg!(windows) { - format!("C:{}", posix.replace('/', "\\")) - } else { - posix.to_owned() - } -} +pub use tracedecay_domain::test_fixtures::fixture_abs_root; From 3ea8b2e3dc89c8d1860ec1f877ad3912f2161dd5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:53:59 +0000 Subject: [PATCH 163/182] simplify(pass-1/5): share correctable invalid-request constructor Co-authored-by: Zack Jackson --- .../src/operation_stream.rs | 10 ++- .../src/primitives/runtime.rs | 8 +-- crates/tracedecay-cli/src/application_cli.rs | 18 ++---- .../execution_topology_metrics/rollup_read.rs | 38 +++++------ .../src/execution_topology_metrics/support.rs | 21 ++---- .../src/result/problem.rs | 9 +++ .../src/result/problem/tests.rs | 20 ++++++ .../src/retained_surfaces/service.rs | 16 ++--- crates/tracedecay-contracts/src/work.rs | 10 ++- .../src/work_artifact_hydration.rs | 22 ++----- .../tracedecay-contracts/src/work_attempt.rs | 23 +++---- .../src/work_attempt/capacity.rs | 12 ++-- .../src/work_attempt/problem.rs | 19 ++---- .../src/work_attempt/product_admission.rs | 40 ++++-------- .../src/work_attempt_effect.rs | 12 ++-- .../src/work_duplicate_adjudication.rs | 42 +++++++----- .../src/work_leak_adjudication.rs | 30 +++++---- .../src/work_placement.rs | 12 ++-- crates/tracedecay-contracts/src/work_retry.rs | 64 +++++++++++++------ .../src/work_run_control.rs | 39 ++++------- .../src/work_synthesis.rs | 41 +++++------- .../application_surface/multi_root_http.rs | 12 ++-- .../application_surface/operation_events.rs | 10 ++- .../application_surface/registered_http.rs | 12 ++-- .../src/invocation/github_stack_signal.rs | 13 ++-- .../src/invocation/native_integration.rs | 13 ++-- .../src/invocation/source_edit.rs | 50 +++++---------- .../src/invocation/work/attempt_operations.rs | 14 ++-- .../src/invocation/work/outcome.rs | 24 +++---- .../src/invocation/work/workflow_dispatch.rs | 6 +- .../work/workflow_effect_journal.rs | 6 +- .../invocation/work/workflow_run_control.rs | 10 +-- .../tracedecay-mcp/src/handlers/multi_root.rs | 26 +++----- .../src/retained/session.rs | 8 +-- .../src/daemon/invocation_tests/work_tests.rs | 10 ++- 35 files changed, 308 insertions(+), 412 deletions(-) diff --git a/crates/tracedecay-application/src/operation_stream.rs b/crates/tracedecay-application/src/operation_stream.rs index b6df8a172f..9567f89773 100644 --- a/crates/tracedecay-application/src/operation_stream.rs +++ b/crates/tracedecay-application/src/operation_stream.rs @@ -295,14 +295,12 @@ impl OperationEventError { Self::InvalidContext(_) | Self::InvalidProgress | Self::InvalidTerminal(_) - | Self::InvalidTestRunEvent => ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic::new( + | Self::InvalidTestRunEvent => { + ApplicationProblem::invalid_request(SafeDiagnostic::new( "operation_event.invalid_request", "The operation-event request is invalid", - )?, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::CorrectRequest], - }, + )?) + } // Idempotency facts: the identity or terminal receipt is already // published, so the client re-reads current state instead of // retrying the same publish. diff --git a/crates/tracedecay-application/src/primitives/runtime.rs b/crates/tracedecay-application/src/primitives/runtime.rs index 55ed5f7351..980e82fe1e 100644 --- a/crates/tracedecay-application/src/primitives/runtime.rs +++ b/crates/tracedecay-application/src/primitives/runtime.rs @@ -989,11 +989,9 @@ fn session_structural_refusal_problem( "The request exceeds its admitted session retrieval budget.", ), }; - Ok(ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic::new(code, message)?, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::CorrectRequest], - }) + Ok(ApplicationProblem::invalid_request(SafeDiagnostic::new( + code, message, + )?)) } const fn session_budget_diagnostic_code(stage: SessionRetrievalBudgetStageV1) -> &'static str { diff --git a/crates/tracedecay-cli/src/application_cli.rs b/crates/tracedecay-cli/src/application_cli.rs index b37a555fa8..224a95a5de 100644 --- a/crates/tracedecay-cli/src/application_cli.rs +++ b/crates/tracedecay-cli/src/application_cli.rs @@ -29,17 +29,13 @@ impl ApplicationKind { } pub(crate) fn invalid_request(self) -> ApplicationProblem { - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { - code: format!("invalid_{}_request", self.1), - message: format!( - "The {} request does not match its operation contract", - self.0 - ), - }, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::CorrectRequest], - } + ApplicationProblem::invalid_request(SafeDiagnostic { + code: format!("invalid_{}_request", self.1), + message: format!( + "The {} request does not match its operation contract", + self.0 + ), + }) } pub(crate) fn daemon_problem(self, problem: DaemonInvocationProblem) -> ApplicationProblem { diff --git a/crates/tracedecay-contracts/src/execution_topology_metrics/rollup_read.rs b/crates/tracedecay-contracts/src/execution_topology_metrics/rollup_read.rs index 84c1792c90..d42c88db41 100644 --- a/crates/tracedecay-contracts/src/execution_topology_metrics/rollup_read.rs +++ b/crates/tracedecay-contracts/src/execution_topology_metrics/rollup_read.rs @@ -10,7 +10,7 @@ use crate::observability::{ ObservabilityFuture, ObservabilityHorizonV1, ObservabilityQueryPort, ObservabilityQueryV1, }; use crate::work::work_authority; -use crate::{ApplicationProblem, RequestAdmission, RequestContext, RetryDirective}; +use crate::{ApplicationProblem, RequestAdmission, RequestContext, RetryDirective, SafeDiagnostic}; use super::projection::TELEMETRY_DROP_EVENT_KIND_V1; use super::rollup::{ @@ -20,7 +20,7 @@ use super::rollup::{ canonical_execution_topology_rollup_fragment_bytes, project_execution_topology_fragments_with_boundaries, }; -use super::support::{invalid_problem, unavailable_model, unavailable_model_with_state_at}; +use super::support::{unavailable_model, unavailable_model_with_state_at}; use super::{ EXECUTION_TOPOLOGY_CAPABILITY_ID_V1, EXECUTION_TOPOLOGY_EVENT_KINDS_V1, EXECUTION_TOPOLOGY_USE_CASE_ID_V1, ExecutionMetricUnavailableV1, @@ -227,16 +227,18 @@ where fn validate_request(request: &ExecutionTopologyMetricsRequestV1) -> Result<(), ApplicationProblem> { if request.horizon.until_micros <= request.horizon.since_micros { - return Err(invalid_problem( - "application.execution-topology-rollup.invalid-horizon", - "The execution topology metrics horizon must end after it starts.", - )); + return Err(ApplicationProblem::invalid_request(SafeDiagnostic { + code: ("application.execution-topology-rollup.invalid-horizon").to_owned(), + message: ("The execution topology metrics horizon must end after it starts.") + .to_owned(), + })); } if request.max_events == 0 || request.max_events > MAX_EXECUTION_TOPOLOGY_EVENTS_V1 { - return Err(invalid_problem( - "application.execution-topology-rollup.invalid-event-budget", - "The execution topology metrics event budget must be between 1 and 10000.", - )); + return Err(ApplicationProblem::invalid_request(SafeDiagnostic { + code: ("application.execution-topology-rollup.invalid-event-budget").to_owned(), + message: ("The execution topology metrics event budget must be between 1 and 10000.") + .to_owned(), + })); } Ok(()) } @@ -251,16 +253,16 @@ fn admit(context: &RequestContext, observed_at: UtcMicros) -> Result<(), Applica fn authorize(context: &RequestContext) -> Result<(), ApplicationProblem> { let capability = CapabilityId::new(EXECUTION_TOPOLOGY_CAPABILITY_ID_V1).map_err(|_| { - invalid_problem( - "application.execution-topology-rollup.invalid-authority", - "The execution topology metrics authority is unavailable.", - ) + ApplicationProblem::invalid_request(SafeDiagnostic { + code: ("application.execution-topology-rollup.invalid-authority").to_owned(), + message: ("The execution topology metrics authority is unavailable.").to_owned(), + }) })?; let use_case = UseCaseId::new(EXECUTION_TOPOLOGY_USE_CASE_ID_V1).map_err(|_| { - invalid_problem( - "application.execution-topology-rollup.invalid-authority", - "The execution topology metrics authority is unavailable.", - ) + ApplicationProblem::invalid_request(SafeDiagnostic { + code: ("application.execution-topology-rollup.invalid-authority").to_owned(), + message: ("The execution topology metrics authority is unavailable.").to_owned(), + }) })?; if context.allows(&capability, &use_case) { Ok(()) diff --git a/crates/tracedecay-contracts/src/execution_topology_metrics/support.rs b/crates/tracedecay-contracts/src/execution_topology_metrics/support.rs index c2c04c3f90..e627073d12 100644 --- a/crates/tracedecay-contracts/src/execution_topology_metrics/support.rs +++ b/crates/tracedecay-contracts/src/execution_topology_metrics/support.rs @@ -1,11 +1,5 @@ use tracedecay_domain::CoverageStateV1; -use crate::observability::{ - MetricCohortV1, MetricCoverageV1, MetricEvidenceClassV1, MetricProvenanceV1, MetricSourceV1, - MetricTemporalV1, MetricUncertaintyV1, MetricValueV1, ObservabilityHorizonV1, -}; -use crate::{ApplicationProblem, LegalAction, RetryDirective, SafeDiagnostic}; - use super::projection::ProjectionContext; use super::{ CONFLICT_MIN_ADJUDICATED_CASES_V1, EXECUTION_TOPOLOGY_DESCRIPTOR_REVISION_V1, @@ -14,6 +8,10 @@ use super::{ MAX_CENSORING_RATIO_V1, MAX_METRIC_DIMENSIONS_V1, MIN_COVERAGE_RATIO_V1, RATE_MIN_ELIGIBLE_CASES_V1, }; +use crate::observability::{ + MetricCohortV1, MetricCoverageV1, MetricEvidenceClassV1, MetricProvenanceV1, MetricSourceV1, + MetricTemporalV1, MetricUncertaintyV1, MetricValueV1, ObservabilityHorizonV1, +}; const SOURCE_REVISION_V1: &str = "observability-envelope.v1"; @@ -493,17 +491,6 @@ fn span(start: i64, end: i64) -> u64 { end.abs_diff(start) } -pub(super) fn invalid_problem(code: &str, message: &str) -> ApplicationProblem { - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { - code: code.to_owned(), - message: message.to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::CorrectRequest], - } -} - #[cfg(test)] #[path = "support_descriptor_tests.rs"] mod descriptor_tests; diff --git a/crates/tracedecay-contracts/src/result/problem.rs b/crates/tracedecay-contracts/src/result/problem.rs index a44e21648e..5b28813c61 100644 --- a/crates/tracedecay-contracts/src/result/problem.rs +++ b/crates/tracedecay-contracts/src/result/problem.rs @@ -688,6 +688,15 @@ impl ApplicationProblem { } } + /// Invalid request the caller can correct. Never retries. + pub fn invalid_request(diagnostic: SafeDiagnostic) -> Self { + Self::InvalidRequest { + diagnostic, + retry: RetryDirective::Never, + legal_actions: vec![LegalAction::CorrectRequest], + } + } + pub fn cancelled_before_admission() -> Self { Self::Cancelled { stage: CancellationStage::BeforeAdmission, diff --git a/crates/tracedecay-contracts/src/result/problem/tests.rs b/crates/tracedecay-contracts/src/result/problem/tests.rs index a4cfcb21b8..3a75c79261 100644 --- a/crates/tracedecay-contracts/src/result/problem/tests.rs +++ b/crates/tracedecay-contracts/src/result/problem/tests.rs @@ -6,6 +6,26 @@ use super::{ RetryDirective, SafeDiagnostic, }; +#[test] +fn invalid_request_offers_correction_and_never_retries() { + let diagnostic = SafeDiagnostic { + code: "application.invalid-request".to_owned(), + message: "The request is invalid.".to_owned(), + }; + let problem = ApplicationProblem::invalid_request(diagnostic.clone()); + + assert_eq!( + problem, + ApplicationProblem::InvalidRequest { + diagnostic, + retry: RetryDirective::Never, + legal_actions: vec![LegalAction::CorrectRequest], + } + ); + assert_eq!(problem.safe_message(), "The request is invalid."); + assert_eq!(problem.reason_code(), "application.invalid-request"); +} + #[test] fn reset_required_is_a_distinct_non_retryable_terminal() { let problem = ApplicationProblem::reset_required( diff --git a/crates/tracedecay-contracts/src/retained_surfaces/service.rs b/crates/tracedecay-contracts/src/retained_surfaces/service.rs index f59b51f139..4e5f540ec8 100644 --- a/crates/tracedecay-contracts/src/retained_surfaces/service.rs +++ b/crates/tracedecay-contracts/src/retained_surfaces/service.rs @@ -532,14 +532,12 @@ pub fn retained_surface_execution_problem( RetainedSurfaceExecutionErrorV1::StructuralRefusal(refusal) => { structural_refusal_problem(refusal) } - RetainedSurfaceExecutionErrorV1::InvalidRequest => ApplicationProblem::InvalidRequest { - diagnostic: diagnostic( + RetainedSurfaceExecutionErrorV1::InvalidRequest => { + ApplicationProblem::invalid_request(diagnostic( "application.retained.invalid-request", "The retained operation request is invalid.", - ), - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::CorrectRequest], - }, + )) + } RetainedSurfaceExecutionErrorV1::NotFoundOrNotAuthorized => { ApplicationProblem::not_found_or_not_authorized(RetryDirective::Never) } @@ -753,11 +751,7 @@ fn structural_refusal_problem(refusal: RetainedStructuralRefusalV1) -> Applicati "The authorized session scope exceeds the cursor manifest byte limit. Narrow the session scope.", ), }; - ApplicationProblem::InvalidRequest { - diagnostic, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::CorrectRequest], - } + ApplicationProblem::invalid_request(diagnostic) } fn diagnostic(code: &'static str, message: &'static str) -> SafeDiagnostic { diff --git a/crates/tracedecay-contracts/src/work.rs b/crates/tracedecay-contracts/src/work.rs index 7831c08db9..647583d6e8 100644 --- a/crates/tracedecay-contracts/src/work.rs +++ b/crates/tracedecay-contracts/src/work.rs @@ -9,7 +9,7 @@ use tracedecay_policy::work_loop::{ WorkRouteOverrideV1, }; -use crate::{ApplicationProblem, LegalAction, RequestContext, RetryDirective, SafeDiagnostic}; +use crate::{ApplicationProblem, RequestContext, SafeDiagnostic}; #[derive(Clone, Debug, Error, PartialEq, Eq)] pub enum WorkRoutingSnapshotErrorV1 { @@ -75,12 +75,10 @@ pub(crate) fn work_authority( context.actor().clone(), context.grant().digest.clone(), ) - .map_err(|_| ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { + .map_err(|_| { + ApplicationProblem::invalid_request(SafeDiagnostic { code: "application.work.invalid-history".to_owned(), message: "The Work command or stored history is invalid.".to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::CorrectRequest], + }) }) } diff --git a/crates/tracedecay-contracts/src/work_artifact_hydration.rs b/crates/tracedecay-contracts/src/work_artifact_hydration.rs index 352345c6f0..b52f5a5390 100644 --- a/crates/tracedecay-contracts/src/work_artifact_hydration.rs +++ b/crates/tracedecay-contracts/src/work_artifact_hydration.rs @@ -22,7 +22,7 @@ use crate::work_attempt::{ WorkAttemptListCursorV1, WorkAttemptStorageError, WorkAttemptTopologyBindingV1, WorkAttemptTopologyStateV1, }; -use crate::{ApplicationProblem, LegalAction, RequestContext, RetryDirective, SafeDiagnostic}; +use crate::{ApplicationProblem, RequestContext, RetryDirective, SafeDiagnostic}; /// One page of attempt rows joined with their sealed evidence records, in the /// same stable task/run/attempt identity order as the attempt list, read @@ -149,10 +149,11 @@ where topology: impl FnOnce(&WorkAuthority) -> Result, ) -> Result { if request.page_size == 0 || request.page_size > MAX_WORK_ATTEMPT_LIST_PAGE_SIZE { - return Err(invalid_problem( - "application.work-artifact-hydration.invalid-page-size", - "The Work artifact hydration page size must be between 1 and 1000.", - )); + return Err(ApplicationProblem::invalid_request(SafeDiagnostic { + code: ("application.work-artifact-hydration.invalid-page-size").to_owned(), + message: ("The Work artifact hydration page size must be between 1 and 1000.") + .to_owned(), + })); } let authority = work_authority(context)?; // Two distinct resources hide inside one hydration: the topology @@ -279,14 +280,3 @@ fn page_contract_problem() -> ApplicationProblem { message: "The Work attempt storage returned an inconsistent hydration page.".to_owned(), }) } - -fn invalid_problem(code: &str, message: &str) -> ApplicationProblem { - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { - code: code.to_owned(), - message: message.to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::CorrectRequest], - } -} diff --git a/crates/tracedecay-contracts/src/work_attempt.rs b/crates/tracedecay-contracts/src/work_attempt.rs index a86a388b73..1535ac0a47 100644 --- a/crates/tracedecay-contracts/src/work_attempt.rs +++ b/crates/tracedecay-contracts/src/work_attempt.rs @@ -22,7 +22,7 @@ use tracedecay_domain::{ }; use crate::work::work_authority; -use crate::{ApplicationProblem, RequestAdmission, RequestContext}; +use crate::{ApplicationProblem, RequestAdmission, RequestContext, SafeDiagnostic}; mod capacity; mod problem; @@ -34,8 +34,8 @@ pub use capacity::{ WorkAttemptCapacityVerdictV1, }; use problem::{ - conflict_problem, contract_problem, denied_problem, invalid_problem, - list_page_contract_problem, not_found_problem, stale_cursor_problem, storage_problem, + conflict_problem, contract_problem, denied_problem, list_page_contract_problem, + not_found_problem, stale_cursor_problem, storage_problem, }; pub use product_admission::WorkProductAttemptServiceV1; pub(crate) use product_admission::{ @@ -299,10 +299,11 @@ pub struct WorkAttemptEvidenceRecordV1 { impl WorkAttemptEvidenceRecordV1 { pub fn digest(&self) -> Result { canonical_sha256(&(WORK_ATTEMPT_EVIDENCE_DOMAIN, self)).map_err(|_| { - invalid_problem( - "application.work-attempt.invalid-evidence", - "The Work attempt evidence record could not be canonicalized.", - ) + ApplicationProblem::invalid_request(SafeDiagnostic { + code: ("application.work-attempt.invalid-evidence").to_owned(), + message: ("The Work attempt evidence record could not be canonicalized.") + .to_owned(), + }) }) } } @@ -491,10 +492,10 @@ where topology: impl FnOnce() -> Result, ) -> Result { if request.page_size == 0 || request.page_size > MAX_WORK_ATTEMPT_LIST_PAGE_SIZE { - return Err(invalid_problem( - "application.work-attempt.invalid-page-size", - "The Work attempt list page size must be between 1 and 1000.", - )); + return Err(ApplicationProblem::invalid_request(SafeDiagnostic { + code: ("application.work-attempt.invalid-page-size").to_owned(), + message: ("The Work attempt list page size must be between 1 and 1000.").to_owned(), + })); } let authority = work_authority(context)?; let binding = match topology()? { diff --git a/crates/tracedecay-contracts/src/work_attempt/capacity.rs b/crates/tracedecay-contracts/src/work_attempt/capacity.rs index 7024b32f93..a67c27a9cd 100644 --- a/crates/tracedecay-contracts/src/work_attempt/capacity.rs +++ b/crates/tracedecay-contracts/src/work_attempt/capacity.rs @@ -5,9 +5,9 @@ use std::collections::BTreeSet; use tracedecay_domain::{TaskId, WorkTopologyPolicyV1, configuration::TopologyConcurrencyPolicyV1}; use crate::work::work_authority; -use crate::{ApplicationProblem, RequestContext}; +use crate::{ApplicationProblem, RequestContext, SafeDiagnostic}; -use super::{WorkAttemptService, WorkAttemptStoragePort, invalid_problem, storage_problem}; +use super::{WorkAttemptService, WorkAttemptStoragePort, storage_problem}; /// Maximum prospective task identities in one exact capacity census. pub const MAX_WORK_ATTEMPT_CAPACITY_TASKS: usize = u16::MAX as usize; @@ -151,8 +151,8 @@ where } fn capacity_query_problem() -> ApplicationProblem { - invalid_problem( - "application.work-attempt.invalid-capacity-query", - "Capacity task identities must be strictly sorted, unique, and within the batch bound.", - ) + ApplicationProblem::invalid_request(SafeDiagnostic { + code: ("application.work-attempt.invalid-capacity-query").to_owned(), + message: ("Capacity task identities must be strictly sorted, unique, and within the batch bound.").to_owned(), + }) } diff --git a/crates/tracedecay-contracts/src/work_attempt/problem.rs b/crates/tracedecay-contracts/src/work_attempt/problem.rs index 82cedd1f05..6cdc2ecf49 100644 --- a/crates/tracedecay-contracts/src/work_attempt/problem.rs +++ b/crates/tracedecay-contracts/src/work_attempt/problem.rs @@ -40,10 +40,10 @@ pub(super) fn storage_problem(error: WorkAttemptStorageError) -> ApplicationProb } pub(super) fn contract_problem(_error: WorkRuntimeContractError) -> ApplicationProblem { - invalid_problem( - "application.work-attempt.invalid-transition", - "The Work attempt command or stored state is invalid.", - ) + ApplicationProblem::invalid_request(SafeDiagnostic { + code: ("application.work-attempt.invalid-transition").to_owned(), + message: ("The Work attempt command or stored state is invalid.").to_owned(), + }) } pub(super) fn not_found_problem() -> ApplicationProblem { @@ -76,17 +76,6 @@ pub(super) fn denied_problem(code: &str, message: &str) -> ApplicationProblem { } } -pub(super) fn invalid_problem(code: &str, message: &str) -> ApplicationProblem { - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { - code: code.to_owned(), - message: message.to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::CorrectRequest], - } -} - pub(super) fn conflict_problem(code: &str, message: &str) -> ApplicationProblem { ApplicationProblem::Conflict { diagnostic: SafeDiagnostic { diff --git a/crates/tracedecay-contracts/src/work_attempt/product_admission.rs b/crates/tracedecay-contracts/src/work_attempt/product_admission.rs index 0608704b46..a06b59e175 100644 --- a/crates/tracedecay-contracts/src/work_attempt/product_admission.rs +++ b/crates/tracedecay-contracts/src/work_attempt/product_admission.rs @@ -208,15 +208,10 @@ pub(crate) fn product_admission_problem( ) -> ApplicationProblem { match error { WorkProductAttemptAdmissionErrorV1::InvalidAdmission => { - ApplicationProblem::InvalidRequest { - diagnostic: crate::SafeDiagnostic { - code: "application.work-attempt.invalid-product-admission".to_owned(), - message: "The Work attempt does not match the canonical product graph." - .to_owned(), - }, - retry: crate::RetryDirective::Never, - legal_actions: vec![crate::LegalAction::CorrectRequest], - } + ApplicationProblem::invalid_request(crate::SafeDiagnostic { + code: "application.work-attempt.invalid-product-admission".to_owned(), + message: "The Work attempt does not match the canonical product graph.".to_owned(), + }) } WorkProductAttemptAdmissionErrorV1::NotFoundOrNotAuthorized => not_found_problem(), WorkProductAttemptAdmissionErrorV1::VersionConflict => conflict_problem( @@ -491,18 +486,13 @@ fn product_problem(error: WorkProductApplicationErrorV1) -> ApplicationProblem { // and the remedy are both specific: the selection covers a slice of // the journal, and widening it is what makes admission possible. WorkProductApplicationErrorV1::SelectionCoverageIncomplete => { - ApplicationProblem::InvalidRequest { - diagnostic: crate::SafeDiagnostic { - code: "application.work-attempt.product-selection-coverage-incomplete" - .to_owned(), - message: "The Work selection covers only part of the owner's journal, so \ + ApplicationProblem::invalid_request(crate::SafeDiagnostic { + code: "application.work-attempt.product-selection-coverage-incomplete".to_owned(), + message: "The Work selection covers only part of the owner's journal, so \ no attempt can be admitted against it; widen the selection to \ the relation scopes the excluded events were admitted under." - .to_owned(), - }, - retry: crate::RetryDirective::Never, - legal_actions: vec![crate::LegalAction::CorrectRequest], - } + .to_owned(), + }) } WorkProductApplicationErrorV1::EventAuthorityUnavailable | WorkProductApplicationErrorV1::GraphAuthorityUnavailable @@ -517,14 +507,10 @@ fn product_problem(error: WorkProductApplicationErrorV1) -> ApplicationProblem { } fn invalid_start_problem() -> ApplicationProblem { - ApplicationProblem::InvalidRequest { - diagnostic: crate::SafeDiagnostic { - code: "application.work-attempt.invalid-product-admission".to_owned(), - message: "The Work attempt command is invalid.".to_owned(), - }, - retry: crate::RetryDirective::Never, - legal_actions: vec![crate::LegalAction::CorrectRequest], - } + ApplicationProblem::invalid_request(crate::SafeDiagnostic { + code: "application.work-attempt.invalid-product-admission".to_owned(), + message: "The Work attempt command is invalid.".to_owned(), + }) } #[cfg(test)] diff --git a/crates/tracedecay-contracts/src/work_attempt_effect.rs b/crates/tracedecay-contracts/src/work_attempt_effect.rs index 4fc3a6b27e..91d3f87b2a 100644 --- a/crates/tracedecay-contracts/src/work_attempt_effect.rs +++ b/crates/tracedecay-contracts/src/work_attempt_effect.rs @@ -276,14 +276,10 @@ fn effect_problem(error: WorkAttemptEffectStorageErrorV1) -> ApplicationProblem } fn invalid_holder_problem() -> ApplicationProblem { - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { - code: "application.work-attempt-effect.invalid-holder".to_owned(), - message: "The Work attempt effect lifecycle time is invalid.".to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::CorrectRequest], - } + ApplicationProblem::invalid_request(SafeDiagnostic { + code: "application.work-attempt-effect.invalid-holder".to_owned(), + message: "The Work attempt effect lifecycle time is invalid.".to_owned(), + }) } #[cfg(test)] diff --git a/crates/tracedecay-contracts/src/work_duplicate_adjudication.rs b/crates/tracedecay-contracts/src/work_duplicate_adjudication.rs index d96b988eb4..dc4feb7477 100644 --- a/crates/tracedecay-contracts/src/work_duplicate_adjudication.rs +++ b/crates/tracedecay-contracts/src/work_duplicate_adjudication.rs @@ -203,9 +203,19 @@ where admit(context, command.occurred_at)?; let authority = work_authority(context)?; let command = command.canonicalized(); - command.validate().map_err(|_| invalid_problem())?; + command.validate().map_err(|_| { + ApplicationProblem::invalid_request(SafeDiagnostic { + code: "application.work.duplicate-adjudication.invalid".to_owned(), + message: "The duplicate Work adjudication is invalid.".to_owned(), + }) + })?; let canonical_input_digest = - work_duplicate_adjudication_input_digest(&command).map_err(|_| invalid_problem())?; + work_duplicate_adjudication_input_digest(&command).map_err(|_| { + ApplicationProblem::invalid_request(SafeDiagnostic { + code: "application.work.duplicate-adjudication.invalid".to_owned(), + message: "The duplicate Work adjudication is invalid.".to_owned(), + }) + })?; let outcome = self .storage .compare_and_record_duplicate_adjudication( @@ -243,7 +253,10 @@ where admit(context, occurred_at)?; let authority = work_authority(context)?; if request.first_attempt == request.second_attempt { - return Err(invalid_problem()); + return Err(ApplicationProblem::invalid_request(SafeDiagnostic { + code: "application.work.duplicate-adjudication.invalid".to_owned(), + message: "The duplicate Work adjudication is invalid.".to_owned(), + })); } let (first_attempt, second_attempt) = if request.first_attempt <= request.second_attempt { (&request.first_attempt, &request.second_attempt) @@ -261,7 +274,12 @@ where command_id, occurred_at, ) - .map_err(|_| invalid_problem()) + .map_err(|_| { + ApplicationProblem::invalid_request(SafeDiagnostic { + code: "application.work.duplicate-adjudication.invalid".to_owned(), + message: "The duplicate Work adjudication is invalid.".to_owned(), + }) + }) } /// Classifies useful attempts only with a complete exact pair matrix at @@ -281,7 +299,10 @@ where if attempts.len() > MAX_WORK_DUPLICATE_CLASSIFICATION_ATTEMPTS_V1 || attempts.windows(2).any(|pair| pair[0] == pair[1]) { - return Err(invalid_problem()); + return Err(ApplicationProblem::invalid_request(SafeDiagnostic { + code: "application.work.duplicate-adjudication.invalid".to_owned(), + message: "The duplicate Work adjudication is invalid.".to_owned(), + })); } let receipts = self .storage @@ -410,17 +431,6 @@ fn admit(context: &RequestContext, observed_at: UtcMicros) -> Result<(), Applica } } -fn invalid_problem() -> ApplicationProblem { - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { - code: "application.work.duplicate-adjudication.invalid".to_owned(), - message: "The duplicate Work adjudication is invalid.".to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::CorrectRequest], - } -} - fn storage_problem(error: WorkDuplicateAdjudicationStorageErrorV1) -> ApplicationProblem { match error { WorkDuplicateAdjudicationStorageErrorV1::NotFoundOrNotAuthorized => { diff --git a/crates/tracedecay-contracts/src/work_leak_adjudication.rs b/crates/tracedecay-contracts/src/work_leak_adjudication.rs index 9908312212..d4fc1d643b 100644 --- a/crates/tracedecay-contracts/src/work_leak_adjudication.rs +++ b/crates/tracedecay-contracts/src/work_leak_adjudication.rs @@ -275,7 +275,10 @@ where || u64::try_from(scan_deadline.0.saturating_sub(scan_started_at.0)) .map_or(true, |duration| duration > MAX_WORK_LEAK_SCAN_MICROS_V1) { - return Err(invalid_problem()); + return Err(ApplicationProblem::invalid_request(SafeDiagnostic { + code: "application.work-leak.invalid".to_owned(), + message: "The Work leak adjudication request is invalid.".to_owned(), + })); } let authority = work_authority(context)?; if let Some(receipt) = self @@ -303,12 +306,22 @@ where } let canonical_input_digest = canonical_sha256(&(LEAK_INPUT_DIGEST_DOMAIN, &command, &evidence, scan_deadline)) - .map_err(|_| invalid_problem())?; + .map_err(|_| { + ApplicationProblem::invalid_request(SafeDiagnostic { + code: "application.work-leak.invalid".to_owned(), + message: "The Work leak adjudication request is invalid.".to_owned(), + }) + })?; let revision = command .expected_revision .unwrap_or(0) .checked_add(1) - .ok_or_else(invalid_problem)?; + .ok_or_else(|| { + ApplicationProblem::invalid_request(SafeDiagnostic { + code: "application.work-leak.invalid".to_owned(), + message: "The Work leak adjudication request is invalid.".to_owned(), + }) + })?; self.storage .compare_and_record_leak( &authority, @@ -342,17 +355,6 @@ fn admit(context: &RequestContext, observed_at: UtcMicros) -> Result<(), Applica } } -fn invalid_problem() -> ApplicationProblem { - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { - code: "application.work-leak.invalid".to_owned(), - message: "The Work leak adjudication request is invalid.".to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::CorrectRequest], - } -} - fn conflict_problem(code: &str, message: &str) -> ApplicationProblem { ApplicationProblem::Conflict { diagnostic: SafeDiagnostic { diff --git a/crates/tracedecay-contracts/src/work_placement.rs b/crates/tracedecay-contracts/src/work_placement.rs index c067bfc19c..ff69e0f8f3 100644 --- a/crates/tracedecay-contracts/src/work_placement.rs +++ b/crates/tracedecay-contracts/src/work_placement.rs @@ -366,14 +366,10 @@ fn contract_problem(error: WorkPlacementContractError) -> ApplicationProblem { "application.work-placement.non-monotonic", "The Work placement transition is older than the published state.", ), - _ => ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { - code: "application.work-placement.invalid-placement".to_owned(), - message: "The Work placement command or stored state is invalid.".to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::CorrectRequest], - }, + _ => ApplicationProblem::invalid_request(SafeDiagnostic { + code: "application.work-placement.invalid-placement".to_owned(), + message: "The Work placement command or stored state is invalid.".to_owned(), + }), } } diff --git a/crates/tracedecay-contracts/src/work_retry.rs b/crates/tracedecay-contracts/src/work_retry.rs index f6ddfbf23e..ef0d79bb14 100644 --- a/crates/tracedecay-contracts/src/work_retry.rs +++ b/crates/tracedecay-contracts/src/work_retry.rs @@ -378,13 +378,26 @@ where ) -> Result { admit(context, restarted_at)?; if !command.validate() { - return Err(invalid_problem()); + return Err(ApplicationProblem::invalid_request(SafeDiagnostic { + code: "application.work-retry.invalid".to_owned(), + message: "The Work retry command is invalid.".to_owned(), + })); } let authority = work_authority(context)?; - let input_digest = canonical_sha256(&(RETRY_INPUT_DIGEST_DOMAIN, &command)) - .map_err(|_| invalid_problem())?; + let input_digest = + canonical_sha256(&(RETRY_INPUT_DIGEST_DOMAIN, &command)).map_err(|_| { + ApplicationProblem::invalid_request(SafeDiagnostic { + code: "application.work-retry.invalid".to_owned(), + message: "The Work retry command is invalid.".to_owned(), + }) + })?; let product_digest = canonical_sha256(&(WORK_PRODUCT_RETRY_INPUT_DIGEST_DOMAIN, &command)) - .map_err(|_| invalid_problem())?; + .map_err(|_| { + ApplicationProblem::invalid_request(SafeDiagnostic { + code: "application.work-retry.invalid".to_owned(), + message: "The Work retry command is invalid.".to_owned(), + }) + })?; let product = current_work_product_attempt_graph(&self.storage, context, binding, restarted_at)?; if let Some(replayed) = self @@ -474,7 +487,10 @@ where ) .map_err(retry_receipt_problem)?; if receipt.canonical_input_digest != input_digest { - return Err(invalid_problem()); + return Err(ApplicationProblem::invalid_request(SafeDiagnostic { + code: "application.work-retry.invalid".to_owned(), + message: "The Work retry command is invalid.".to_owned(), + })); } let draft = accepted_attempt_draft( &product, @@ -672,7 +688,12 @@ where .execution() .cancellation_generation() .checked_add(1) - .ok_or_else(invalid_problem)?; + .ok_or_else(|| { + ApplicationProblem::invalid_request(SafeDiagnostic { + code: "application.work-retry.invalid".to_owned(), + message: "The Work retry command is invalid.".to_owned(), + }) + })?; let envelope = WorkExecutionEnvelopeV1::new( identity.clone(), binding.clone(), @@ -692,13 +713,22 @@ where let epoch = storage .next_fence_epoch(authority) .map_err(storage_problem)?; - let lease_digest = - canonical_sha256(&(RETRY_LEASE_DOMAIN, &identity)).map_err(|_| invalid_problem())?; + let lease_digest = canonical_sha256(&(RETRY_LEASE_DOMAIN, &identity)).map_err(|_| { + ApplicationProblem::invalid_request(SafeDiagnostic { + code: "application.work-retry.invalid".to_owned(), + message: "The Work retry command is invalid.".to_owned(), + }) + })?; let lease_id = WorkLeaseId::new(format!( "work-retry-lease:{}", lease_digest.as_str().trim_start_matches("sha256:") )) - .map_err(|_| invalid_problem())?; + .map_err(|_| { + ApplicationProblem::invalid_request(SafeDiagnostic { + code: "application.work-retry.invalid".to_owned(), + message: "The Work retry command is invalid.".to_owned(), + }) + })?; let lease = WorkLeaseFenceV1::new( lease_id, WorkFenceEpochV1::new(epoch).map_err(contract_problem)?, @@ -797,17 +827,6 @@ fn admit(context: &RequestContext, observed_at: UtcMicros) -> Result<(), Applica } } -fn invalid_problem() -> ApplicationProblem { - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { - code: "application.work-retry.invalid".to_owned(), - message: "The Work retry command is invalid.".to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::CorrectRequest], - } -} - fn retry_receipt_problem(_error: ApplicationContractError) -> ApplicationProblem { ApplicationProblem::unavailable(SafeDiagnostic { code: "application.work-retry.receipt-unavailable".to_owned(), @@ -887,7 +906,10 @@ fn effect_storage_problem(error: WorkAttemptEffectStorageErrorV1) -> Application } fn contract_problem(_error: WorkRuntimeContractError) -> ApplicationProblem { - invalid_problem() + ApplicationProblem::invalid_request(SafeDiagnostic { + code: "application.work-retry.invalid".to_owned(), + message: "The Work retry command is invalid.".to_owned(), + }) } #[cfg(test)] diff --git a/crates/tracedecay-contracts/src/work_run_control.rs b/crates/tracedecay-contracts/src/work_run_control.rs index 97b51937e3..45e1d25945 100644 --- a/crates/tracedecay-contracts/src/work_run_control.rs +++ b/crates/tracedecay-contracts/src/work_run_control.rs @@ -625,28 +625,20 @@ fn contract_problem(error: WorkRunControlContractError) -> ApplicationProblem { | WorkRunControlContractError::DuplicateFencedAttempt | WorkRunControlContractError::InvalidBlockedIntervalRevision | WorkRunControlContractError::InvalidBlockedIntervalClosure => { - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { - code: "application.work-run-control.invalid-transition".to_owned(), - message: "The Work run control command or stored state is invalid.".to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::CorrectRequest], - } + ApplicationProblem::invalid_request(SafeDiagnostic { + code: "application.work-run-control.invalid-transition".to_owned(), + message: "The Work run control command or stored state is invalid.".to_owned(), + }) } } } fn invalid_pending_interval_limit_problem() -> ApplicationProblem { - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { - code: "application.work-run-control.invalid-pending-interval-limit".to_owned(), - message: "The Work blocked-interval recovery page limit must be between 1 and 128." - .to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::CorrectRequest], - } + ApplicationProblem::invalid_request(SafeDiagnostic { + code: "application.work-run-control.invalid-pending-interval-limit".to_owned(), + message: "The Work blocked-interval recovery page limit must be between 1 and 128." + .to_owned(), + }) } fn workflow_steps_for_live_attempts( @@ -673,15 +665,10 @@ fn workflow_steps_for_live_attempts( } fn invalid_open_interval_durable_problem() -> ApplicationProblem { - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { - code: "application.work-run-control.open-interval-durable".to_owned(), - message: "Only a settled Work blocked interval can be marked durably delivered." - .to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::CorrectRequest], - } + ApplicationProblem::invalid_request(SafeDiagnostic { + code: "application.work-run-control.open-interval-durable".to_owned(), + message: "Only a settled Work blocked interval can be marked durably delivered.".to_owned(), + }) } fn authority_conflict_problem() -> ApplicationProblem { diff --git a/crates/tracedecay-contracts/src/work_synthesis.rs b/crates/tracedecay-contracts/src/work_synthesis.rs index dfc9f2bf64..d52bc5a6e5 100644 --- a/crates/tracedecay-contracts/src/work_synthesis.rs +++ b/crates/tracedecay-contracts/src/work_synthesis.rs @@ -33,9 +33,9 @@ use crate::work_attempt::{ }; use crate::workflow_synthesis::WorkflowSynthesisDraft; use crate::{ - ApplicationProblem, LegalAction, RequestContext, RetryDirective, SafeDiagnostic, - WorkGraphReadPortV1, WorkProductAttemptAdmissionPortV1, WorkProductBindingV1, - WorkProductOwnerAuthorizationPortV1, WorkProductRevisionPinsV1, + ApplicationProblem, RequestContext, SafeDiagnostic, WorkGraphReadPortV1, + WorkProductAttemptAdmissionPortV1, WorkProductBindingV1, WorkProductOwnerAuthorizationPortV1, + WorkProductRevisionPinsV1, }; const WORK_SYNTHESIS_SOURCE_SET_DOMAIN: &str = @@ -218,27 +218,27 @@ where registered_topology, )?; if command.sources.is_empty() { - return Err(invalid_problem( - "application.work-synthesis.no-sources", - "A synthesis attempt must name at least one source attempt.", - )); + return Err(ApplicationProblem::invalid_request(SafeDiagnostic { + code: ("application.work-synthesis.no-sources").to_owned(), + message: ("A synthesis attempt must name at least one source attempt.").to_owned(), + })); } let mut seen = BTreeSet::new(); for source in &command.sources { if !seen.insert(source.clone()) { - return Err(invalid_problem( - "application.work-synthesis.duplicate-source", - "A synthesis source attempt was named more than once.", - )); + return Err(ApplicationProblem::invalid_request(SafeDiagnostic { + code: ("application.work-synthesis.duplicate-source").to_owned(), + message: ("A synthesis source attempt was named more than once.").to_owned(), + })); } if source.task_id() == &command.start.task_id && source.run_id() == &command.start.run_id && source.attempt_id() == &command.start.attempt_id { - return Err(invalid_problem( - "application.work-synthesis.self-citation", - "A synthesis attempt cannot name itself as a source.", - )); + return Err(ApplicationProblem::invalid_request(SafeDiagnostic { + code: ("application.work-synthesis.self-citation").to_owned(), + message: ("A synthesis attempt cannot name itself as a source.").to_owned(), + })); } } let request_digest = canonical_sha256(&(WORK_SYNTHESIS_REQUEST_DOMAIN, &command)) @@ -394,17 +394,6 @@ fn evidence_groups(sources: &[WorkSynthesisSourceEnvelopeV1]) -> Vec ApplicationProblem { - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { - code: code.to_owned(), - message: message.to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::CorrectRequest], - } -} - fn contract_problem() -> ApplicationProblem { ApplicationProblem::unavailable(SafeDiagnostic { code: "application.work-synthesis.evidence-inconsistent".to_owned(), diff --git a/crates/tracedecay-daemon-service/src/application_surface/multi_root_http.rs b/crates/tracedecay-daemon-service/src/application_surface/multi_root_http.rs index 299156ab5a..a9ba5ca6f5 100644 --- a/crates/tracedecay-daemon-service/src/application_surface/multi_root_http.rs +++ b/crates/tracedecay-daemon-service/src/application_surface/multi_root_http.rs @@ -16,9 +16,9 @@ use tracedecay_contracts::multi_root::{ MultiRootApplicationOperation, multi_root_executable_binding_registry, }; use tracedecay_contracts::{ - ApplicationProblem, AuthorizedScopeSet, LegalAction, MultiRootExecuteRequestV1, - MultiRootQueryPageV1, MultiRootScopeSetCasRequestV1, MultiRootScopeSetCasResultV1, - MultiRootScopeSetReadRequestV1, RequestId, RetryDirective, SafeDiagnostic, + ApplicationProblem, AuthorizedScopeSet, MultiRootExecuteRequestV1, MultiRootQueryPageV1, + MultiRootScopeSetCasRequestV1, MultiRootScopeSetCasResultV1, MultiRootScopeSetReadRequestV1, + RequestId, SafeDiagnostic, }; use tracedecay_tool_catalog::RouteExposureV1; @@ -208,11 +208,7 @@ fn invalid_request_response(request_id: RequestId) -> Response { }; tracedecay_api::adapter_problem_response( request_id, - ApplicationProblem::InvalidRequest { - diagnostic, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::CorrectRequest], - }, + ApplicationProblem::invalid_request(diagnostic), ) } diff --git a/crates/tracedecay-daemon-service/src/application_surface/operation_events.rs b/crates/tracedecay-daemon-service/src/application_surface/operation_events.rs index 66e1286cc4..a4a0249744 100644 --- a/crates/tracedecay-daemon-service/src/application_surface/operation_events.rs +++ b/crates/tracedecay-daemon-service/src/application_surface/operation_events.rs @@ -857,14 +857,12 @@ pub(super) fn operation_event_problem( OperationEventError::InvalidContext(_) | OperationEventError::InvalidProgress | OperationEventError::InvalidTerminal(_) - | OperationEventError::InvalidTestRunEvent => ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { + | OperationEventError::InvalidTestRunEvent => { + ApplicationProblem::invalid_request(SafeDiagnostic { code: "operation_event.invalid_request".to_owned(), message: "The operation-event request is invalid".to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::CorrectRequest], - }, + }) + } // Idempotency facts: the identity or terminal receipt is already // published, so the client re-reads current state instead of retrying // the same publish. diff --git a/crates/tracedecay-daemon-service/src/application_surface/registered_http.rs b/crates/tracedecay-daemon-service/src/application_surface/registered_http.rs index 596f93f10b..91dd2da06a 100644 --- a/crates/tracedecay-daemon-service/src/application_surface/registered_http.rs +++ b/crates/tracedecay-daemon-service/src/application_surface/registered_http.rs @@ -5,8 +5,8 @@ use tracedecay_api::{ WorkflowOperation, }; use tracedecay_contracts::{ - ApplicationEnvelope, ApplicationProblem, ApplicationProblemEnvelope, LegalAction, - ProblemOwningLayer, RequestId, ResultContractRef, RetryDirective, SafeDiagnostic, + ApplicationEnvelope, ApplicationProblem, ApplicationProblemEnvelope, ProblemOwningLayer, + RequestId, ResultContractRef, RetryDirective, SafeDiagnostic, }; use tracedecay_daemon_protocol::{ ApplicationSurfaceAdapterError, DaemonInvocationError, InvocationCancellationPolicy, @@ -388,14 +388,10 @@ where tracedecay_daemon_protocol::DaemonInvocationOutcome::Problem { problem } => match problem { tracedecay_daemon_protocol::DaemonInvocationProblem::InvalidRequest | tracedecay_daemon_protocol::DaemonInvocationProblem::UnsupportedRevision => { - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { + ApplicationProblem::invalid_request(SafeDiagnostic { code: problem_code("invalid_request"), message: format!("The {family} application request is invalid"), - }, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::CorrectRequest], - } + }) } tracedecay_daemon_protocol::DaemonInvocationProblem::NotFoundOrNotAuthorized => { ApplicationProblem::not_found_or_not_authorized(RetryDirective::Never) diff --git a/crates/tracedecay-daemon-service/src/invocation/github_stack_signal.rs b/crates/tracedecay-daemon-service/src/invocation/github_stack_signal.rs index 23e6ae4da6..f62217b8f7 100644 --- a/crates/tracedecay-daemon-service/src/invocation/github_stack_signal.rs +++ b/crates/tracedecay-daemon-service/src/invocation/github_stack_signal.rs @@ -194,13 +194,8 @@ fn github_stack_signal_evidence( } fn invalid_github_stack_signal_request() -> ApplicationProblem { - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { - code: "invalid_github_stack_signal_expand_request".to_owned(), - message: "The GitHub stack signal request does not match its operation contract" - .to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: vec![tracedecay_contracts::LegalAction::CorrectRequest], - } + ApplicationProblem::invalid_request(SafeDiagnostic { + code: "invalid_github_stack_signal_expand_request".to_owned(), + message: "The GitHub stack signal request does not match its operation contract".to_owned(), + }) } diff --git a/crates/tracedecay-daemon-service/src/invocation/native_integration.rs b/crates/tracedecay-daemon-service/src/invocation/native_integration.rs index 7b45dc34fa..75f9f54ca4 100644 --- a/crates/tracedecay-daemon-service/src/invocation/native_integration.rs +++ b/crates/tracedecay-daemon-service/src/invocation/native_integration.rs @@ -911,15 +911,10 @@ fn stack_coordinator_contract_error( /// its operation contract, or whose bounded authority receipt cannot be /// minted from the values the request supplied. fn invalid_native_integration_request() -> ApplicationProblem { - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { - code: "invalid_native_integration_request".to_owned(), - message: "The native-integration request does not match its operation contract" - .to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: vec![tracedecay_contracts::LegalAction::CorrectRequest], - } + ApplicationProblem::invalid_request(SafeDiagnostic { + code: "invalid_native_integration_request".to_owned(), + message: "The native-integration request does not match its operation contract".to_owned(), + }) } /// Mint the request context and authority for exactly one native-integration diff --git a/crates/tracedecay-daemon-service/src/invocation/source_edit.rs b/crates/tracedecay-daemon-service/src/invocation/source_edit.rs index cf79d48429..8c6a43e759 100644 --- a/crates/tracedecay-daemon-service/src/invocation/source_edit.rs +++ b/crates/tracedecay-daemon-service/src/invocation/source_edit.rs @@ -40,14 +40,10 @@ pub(super) async fn execute_source_edit( Err(_) => { return application_problem( request_id, - ApplicationProblem::InvalidRequest { - diagnostic: tracedecay_contracts::SafeDiagnostic { - code: "source_edit.invalid_request_id".to_owned(), - message: "The source-edit request id is invalid".to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: vec![tracedecay_contracts::LegalAction::CorrectRequest], - }, + ApplicationProblem::invalid_request(tracedecay_contracts::SafeDiagnostic { + code: "source_edit.invalid_request_id".to_owned(), + message: "The source-edit request id is invalid".to_owned(), + }), ); } }; @@ -94,14 +90,10 @@ pub(super) async fn execute_source_edit_reconcile( Err(_) => { return application_problem( request_id, - ApplicationProblem::InvalidRequest { - diagnostic: tracedecay_contracts::SafeDiagnostic { - code: "source_edit.invalid_request_id".to_owned(), - message: "The source-edit request id is invalid".to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: vec![tracedecay_contracts::LegalAction::CorrectRequest], - }, + ApplicationProblem::invalid_request(tracedecay_contracts::SafeDiagnostic { + code: "source_edit.invalid_request_id".to_owned(), + message: "The source-edit request id is invalid".to_owned(), + }), ); } }; @@ -148,14 +140,10 @@ pub(super) async fn execute_source_edit_rollback( Err(_) => { return application_problem( request_id, - ApplicationProblem::InvalidRequest { - diagnostic: tracedecay_contracts::SafeDiagnostic { - code: "source_edit.invalid_request_id".to_owned(), - message: "The source-edit request id is invalid".to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: vec![tracedecay_contracts::LegalAction::CorrectRequest], - }, + ApplicationProblem::invalid_request(tracedecay_contracts::SafeDiagnostic { + code: "source_edit.invalid_request_id".to_owned(), + message: "The source-edit request id is invalid".to_owned(), + }), ); } }; @@ -189,15 +177,11 @@ fn map_source_edit_error( SourceEditOwnerError::NotAuthorized => concealed_application_problem(request_id), SourceEditOwnerError::InvalidContract => application_problem( request_id, - ApplicationProblem::InvalidRequest { - diagnostic: tracedecay_contracts::SafeDiagnostic { - code: "source_edit.invalid_request".to_owned(), - message: "The source-edit request does not match its invocation contract" - .to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: vec![tracedecay_contracts::LegalAction::CorrectRequest], - }, + ApplicationProblem::invalid_request(tracedecay_contracts::SafeDiagnostic { + code: "source_edit.invalid_request".to_owned(), + message: "The source-edit request does not match its invocation contract" + .to_owned(), + }), ), SourceEditOwnerError::Cancelled => { application_problem(request_id, ApplicationProblem::cancelled_before_admission()) diff --git a/crates/tracedecay-daemon-service/src/invocation/work/attempt_operations.rs b/crates/tracedecay-daemon-service/src/invocation/work/attempt_operations.rs index 31835c757e..69ec2950f0 100644 --- a/crates/tracedecay-daemon-service/src/invocation/work/attempt_operations.rs +++ b/crates/tracedecay-daemon-service/src/invocation/work/attempt_operations.rs @@ -5,9 +5,9 @@ use std::sync::Arc; use tracedecay_application::observability::BoundedObservabilityProducerV1; use tracedecay_contracts::{ - AdmitWorkSynthesisCommand, ApplicationProblem, CancelWorkAttemptCommand, Deadline, LegalAction, - RequestContext, RequestId, ResumeWorkAttemptsCommand, RetryDirective, - RetryWorkAttemptCommandV1, SafeDiagnostic, StartWorkAttemptCommand, WorkAttemptStatusRequestV1, + AdmitWorkSynthesisCommand, ApplicationProblem, CancelWorkAttemptCommand, Deadline, + RequestContext, RequestId, ResumeWorkAttemptsCommand, RetryWorkAttemptCommandV1, + SafeDiagnostic, StartWorkAttemptCommand, WorkAttemptStatusRequestV1, WorkSynthesisAttemptV1, WorkflowArtifactStorePort, }; use tracedecay_domain::{ManifestDigest, UtcMicros, WorkAttemptStateV1, WorkAttemptV1}; @@ -26,14 +26,10 @@ use super::{ }; fn consume_synthesis_bytes(remaining: &mut u64, bytes: u64) -> Result<(), ApplicationProblem> { - *remaining = remaining.checked_sub(bytes).ok_or_else(|| ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { + *remaining = remaining.checked_sub(bytes).ok_or_else(|| ApplicationProblem::invalid_request(SafeDiagnostic { code: "application.work-synthesis.source-context-oversized".to_owned(), message: "The synthesis instructions and source payloads exceed the admitted protocol byte bound.".to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::CorrectRequest], - })?; + }))?; Ok(()) } diff --git a/crates/tracedecay-daemon-service/src/invocation/work/outcome.rs b/crates/tracedecay-daemon-service/src/invocation/work/outcome.rs index 1e546ffbfb..d417c84075 100644 --- a/crates/tracedecay-daemon-service/src/invocation/work/outcome.rs +++ b/crates/tracedecay-daemon-service/src/invocation/work/outcome.rs @@ -69,32 +69,26 @@ pub(super) fn work_product_problem(error: WorkProductApplicationErrorV1) -> Appl ApplicationProblem::cancelled_before_admission() } WorkProductApplicationErrorV1::TimedOut => ApplicationProblem::timed_out_before_admission(), - WorkProductApplicationErrorV1::InvalidRequest => ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { + WorkProductApplicationErrorV1::InvalidRequest => { + ApplicationProblem::invalid_request(SafeDiagnostic { code: "work.invalid_graph_operation".to_owned(), message: "The Work graph request is invalid".to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: vec![tracedecay_contracts::LegalAction::CorrectRequest], - }, + }) + } // A read under this selection succeeds and discloses what it left out; // a mutation cannot, because the head it would pin is the covered // slice's, not the journal's. The refusal therefore names the cause and // the remedy instead of hiding behind the concealed not-found answer // the old fail-closed refusal produced. WorkProductApplicationErrorV1::SelectionCoverageIncomplete => { - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { - code: "work.selection_coverage_incomplete".to_owned(), - message: "The Work selection covers only part of the owner's journal, so no \ + ApplicationProblem::invalid_request(SafeDiagnostic { + code: "work.selection_coverage_incomplete".to_owned(), + message: "The Work selection covers only part of the owner's journal, so no \ graph mutation can be prepared or submitted against it; widen the \ selection to the relation scopes the excluded events were admitted \ under" - .to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: vec![tracedecay_contracts::LegalAction::CorrectRequest], - } + .to_owned(), + }) } WorkProductApplicationErrorV1::VersionConflict => { ApplicationProblem::stale(SafeDiagnostic { diff --git a/crates/tracedecay-daemon-service/src/invocation/work/workflow_dispatch.rs b/crates/tracedecay-daemon-service/src/invocation/work/workflow_dispatch.rs index 4fb37d445f..d6e874dbdb 100644 --- a/crates/tracedecay-daemon-service/src/invocation/work/workflow_dispatch.rs +++ b/crates/tracedecay-daemon-service/src/invocation/work/workflow_dispatch.rs @@ -247,11 +247,7 @@ pub(crate) async fn execute_workflow_application( { return DaemonInvocationResponse::application_problem( request_id, - tracedecay_contracts::ApplicationProblem::InvalidRequest { - diagnostic, - retry: tracedecay_contracts::RetryDirective::Never, - legal_actions: vec![tracedecay_contracts::LegalAction::CorrectRequest], - }, + tracedecay_contracts::ApplicationProblem::invalid_request(diagnostic), ); } complete_workflow_read( diff --git a/crates/tracedecay-daemon-service/src/invocation/work/workflow_effect_journal.rs b/crates/tracedecay-daemon-service/src/invocation/work/workflow_effect_journal.rs index 5c51fa9ba3..0a46710414 100644 --- a/crates/tracedecay-daemon-service/src/invocation/work/workflow_effect_journal.rs +++ b/crates/tracedecay-daemon-service/src/invocation/work/workflow_effect_journal.rs @@ -236,11 +236,7 @@ pub(super) fn execute_journaled_workflow_effect( { return DaemonInvocationResponse::application_problem( request_id, - ApplicationProblem::InvalidRequest { - diagnostic: diagnostic.clone(), - retry: tracedecay_contracts::RetryDirective::Never, - legal_actions: vec![tracedecay_contracts::LegalAction::CorrectRequest], - }, + ApplicationProblem::invalid_request(diagnostic.clone()), ); } let outcome = match workflow_effect_outcome(terminal) { diff --git a/crates/tracedecay-daemon-service/src/invocation/work/workflow_run_control.rs b/crates/tracedecay-daemon-service/src/invocation/work/workflow_run_control.rs index 9fa63a25a1..b6864ab7a0 100644 --- a/crates/tracedecay-daemon-service/src/invocation/work/workflow_run_control.rs +++ b/crates/tracedecay-daemon-service/src/invocation/work/workflow_run_control.rs @@ -7,8 +7,8 @@ use std::sync::atomic::AtomicBool; use tracedecay_application::work::workflow_topology::WorkflowTopologyError; use tracedecay_contracts::{ - ApplicationProblem, LegalAction, RequestContext, RetryDirective, SafeDiagnostic, - WorkflowCatalogAdmissionError, WorkflowCoordinationError, WorkflowRunStoragePort, + ApplicationProblem, RequestContext, SafeDiagnostic, WorkflowCatalogAdmissionError, + WorkflowCoordinationError, WorkflowRunStoragePort, }; use tracedecay_domain::{ManifestDigest, UtcMicros}; @@ -447,11 +447,7 @@ pub(super) fn workflow_coordination_application_problem( | WorkflowCoordinationError::DefinitionNotFound | WorkflowCoordinationError::AuthorityUnavailable(_) => return None, }; - Some(ApplicationProblem::InvalidRequest { - diagnostic, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::CorrectRequest], - }) + Some(ApplicationProblem::invalid_request(diagnostic)) } pub(super) fn workflow_run_storage_problem( diff --git a/crates/tracedecay-mcp/src/handlers/multi_root.rs b/crates/tracedecay-mcp/src/handlers/multi_root.rs index 0d4b842edf..c57537ddbe 100644 --- a/crates/tracedecay-mcp/src/handlers/multi_root.rs +++ b/crates/tracedecay-mcp/src/handlers/multi_root.rs @@ -5,7 +5,7 @@ use serde_json::{Value, json}; use tracedecay_contracts::multi_root::MultiRootApplicationOperation; use tracedecay_contracts::{ ApplicationEnvelope, ApplicationOutcome, ApplicationProblem, ApplicationProblemEnvelope, - CancellationSignal, Deadline, LegalAction, MultiRootExecuteRequestV1, + CancellationSignal, Deadline, MultiRootExecuteRequestV1, MultiRootScopeSetCasRequestV1, MultiRootScopeSetReadRequestV1, ProblemOwningLayer, RequestId, ResultContractRef, RetryDirective, SafeDiagnostic, }; @@ -236,14 +236,10 @@ fn invalid_request( problem_result( operation, request_id, - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { - code: "multi_root.invalid_request".to_owned(), - message: "The multi-root application request is invalid".to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::CorrectRequest], - }, + ApplicationProblem::invalid_request(SafeDiagnostic { + code: "multi_root.invalid_request".to_owned(), + message: "The multi-root application request is invalid".to_owned(), + }), ) } @@ -271,14 +267,10 @@ fn problem_result( fn daemon_problem(problem: DaemonInvocationProblem) -> ApplicationProblem { match problem { DaemonInvocationProblem::InvalidRequest | DaemonInvocationProblem::UnsupportedRevision => { - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { - code: "multi_root.invalid_request".to_owned(), - message: "The multi-root application request is invalid".to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::CorrectRequest], - } + ApplicationProblem::invalid_request(SafeDiagnostic { + code: "multi_root.invalid_request".to_owned(), + message: "The multi-root application request is invalid".to_owned(), + }) } DaemonInvocationProblem::NotFoundOrNotAuthorized => { ApplicationProblem::not_found_or_not_authorized(RetryDirective::Never) diff --git a/crates/tracedecay-session-runtime/src/retained/session.rs b/crates/tracedecay-session-runtime/src/retained/session.rs index 5ce05e0b45..e847d1625f 100644 --- a/crates/tracedecay-session-runtime/src/retained/session.rs +++ b/crates/tracedecay-session-runtime/src/retained/session.rs @@ -488,13 +488,7 @@ impl MessageSearchInput { RetainedSurfaceExecutionErrorV1::InvalidRequest, |diagnostic| { RetainedSurfaceExecutionErrorV1::ApplicationProblem( - tracedecay_contracts::ApplicationProblem::InvalidRequest { - diagnostic, - retry: tracedecay_contracts::RetryDirective::Never, - legal_actions: vec![ - tracedecay_contracts::LegalAction::CorrectRequest, - ], - }, + tracedecay_contracts::ApplicationProblem::invalid_request(diagnostic), ) }, ) diff --git a/crates/tracedecay/src/daemon/invocation_tests/work_tests.rs b/crates/tracedecay/src/daemon/invocation_tests/work_tests.rs index ddd6cf9294..c36ceebcf3 100644 --- a/crates/tracedecay/src/daemon/invocation_tests/work_tests.rs +++ b/crates/tracedecay/src/daemon/invocation_tests/work_tests.rs @@ -661,14 +661,12 @@ async fn registered_work_services_dispatch_the_core_lifecycle() { }; assert_eq!( problem, - tracedecay_contracts::ApplicationProblem::InvalidRequest { - diagnostic: tracedecay_contracts::SafeDiagnostic { + tracedecay_contracts::ApplicationProblem::invalid_request( + tracedecay_contracts::SafeDiagnostic { code: "work.invalid_graph_operation".to_owned(), message: "The Work graph request is invalid".to_owned(), - }, - retry: tracedecay_contracts::RetryDirective::Never, - legal_actions: vec![tracedecay_contracts::LegalAction::CorrectRequest], - }, + } + ), "an unroutable admission is a request to correct, not an authority to retry" ); } From be16e8439cd490b7048a2e81a232c3820d068b00 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:46:07 +0000 Subject: [PATCH 164/182] simplify(pass-5/5): drop inert flags and gate query dep criterion 0.5.1's html_reports feature is empty and unreferenced. tracedecay's temporal-query path is test-helpers only, so the edge is optional behind that feature. Co-authored-by: Zack Jackson --- crates/tracedecay-code-index/Cargo.toml | 2 +- crates/tracedecay-global-db/Cargo.toml | 2 +- crates/tracedecay-graph-db/Cargo.toml | 2 +- crates/tracedecay/Cargo.toml | 6 ++++-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/tracedecay-code-index/Cargo.toml b/crates/tracedecay-code-index/Cargo.toml index db864b358f..261f638d68 100644 --- a/crates/tracedecay-code-index/Cargo.toml +++ b/crates/tracedecay-code-index/Cargo.toml @@ -89,7 +89,7 @@ tracedecay-runtime-core = { path = "../tracedecay-runtime-core", version = "0.1. tree-sitter = "0.26" [dev-dependencies] -criterion = { version = "0.5", features = ["html_reports"] } +criterion = "0.5" rusqlite = { version = "0.40.1", default-features = false, features = ["backup"] } tempfile = "3" tracedecay-code-extraction = { path = "../tracedecay-code-extraction", version = "0.1.0", default-features = false, features = ["lite", "test-helpers"] } diff --git a/crates/tracedecay-global-db/Cargo.toml b/crates/tracedecay-global-db/Cargo.toml index 7eeadf9d8d..0935da236e 100644 --- a/crates/tracedecay-global-db/Cargo.toml +++ b/crates/tracedecay-global-db/Cargo.toml @@ -58,7 +58,7 @@ tracedecay-temporal-query = { path = "../tracedecay-temporal-query", version = " tempfile = { version = "3", optional = true } [dev-dependencies] -criterion = { version = "0.5", features = ["async_tokio", "html_reports"] } +criterion = { version = "0.5", features = ["async_tokio"] } tempfile = "3" tokio = { version = "1", features = ["full", "test-util"] } # Test-only helper surfaces this crate's test targets reach across crate diff --git a/crates/tracedecay-graph-db/Cargo.toml b/crates/tracedecay-graph-db/Cargo.toml index 2ef568a829..5505c4c908 100644 --- a/crates/tracedecay-graph-db/Cargo.toml +++ b/crates/tracedecay-graph-db/Cargo.toml @@ -83,7 +83,7 @@ tracedecay-store = { path = "../tracedecay-store", version = "0.1.0" } tracing = "0.1" [dev-dependencies] -criterion = { version = "0.5", features = ["html_reports"] } +criterion = "0.5" rusqlite = { version = "0.40.1", default-features = false, features = ["backup"] } tempfile = "3" tracedecay-rusqlite-runtime = { path = "../tracedecay-rusqlite-runtime", version = "0.1.0" } diff --git a/crates/tracedecay/Cargo.toml b/crates/tracedecay/Cargo.toml index 527a890030..da35d4ac8b 100644 --- a/crates/tracedecay/Cargo.toml +++ b/crates/tracedecay/Cargo.toml @@ -203,6 +203,7 @@ test-helpers = [ "tracedecay-session-runtime/test-helpers", "tracedecay-sessions/test-helpers", "tracedecay-store-runtime/test-helpers", + "dep:tracedecay-temporal-query", ] test-transport = [ @@ -313,10 +314,11 @@ tracedecay-session-memory = { path = "../tracedecay-session-memory", version = " tracedecay-session-runtime = { path = "../tracedecay-session-runtime", version = "0.1.0" } tracedecay-store-runtime = { path = "../tracedecay-store-runtime", version = "0.1.0" } tracedecay-session-temporal-store = { path = "../tracedecay-session-temporal-store", version = "0.1.0" } +# Only the `test-helpers` session-temporal bench fixture names this crate. +tracedecay-temporal-query = { path = "../tracedecay-temporal-query", version = "0.1.0", optional = true } tracedecay-lcm = { path = "../tracedecay-lcm", version = "0.1.0" } tracedecay-sessions = { path = "../tracedecay-sessions", version = "0.1.0" } tracedecay-source-edit = { path = "../tracedecay-source-edit", version = "0.1.0" } -tracedecay-temporal-query = { path = "../tracedecay-temporal-query", version = "0.1.0" } tracedecay-rusqlite-runtime = { path = "../tracedecay-rusqlite-runtime", version = "0.1.0" } rusqlite = { version = "0.40.1", default-features = false, features = ["backup"] } tracedecay-store = { path = "../tracedecay-store", version = "0.1.0" } @@ -358,7 +360,7 @@ toml = "1" regex = "1.12.3" filetime = "0.2" rmcp = { version = "3.0.1", default-features = false, features = ["client", "server", "transport-async-rw"] } -criterion = { version = "0.5", features = ["async_tokio", "html_reports"] } +criterion = { version = "0.5", features = ["async_tokio"] } # test-util enables tokio::test(start_paused = true) so timer-driven unit # tests (daemon restart-grace windows) run on virtual time instead of real # sleeps. From d7e68cab0d0f650498bf6d49caea380d3ba58787 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:57:25 +0000 Subject: [PATCH 165/182] refactor(store): share sql row text decode Co-authored-by: Zack Jackson --- .../src/git_index_transactions/store.rs | 10 ++-- .../src/native_integration/store.rs | 10 ++-- .../src/sqlite_persist.rs | 5 ++ crates/tracedecay-runtime-core/src/db/mod.rs | 2 + .../src/db/row_codec.rs | 52 +++++++++++++++++++ .../src/workflow.rs | 5 +- .../src/workflow/census.rs | 5 +- .../src/fact_store/primitives.rs | 8 +-- 8 files changed, 78 insertions(+), 19 deletions(-) create mode 100644 crates/tracedecay-runtime-core/src/db/row_codec.rs diff --git a/crates/tracedecay-global-db/src/git_index_transactions/store.rs b/crates/tracedecay-global-db/src/git_index_transactions/store.rs index 3db2cbf1cf..b8a06771f5 100644 --- a/crates/tracedecay-global-db/src/git_index_transactions/store.rs +++ b/crates/tracedecay-global-db/src/git_index_transactions/store.rs @@ -1196,16 +1196,15 @@ fn receipt_outcome_code(outcome: GitIndexReceiptOutcomeV1) -> &'static str { } fn encode(value: &T) -> GitIndexTransactionStoreResult { - serde_json::to_string(value).map_err(|error| invalid(error.to_string())) + crate::sqlite_persist::encode_json(value).map_err(|error| invalid(error.to_string())) } fn decode(value: &str) -> GitIndexTransactionStoreResult { - serde_json::from_str(value).map_err(|error| invalid(error.to_string())) + crate::sqlite_persist::decode_json(value).map_err(|error| invalid(error.to_string())) } fn text(row: &Row, column: i32, field: &'static str) -> GitIndexTransactionStoreResult { - row.get::(column) - .map_err(|error| invalid(format!("read {field}: {error}"))) + crate::sqlite_persist::row_text(row, column, field).map_err(invalid) } fn optional_text( @@ -1213,8 +1212,7 @@ fn optional_text( column: i32, field: &'static str, ) -> GitIndexTransactionStoreResult> { - row.get::>(column) - .map_err(|error| invalid(format!("read {field}: {error}"))) + crate::sqlite_persist::row_optional_text(row, column, field).map_err(invalid) } fn invalid(message: impl Into) -> GitIndexTransactionStoreError { diff --git a/crates/tracedecay-global-db/src/native_integration/store.rs b/crates/tracedecay-global-db/src/native_integration/store.rs index 7175630fae..cf0308655d 100644 --- a/crates/tracedecay-global-db/src/native_integration/store.rs +++ b/crates/tracedecay-global-db/src/native_integration/store.rs @@ -976,13 +976,13 @@ fn terminal_outcome_code(outcome: NativeIntegrationTerminalOutcomeV1) -> &'stati } pub(super) fn encode(value: &T) -> NativeIntegrationStoreResult { - serde_json::to_string(value).map_err(|error| invalid(error.to_string())) + crate::sqlite_persist::encode_json(value).map_err(|error| invalid(error.to_string())) } pub(super) fn decode( value: &str, ) -> NativeIntegrationStoreResult { - serde_json::from_str(value).map_err(|error| invalid(error.to_string())) + crate::sqlite_persist::decode_json(value).map_err(|error| invalid(error.to_string())) } pub(super) fn text( @@ -990,8 +990,7 @@ pub(super) fn text( column: i32, field: &'static str, ) -> NativeIntegrationStoreResult { - row.get::(column) - .map_err(|error| invalid(format!("read {field}: {error}"))) + crate::sqlite_persist::row_text(row, column, field).map_err(invalid) } fn optional_text( @@ -999,8 +998,7 @@ fn optional_text( column: i32, field: &'static str, ) -> NativeIntegrationStoreResult> { - row.get::>(column) - .map_err(|error| invalid(format!("read {field}: {error}"))) + crate::sqlite_persist::row_optional_text(row, column, field).map_err(invalid) } pub(super) fn invalid(message: impl Into) -> NativeIntegrationStoreError { diff --git a/crates/tracedecay-global-db/src/sqlite_persist.rs b/crates/tracedecay-global-db/src/sqlite_persist.rs index e20beb065d..3281e2aa1b 100644 --- a/crates/tracedecay-global-db/src/sqlite_persist.rs +++ b/crates/tracedecay-global-db/src/sqlite_persist.rs @@ -11,6 +11,11 @@ use std::future::Future; use tracedecay_runtime_core::db::engine::{self, Transaction}; +pub(crate) use tracedecay_runtime_core::db::{ + decode_stored_json as decode_json, encode_stored_json as encode_json, + optional_text_column as row_optional_text, text_column as row_text, +}; + use crate::RegisteredGlobalDbWriteTransaction; /// Write transaction that can commit or roll back through the engine. diff --git a/crates/tracedecay-runtime-core/src/db/mod.rs b/crates/tracedecay-runtime-core/src/db/mod.rs index c025e37328..e0354c3fc1 100644 --- a/crates/tracedecay-runtime-core/src/db/mod.rs +++ b/crates/tracedecay-runtime-core/src/db/mod.rs @@ -1,6 +1,8 @@ mod access; mod connection; pub mod engine; +mod row_codec; +pub use row_codec::{decode_stored_json, encode_stored_json, optional_text_column, text_column}; mod evidence_assembly; mod external_source; mod file_identity; diff --git a/crates/tracedecay-runtime-core/src/db/row_codec.rs b/crates/tracedecay-runtime-core/src/db/row_codec.rs new file mode 100644 index 0000000000..b6f49e1a39 --- /dev/null +++ b/crates/tracedecay-runtime-core/src/db/row_codec.rs @@ -0,0 +1,52 @@ +//! Shared JSON and text-column decode for durable SQL rows. +//! +//! Git-index, native integration, and the fact store used to repeat the same +//! `serde_json` and `row.get` mappings. The column message names the field so +//! a missing cell is the same failure in every caller. + +use serde::Serialize; +use serde::de::DeserializeOwned; + +use super::engine::Row; + +pub fn encode_stored_json(value: &T) -> Result { + serde_json::to_string(value) +} + +pub fn decode_stored_json(value: &str) -> Result { + serde_json::from_str(value) +} + +pub fn text_column(row: &Row, column: i32, field: &'static str) -> Result { + row.get::(column) + .map_err(|error| format!("read {field}: {error}")) +} + +pub fn optional_text_column( + row: &Row, + column: i32, + field: &'static str, +) -> Result, String> { + row.get::>(column) + .map_err(|error| format!("read {field}: {error}")) +} + +#[cfg(test)] +mod tests { + use super::super::engine::{Row, Value}; + use super::{optional_text_column, text_column}; + + #[test] + fn text_column_names_the_field_and_keeps_null_optional() { + let row = Row::from_values(vec![Value::Text("kept".to_owned()), Value::Null]); + assert_eq!(text_column(&row, 0, "preview").expect("text"), "kept"); + assert_eq!( + text_column(&row, 2, "preview").expect_err("missing"), + "read preview: invalid column index 2" + ); + assert_eq!( + optional_text_column(&row, 1, "receipt").expect("null"), + None + ); + } +} diff --git a/crates/tracedecay-rusqlite-runtime/src/workflow.rs b/crates/tracedecay-rusqlite-runtime/src/workflow.rs index 8fbee458a4..42ee8fba61 100644 --- a/crates/tracedecay-rusqlite-runtime/src/workflow.rs +++ b/crates/tracedecay-rusqlite-runtime/src/workflow.rs @@ -96,8 +96,9 @@ impl WorkflowSqliteAuthority { let Some(ExactSqlValue::Text(stored_digest)) = row.values.get(1) else { return Err(WorkflowSqliteAuthorityBuildError::ResetRequired); }; - let definition = tracedecay_domain::decode_with_canonical_digest(payload, stored_digest) - .map_err(|_| WorkflowSqliteAuthorityBuildError::ResetRequired)?; + let definition: WorkflowDefinition = + tracedecay_domain::decode_with_canonical_digest(payload, stored_digest) + .map_err(|_| WorkflowSqliteAuthorityBuildError::ResetRequired)?; if definition.definition_id() != definition_id || definition.definition_version() != definition_version { diff --git a/crates/tracedecay-rusqlite-runtime/src/workflow/census.rs b/crates/tracedecay-rusqlite-runtime/src/workflow/census.rs index 1b566e3172..5533104e4a 100644 --- a/crates/tracedecay-rusqlite-runtime/src/workflow/census.rs +++ b/crates/tracedecay-rusqlite-runtime/src/workflow/census.rs @@ -22,8 +22,9 @@ fn decode_census( payload: &str, stored_digest: &str, ) -> Result { - let census = tracedecay_domain::decode_with_canonical_digest(payload, stored_digest) - .map_err(|_| WorkflowFanOutCensusError::InvalidHistory)?; + let census: WorkflowFanOutCensusV1 = + tracedecay_domain::decode_with_canonical_digest(payload, stored_digest) + .map_err(|_| WorkflowFanOutCensusError::InvalidHistory)?; census .validate() .map_err(|_| WorkflowFanOutCensusError::InvalidHistory)?; diff --git a/crates/tracedecay-session-memory/src/fact_store/primitives.rs b/crates/tracedecay-session-memory/src/fact_store/primitives.rs index 463653efa8..0ce5f591f3 100644 --- a/crates/tracedecay-session-memory/src/fact_store/primitives.rs +++ b/crates/tracedecay-session-memory/src/fact_store/primitives.rs @@ -4,7 +4,9 @@ use std::error::Error; use std::time::{SystemTime, UNIX_EPOCH}; use serde::{Serialize, de::DeserializeOwned}; -use tracedecay_runtime_core::db::DatabaseMemoryTransaction as Transaction; +use tracedecay_runtime_core::db::{ + DatabaseMemoryTransaction as Transaction, decode_stored_json, encode_stored_json, +}; use tracedecay_domain::{FactCategoryV1, FactOwnerV1, PayloadAccessState, UtcMicros}; use tracedecay_store::{FactReadControl, FactStoreError, FactStoreResult}; @@ -114,14 +116,14 @@ pub(super) fn to_json( value: &T, operation: &'static str, ) -> FactStoreResult { - serde_json::to_string(value).map_err(|error| storage_error(operation, error)) + encode_stored_json(value).map_err(|error| storage_error(operation, error)) } pub(super) fn from_json( value: &str, operation: &'static str, ) -> FactStoreResult { - serde_json::from_str(value).map_err(|error| storage_error(operation, error)) + decode_stored_json(value).map_err(|error| storage_error(operation, error)) } pub(super) fn row_string( From 9957baa33a46bd6f6f74517e203ba156aec9e2b2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 20:00:13 +0000 Subject: [PATCH 166/182] refactor(contracts): share coverage status mapping Session and LCM projections shared the coverage mode, status, reason, and hydration tables. Each table now lives once as a From impl. Co-authored-by: Zack Jackson --- .../retained_surfaces/sdk/results/session.rs | 109 ++++++++++++++++++ .../src/retained/lcm/output.rs | 78 ++----------- .../src/retained/lcm/retrieval.rs | 15 +-- .../src/retained/session.rs | 72 +----------- .../src/session_retrieval.rs | 2 +- 5 files changed, 131 insertions(+), 145 deletions(-) diff --git a/crates/tracedecay-contracts/src/retained_surfaces/sdk/results/session.rs b/crates/tracedecay-contracts/src/retained_surfaces/sdk/results/session.rs index 4c00ce099e..0b5e131812 100644 --- a/crates/tracedecay-contracts/src/retained_surfaces/sdk/results/session.rs +++ b/crates/tracedecay-contracts/src/retained_surfaces/sdk/results/session.rs @@ -1,5 +1,8 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use tracedecay_domain::{ + HydrationStateV1, SessionSourceCoverageReasonV1, SessionSourceCoverageStateV1, TemporalModeV1, +}; use super::{RetainedErrorV1, RetainedOutcomeStatusV1}; @@ -98,6 +101,21 @@ pub enum HydrationStateResultV1 { UnverifiableLegacy, } +impl From for HydrationStateResultV1 { + fn from(value: HydrationStateV1) -> Self { + match value { + HydrationStateV1::Available => Self::Available, + HydrationStateV1::RetainedButUnavailable => Self::RetainedButUnavailable, + HydrationStateV1::Redacted => Self::Redacted, + HydrationStateV1::Deleted => Self::Deleted, + HydrationStateV1::RetentionExpired => Self::RetentionExpired, + HydrationStateV1::Unauthorized => Self::Unauthorized, + HydrationStateV1::Locked => Self::Locked, + HydrationStateV1::UnverifiableLegacy => Self::UnverifiableLegacy, + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct TemporalOmissionV1 { @@ -151,6 +169,17 @@ pub enum SessionCoverageModeV1 { Forensic, } +impl From for SessionCoverageModeV1 { + fn from(value: TemporalModeV1) -> Self { + match value { + TemporalModeV1::Current => Self::Current, + TemporalModeV1::AsOf { cutoff } => Self::AsOf { cutoff: cutoff.0 }, + TemporalModeV1::Evolution => Self::Evolution, + TemporalModeV1::Forensic => Self::Forensic, + } + } +} + #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum SessionCoverageStateV1 { @@ -163,6 +192,20 @@ pub enum SessionCoverageStateV1 { Unavailable, } +impl From for SessionCoverageStateV1 { + fn from(value: SessionSourceCoverageStateV1) -> Self { + match value { + SessionSourceCoverageStateV1::Fresh => Self::Fresh, + SessionSourceCoverageStateV1::Stale => Self::Stale, + SessionSourceCoverageStateV1::Partial => Self::Partial, + SessionSourceCoverageStateV1::Locked => Self::Locked, + SessionSourceCoverageStateV1::Redacted => Self::Redacted, + SessionSourceCoverageStateV1::RetentionWithheld => Self::RetentionWithheld, + SessionSourceCoverageStateV1::Unavailable => Self::Unavailable, + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct SessionCoverageIntervalV1 { @@ -204,6 +247,31 @@ pub enum SessionCoverageReasonV1 { Unavailable, } +impl From<&SessionSourceCoverageReasonV1> for SessionCoverageReasonV1 { + fn from(value: &SessionSourceCoverageReasonV1) -> Self { + match value { + SessionSourceCoverageReasonV1::CaughtUp => Self::CaughtUp, + SessionSourceCoverageReasonV1::ProjectionBehindSource { lag } => { + Self::ProjectionBehindSource { lag: *lag } + } + SessionSourceCoverageReasonV1::SourceBehindTarget { lag } => { + Self::SourceBehindTarget { lag: *lag } + } + SessionSourceCoverageReasonV1::ProjectionAndSourceBehind { + projection_lag, + source_lag, + } => Self::ProjectionAndSourceBehind { + projection_lag: *projection_lag, + source_lag: *source_lag, + }, + SessionSourceCoverageReasonV1::Locked => Self::Locked, + SessionSourceCoverageReasonV1::Redacted => Self::Redacted, + SessionSourceCoverageReasonV1::RetentionWithheld => Self::RetentionWithheld, + SessionSourceCoverageReasonV1::Unavailable => Self::Unavailable, + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct TemporalMetadataV1 { @@ -595,3 +663,44 @@ pub struct WorkflowsResultV1 { #[serde(default, skip_serializing_if = "Option::is_none")] pub session_id: Option, } + +#[cfg(test)] +mod coverage_projection_tests { + use tracedecay_domain::{ + HydrationStateV1, SessionSourceCoverageReasonV1, SessionSourceCoverageStateV1, + TemporalModeV1, UtcMicros, + }; + + use super::{ + HydrationStateResultV1, SessionCoverageModeV1, SessionCoverageReasonV1, + SessionCoverageStateV1, + }; + + #[test] + fn coverage_projection_keeps_mode_cutoff_and_status_labels() { + assert_eq!( + SessionCoverageModeV1::from(TemporalModeV1::AsOf { + cutoff: UtcMicros(7), + }), + SessionCoverageModeV1::AsOf { cutoff: 7 } + ); + assert_eq!( + SessionCoverageModeV1::from(TemporalModeV1::Forensic), + SessionCoverageModeV1::Forensic + ); + assert_eq!( + SessionCoverageStateV1::from(SessionSourceCoverageStateV1::RetentionWithheld), + SessionCoverageStateV1::RetentionWithheld + ); + assert_eq!( + SessionCoverageReasonV1::from(&SessionSourceCoverageReasonV1::ProjectionBehindSource { + lag: 4 + }), + SessionCoverageReasonV1::ProjectionBehindSource { lag: 4 } + ); + assert_eq!( + HydrationStateResultV1::from(HydrationStateV1::UnverifiableLegacy), + HydrationStateResultV1::UnverifiableLegacy + ); + } +} diff --git a/crates/tracedecay-session-runtime/src/retained/lcm/output.rs b/crates/tracedecay-session-runtime/src/retained/lcm/output.rs index 975bfeb0ee..c3ca4f9b3d 100644 --- a/crates/tracedecay-session-runtime/src/retained/lcm/output.rs +++ b/crates/tracedecay-session-runtime/src/retained/lcm/output.rs @@ -15,9 +15,8 @@ use tracedecay_contracts::retained_surfaces::{ TemporalFreshnessV1, TemporalOmissionV1, TemporalWatermarksV1, ValidCoverageIntervalV1, }; use tracedecay_domain::{ - CompactContextLineageEdgeV1, HydrationStateV1, SessionSourceCoverageIntervalV1, - SessionSourceCoverageReasonV1, SessionSourceCoverageStateV1, SessionSourceCoverageV1, - TemporalModeV1, ValidCoverageIntervalV1 as DomainValidCoverageIntervalV1, + CompactContextLineageEdgeV1, SessionSourceCoverageIntervalV1, SessionSourceCoverageV1, + ValidCoverageIntervalV1 as DomainValidCoverageIntervalV1, }; use tracedecay_lcm::contracts::{ LcmContentRange, LcmDataFreshness, LcmDescribeResponse, LcmExpandResponse, LcmRawMessage, @@ -74,7 +73,7 @@ pub(super) fn temporal_fields(value: SessionTemporalMetadataView) -> LcmTemporal .map(|item| TemporalOmissionV1 { rank: item.rank, anchor: item.anchor.as_str().to_owned(), - reason: hydration(item.reason), + reason: HydrationStateResultV1::from(item.reason), }) .collect(), next_cursor: value.cursor, @@ -88,7 +87,7 @@ fn source_coverage(value: SessionSourceCoverageV1) -> RetainedSourceCoverageV1 { committed_frontier: value.committed_frontier().value(), target_watermark: value.target_watermark().value(), request: SessionCoverageRequestV1 { - mode: coverage_mode(value.request().mode()), + mode: SessionCoverageModeV1::from(value.request().mode()), }, covered_intervals: value .covered_intervals() @@ -102,8 +101,8 @@ fn source_coverage(value: SessionSourceCoverageV1) -> RetainedSourceCoverageV1 { .cloned() .map(coverage_interval) .collect(), - state: coverage_state(value.state()), - reason: coverage_reason(value.reason()), + state: SessionCoverageStateV1::from(value.state()), + reason: SessionCoverageReasonV1::from(value.reason()), } } @@ -126,54 +125,6 @@ fn closed_interval(value: tracedecay_domain::ClosedUtcIntervalV1) -> ClosedUtcIn } } -const fn coverage_mode(value: TemporalModeV1) -> SessionCoverageModeV1 { - match value { - TemporalModeV1::Current => SessionCoverageModeV1::Current, - TemporalModeV1::AsOf { cutoff } => SessionCoverageModeV1::AsOf { cutoff: cutoff.0 }, - TemporalModeV1::Evolution => SessionCoverageModeV1::Evolution, - TemporalModeV1::Forensic => SessionCoverageModeV1::Forensic, - } -} - -const fn coverage_state(value: SessionSourceCoverageStateV1) -> SessionCoverageStateV1 { - match value { - SessionSourceCoverageStateV1::Fresh => SessionCoverageStateV1::Fresh, - SessionSourceCoverageStateV1::Stale => SessionCoverageStateV1::Stale, - SessionSourceCoverageStateV1::Partial => SessionCoverageStateV1::Partial, - SessionSourceCoverageStateV1::Locked => SessionCoverageStateV1::Locked, - SessionSourceCoverageStateV1::Redacted => SessionCoverageStateV1::Redacted, - SessionSourceCoverageStateV1::RetentionWithheld => { - SessionCoverageStateV1::RetentionWithheld - } - SessionSourceCoverageStateV1::Unavailable => SessionCoverageStateV1::Unavailable, - } -} - -fn coverage_reason(value: &SessionSourceCoverageReasonV1) -> SessionCoverageReasonV1 { - match value { - SessionSourceCoverageReasonV1::CaughtUp => SessionCoverageReasonV1::CaughtUp, - SessionSourceCoverageReasonV1::ProjectionBehindSource { lag } => { - SessionCoverageReasonV1::ProjectionBehindSource { lag: *lag } - } - SessionSourceCoverageReasonV1::SourceBehindTarget { lag } => { - SessionCoverageReasonV1::SourceBehindTarget { lag: *lag } - } - SessionSourceCoverageReasonV1::ProjectionAndSourceBehind { - projection_lag, - source_lag, - } => SessionCoverageReasonV1::ProjectionAndSourceBehind { - projection_lag: *projection_lag, - source_lag: *source_lag, - }, - SessionSourceCoverageReasonV1::Locked => SessionCoverageReasonV1::Locked, - SessionSourceCoverageReasonV1::Redacted => SessionCoverageReasonV1::Redacted, - SessionSourceCoverageReasonV1::RetentionWithheld => { - SessionCoverageReasonV1::RetentionWithheld - } - SessionSourceCoverageReasonV1::Unavailable => SessionCoverageReasonV1::Unavailable, - } -} - pub(super) fn sliced_message( result: SessionMessageSearchResult, slice: LcmContentSlice, @@ -362,7 +313,7 @@ pub(super) fn expansion(value: LcmExpandResponse) -> LcmExpansionV1 { .into_iter() .map(|source| LcmExpandedSourceV1 { source_ref: source_ref(source.source_ref), - state: hydration(source.state), + state: HydrationStateResultV1::from(source.state), content: source.content, content_range: source.content_range.map(content_range), content_truncated: source.content_truncated, @@ -454,7 +405,7 @@ pub(super) fn expand_query_result( kind: page.kind, node_id: page.node_id, source_ref: page.source_ref.map(source_ref), - state: page.state.map(hydration), + state: page.state.map(HydrationStateResultV1::from), next_content_offset: page.next_content_offset, has_more: page.has_more, }) @@ -595,19 +546,6 @@ fn rebuild_synthesis_user_prompt(result: &mut LcmExpandQueryResultV1) { synthesis.user = format!("QUESTION:\n{prompt}\n\nEXPANDED CONTEXT:\n{context}"); } -pub(super) const fn hydration(value: HydrationStateV1) -> HydrationStateResultV1 { - match value { - HydrationStateV1::Available => HydrationStateResultV1::Available, - HydrationStateV1::RetainedButUnavailable => HydrationStateResultV1::RetainedButUnavailable, - HydrationStateV1::Redacted => HydrationStateResultV1::Redacted, - HydrationStateV1::Deleted => HydrationStateResultV1::Deleted, - HydrationStateV1::RetentionExpired => HydrationStateResultV1::RetentionExpired, - HydrationStateV1::Unauthorized => HydrationStateResultV1::Unauthorized, - HydrationStateV1::Locked => HydrationStateResultV1::Locked, - HydrationStateV1::UnverifiableLegacy => HydrationStateResultV1::UnverifiableLegacy, - } -} - const fn storage_kind(value: LcmStorageKind) -> LcmStorageKindV1 { match value { LcmStorageKind::Inline => LcmStorageKindV1::Inline, diff --git a/crates/tracedecay-session-runtime/src/retained/lcm/retrieval.rs b/crates/tracedecay-session-runtime/src/retained/lcm/retrieval.rs index f01b170fc6..7af90825ee 100644 --- a/crates/tracedecay-session-runtime/src/retained/lcm/retrieval.rs +++ b/crates/tracedecay-session-runtime/src/retained/lcm/retrieval.rs @@ -5,10 +5,11 @@ use std::collections::BTreeMap; use futures_util::stream::{self, StreamExt}; use tracedecay_contracts::retained_surfaces::{ - LcmDescribeRequestV1, LcmDescribeResultV1, LcmDescribeTargetV1, LcmExpandQueryRequestV1, - LcmExpandRequestV1, LcmExpandResultV1, LcmExpandTargetV1, LcmGrepRequestV1, LcmGrepResultV1, - LcmGrepSortV1, LcmLoadSessionRequestV1, LcmLoadSessionResultV1, LcmNodeIdV1, LcmSearchScopeV1, - RetainedOutcomeStatusV1, RetainedSurfaceOperation, RetainedSurfaceResultV1, + HydrationStateResultV1, LcmDescribeRequestV1, LcmDescribeResultV1, LcmDescribeTargetV1, + LcmExpandQueryRequestV1, LcmExpandRequestV1, LcmExpandResultV1, LcmExpandTargetV1, + LcmGrepRequestV1, LcmGrepResultV1, LcmGrepSortV1, LcmLoadSessionRequestV1, + LcmLoadSessionResultV1, LcmNodeIdV1, LcmSearchScopeV1, RetainedOutcomeStatusV1, + RetainedSurfaceOperation, RetainedSurfaceResultV1, }; use tracedecay_contracts::{ ApplicationOutcome, RetainedSurfaceExecutionContextV1, RetainedSurfaceExecutionErrorV1, @@ -357,7 +358,7 @@ pub(super) async fn execute_describe( provider: Some(provider.to_owned()), session_id: Some(session_id.as_str().to_owned()), grain: Some(grain.as_str().to_owned()), - state: Some(output::hydration(state)), + state: Some(HydrationStateResultV1::from(state)), lineage: Some(output::lineage(lineage)), retrieval: Some(output::retrieval(retrieval)), omitted: Some(retrieval.omitted()), @@ -379,7 +380,7 @@ pub(super) async fn execute_describe( provider: Some(provider.to_owned()), session_id: Some(session_id.as_str().to_owned()), grain: Some(grain.as_str().to_owned()), - state: state.map(output::hydration), + state: state.map(HydrationStateResultV1::from), lineage: Some(output::lineage(lineage)), retrieval: Some(output::retrieval(retrieval)), omitted: Some(retrieval.omitted()), @@ -908,7 +909,7 @@ fn expand_result( provider: Some(provider.to_owned()), session_id: Some(session_id.as_str().to_owned()), grain: grain.map(|value| value.as_str().to_owned()), - state: state.map(output::hydration), + state: state.map(HydrationStateResultV1::from), retrieval: Some(output::retrieval(retrieval)), omitted: Some(retrieval.omitted()), temporal: Some(output::temporal_fields(temporal)), diff --git a/crates/tracedecay-session-runtime/src/retained/session.rs b/crates/tracedecay-session-runtime/src/retained/session.rs index 684a3e52e2..92b6a42794 100644 --- a/crates/tracedecay-session-runtime/src/retained/session.rs +++ b/crates/tracedecay-session-runtime/src/retained/session.rs @@ -20,8 +20,7 @@ use tracedecay_contracts::{ RetainedSurfaceExecutionFutureV1, now_micros, }; use tracedecay_domain::{ - HydrationStateV1, ManifestDigest, ProjectId, RetrievalGrainV1, SessionId, - SessionSourceCoverageIntervalV1, SessionSourceCoverageReasonV1, SessionSourceCoverageStateV1, + ManifestDigest, ProjectId, RetrievalGrainV1, SessionId, SessionSourceCoverageIntervalV1, SessionSourceCoverageV1, TemporalCoverageCountsV1, TemporalModeV1, UserProfileId, ValidCoverageIntervalV1 as DomainValidCoverageIntervalV1, canonical_sha256, }; @@ -1048,7 +1047,7 @@ fn temporal( .map(|omission| TemporalOmissionV1 { rank: omission.rank, anchor: omission.anchor.as_str().to_owned(), - reason: hydration(omission.reason), + reason: HydrationStateResultV1::from(omission.reason), }) .collect(), coverage_omissions: value @@ -1105,7 +1104,7 @@ pub(super) fn source_coverage(value: SessionSourceCoverageV1) -> WireSourceCover committed_frontier: value.committed_frontier().value(), target_watermark: value.target_watermark().value(), request: SessionCoverageRequestV1 { - mode: coverage_mode(value.request().mode()), + mode: SessionCoverageModeV1::from(value.request().mode()), }, covered_intervals: value .covered_intervals() @@ -1119,8 +1118,8 @@ pub(super) fn source_coverage(value: SessionSourceCoverageV1) -> WireSourceCover .cloned() .map(coverage_interval) .collect(), - state: coverage_state(value.state()), - reason: coverage_reason(value.reason()), + state: SessionCoverageStateV1::from(value.state()), + reason: SessionCoverageReasonV1::from(value.reason()), } } @@ -1142,67 +1141,6 @@ fn coverage_interval(value: SessionSourceCoverageIntervalV1) -> SessionCoverageI } } -const fn coverage_mode(value: TemporalModeV1) -> SessionCoverageModeV1 { - match value { - TemporalModeV1::Current => SessionCoverageModeV1::Current, - TemporalModeV1::AsOf { cutoff } => SessionCoverageModeV1::AsOf { cutoff: cutoff.0 }, - TemporalModeV1::Evolution => SessionCoverageModeV1::Evolution, - TemporalModeV1::Forensic => SessionCoverageModeV1::Forensic, - } -} - -const fn coverage_state(value: SessionSourceCoverageStateV1) -> SessionCoverageStateV1 { - match value { - SessionSourceCoverageStateV1::Fresh => SessionCoverageStateV1::Fresh, - SessionSourceCoverageStateV1::Stale => SessionCoverageStateV1::Stale, - SessionSourceCoverageStateV1::Partial => SessionCoverageStateV1::Partial, - SessionSourceCoverageStateV1::Locked => SessionCoverageStateV1::Locked, - SessionSourceCoverageStateV1::Redacted => SessionCoverageStateV1::Redacted, - SessionSourceCoverageStateV1::RetentionWithheld => { - SessionCoverageStateV1::RetentionWithheld - } - SessionSourceCoverageStateV1::Unavailable => SessionCoverageStateV1::Unavailable, - } -} - -fn coverage_reason(value: &SessionSourceCoverageReasonV1) -> SessionCoverageReasonV1 { - match value { - SessionSourceCoverageReasonV1::CaughtUp => SessionCoverageReasonV1::CaughtUp, - SessionSourceCoverageReasonV1::ProjectionBehindSource { lag } => { - SessionCoverageReasonV1::ProjectionBehindSource { lag: *lag } - } - SessionSourceCoverageReasonV1::SourceBehindTarget { lag } => { - SessionCoverageReasonV1::SourceBehindTarget { lag: *lag } - } - SessionSourceCoverageReasonV1::ProjectionAndSourceBehind { - projection_lag, - source_lag, - } => SessionCoverageReasonV1::ProjectionAndSourceBehind { - projection_lag: *projection_lag, - source_lag: *source_lag, - }, - SessionSourceCoverageReasonV1::Locked => SessionCoverageReasonV1::Locked, - SessionSourceCoverageReasonV1::Redacted => SessionCoverageReasonV1::Redacted, - SessionSourceCoverageReasonV1::RetentionWithheld => { - SessionCoverageReasonV1::RetentionWithheld - } - SessionSourceCoverageReasonV1::Unavailable => SessionCoverageReasonV1::Unavailable, - } -} - -const fn hydration(value: HydrationStateV1) -> HydrationStateResultV1 { - match value { - HydrationStateV1::Available => HydrationStateResultV1::Available, - HydrationStateV1::RetainedButUnavailable => HydrationStateResultV1::RetainedButUnavailable, - HydrationStateV1::Redacted => HydrationStateResultV1::Redacted, - HydrationStateV1::Deleted => HydrationStateResultV1::Deleted, - HydrationStateV1::RetentionExpired => HydrationStateResultV1::RetentionExpired, - HydrationStateV1::Unauthorized => HydrationStateResultV1::Unauthorized, - HydrationStateV1::Locked => HydrationStateResultV1::Locked, - HydrationStateV1::UnverifiableLegacy => HydrationStateResultV1::UnverifiableLegacy, - } -} - #[cfg(test)] mod refusal_tests { use tracedecay_contracts::{ diff --git a/crates/tracedecay-session-runtime/src/session_retrieval.rs b/crates/tracedecay-session-runtime/src/session_retrieval.rs index a03bb5aaa9..2de0cd6590 100644 --- a/crates/tracedecay-session-runtime/src/session_retrieval.rs +++ b/crates/tracedecay-session-runtime/src/session_retrieval.rs @@ -105,8 +105,8 @@ pub use contract::{ SessionRetrievalFilters, SessionRetrievalOmissionView, SessionRetrievalPageView, SessionRetrievalServiceOutcome, SessionRetrievalStoreScope, SessionRetrievalUnavailable, SessionRetrievalUnavailableReason, SessionTemporalMetadataView, SessionTemporalWatermarksView, - temporal_message_type, temporal_session_scope, }; +pub(crate) use contract::{temporal_message_type, temporal_session_scope}; pub use primitive::DaemonSessionLookupPrimitiveV1; /// Serving identity of the store the daemon currently serves, extracted From bd7004d5a1a59fef797aeb4b7ef23e814c0a4543 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 20:00:23 +0000 Subject: [PATCH 167/182] simplify(pass-2/5): share no-action invalid-request constructor Co-authored-by: Zack Jackson --- crates/tracedecay-api/src/http.rs | 6 +--- .../src/primitives/runtime.rs | 34 +++++++----------- .../src/authorization/service.rs | 22 ++++++------ .../tracedecay-contracts/src/feedback/read.rs | 14 +++----- .../src/result/problem.rs | 12 +++++++ .../src/result/problem/tests.rs | 20 +++++++++++ .../src/retrieval/callable_code_service.rs | 8 ++--- .../src/application_surface/problems.rs | 36 +++++++------------ .../src/invocation/configuration.rs | 32 +++++++---------- .../src/invocation/feedback.rs | 8 ++--- .../src/invocation/git.rs | 12 +++---- .../src/invocation/observatory.rs | 12 +++---- .../src/invocation/primitive.rs | 27 ++++++-------- .../src/invocation/tests/feedback_tests.rs | 16 ++++----- .../tests/invocation_observability_tests.rs | 8 ++--- .../src/handlers/retained_response.rs | 12 +++---- .../advisory_runtime/model.rs | 10 +++--- 17 files changed, 125 insertions(+), 164 deletions(-) diff --git a/crates/tracedecay-api/src/http.rs b/crates/tracedecay-api/src/http.rs index 140b209956..6dfb4d36f5 100644 --- a/crates/tracedecay-api/src/http.rs +++ b/crates/tracedecay-api/src/http.rs @@ -416,11 +416,7 @@ pub(crate) fn invalid_request_problem( let diagnostic = SafeDiagnostic::new(code, message)?; adapter_problem( request_id, - ApplicationProblem::InvalidRequest { - diagnostic, - retry: RetryDirective::Never, - legal_actions: Vec::new(), - }, + ApplicationProblem::invalid_request_without_action(diagnostic), ) } diff --git a/crates/tracedecay-application/src/primitives/runtime.rs b/crates/tracedecay-application/src/primitives/runtime.rs index 980e82fe1e..43451aa35b 100644 --- a/crates/tracedecay-application/src/primitives/runtime.rs +++ b/crates/tracedecay-application/src/primitives/runtime.rs @@ -1752,14 +1752,12 @@ fn primitive_failure( failure: tracedecay_contracts::retrieval::PrimitiveFailure, ) -> Result, ApplicationContractError> { let application_problem = match failure.kind { - PrimitiveFailureKind::InvalidRequest => ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { + PrimitiveFailureKind::InvalidRequest => { + ApplicationProblem::invalid_request_without_action(SafeDiagnostic { code: failure.code, message: failure.message, - }, - retry: RetryDirective::Never, - legal_actions: Vec::new(), - }, + }) + } PrimitiveFailureKind::NotFoundOrNotAuthorized => { ApplicationProblem::not_found_or_not_authorized(RetryDirective::Never) } @@ -1791,14 +1789,10 @@ fn grep_problem( GrepAnalysisProblemV1::InvalidRequest(message) => problem( context, operation, - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { - code: "application.retrieval.invalid-request".to_owned(), - message, - }, - retry: RetryDirective::Never, - legal_actions: Vec::new(), - }, + ApplicationProblem::invalid_request_without_action(SafeDiagnostic { + code: "application.retrieval.invalid-request".to_owned(), + message, + }), ), GrepAnalysisProblemV1::AuthorityFailed(_) => unavailable(context, operation), } @@ -1824,14 +1818,10 @@ fn invalid_request( problem( context, operation, - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { - code: "application.retrieval.invalid-request".to_owned(), - message: "The primitive request is invalid.".to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: Vec::new(), - }, + ApplicationProblem::invalid_request_without_action(SafeDiagnostic { + code: "application.retrieval.invalid-request".to_owned(), + message: "The primitive request is invalid.".to_owned(), + }), ) } diff --git a/crates/tracedecay-contracts/src/authorization/service.rs b/crates/tracedecay-contracts/src/authorization/service.rs index c18adef10a..1d241e6cf5 100644 --- a/crates/tracedecay-contracts/src/authorization/service.rs +++ b/crates/tracedecay-contracts/src/authorization/service.rs @@ -101,15 +101,13 @@ where let policy = self.policy_reference(&decision)?; AuthorityReceipt::from_context(context, policy, observed_at).map_err(|_| { - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic::new( + ApplicationProblem::invalid_request_without_action( + SafeDiagnostic::new( "application.authorization.invalid-context", "The request context is invalid.", ) .expect("static safe diagnostic is valid"), - retry: RetryDirective::Never, - legal_actions: Vec::new(), - } + ) }) } @@ -207,14 +205,14 @@ where .ok_or_else(|| self.non_disclosure.proof_problem())?; let policy = self.policy_reference(&decision)?; let receipt = AuthorityReceipt::from_context(request.context, policy, request.observed_at) - .map_err(|_| ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic::new( - "application.authorization.invalid-context", - "The request context is invalid.", + .map_err(|_| { + ApplicationProblem::invalid_request_without_action( + SafeDiagnostic::new( + "application.authorization.invalid-context", + "The request context is invalid.", + ) + .expect("static safe diagnostic is valid"), ) - .expect("static safe diagnostic is valid"), - retry: RetryDirective::Never, - legal_actions: Vec::new(), })?; Ok(AuthorizationAdmission { diff --git a/crates/tracedecay-contracts/src/feedback/read.rs b/crates/tracedecay-contracts/src/feedback/read.rs index 0fd6097ea9..37b91b52cf 100644 --- a/crates/tracedecay-contracts/src/feedback/read.rs +++ b/crates/tracedecay-contracts/src/feedback/read.rs @@ -24,7 +24,7 @@ use crate::error::ApplicationContractError; use crate::handlers::ApplicationOperation; use crate::result::{ ApplicationEnvelope, ApplicationProblem, ApplicationProblemEnvelope, ApplicationResult, - AuthorityReceipt, EvidencePacket, LegalAction, OpaqueCursor, OperationReceipt, + AuthorityReceipt, EvidencePacket, OpaqueCursor, OperationReceipt, OperationTermination, PageCursor, RetrievalEvidence, RetryDirective, SafeDiagnostic, }; use crate::retrieval::{ @@ -605,14 +605,10 @@ fn invalid_request( problem_envelope( context, operation, - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic::new( - "application.feedback.invalid-request", - "The feedback read request is invalid.", - )?, - retry: RetryDirective::Never, - legal_actions: Vec::::new(), - }, + ApplicationProblem::invalid_request_without_action(SafeDiagnostic::new( + "application.feedback.invalid-request", + "The feedback read request is invalid.", + )?), ) } diff --git a/crates/tracedecay-contracts/src/result/problem.rs b/crates/tracedecay-contracts/src/result/problem.rs index 5b28813c61..80f627cfc5 100644 --- a/crates/tracedecay-contracts/src/result/problem.rs +++ b/crates/tracedecay-contracts/src/result/problem.rs @@ -697,6 +697,18 @@ impl ApplicationProblem { } } + /// Invalid request that offers no recovery action. Never retries. + /// + /// Empty legal actions are part of the refusal: adapters must not invent + /// `CorrectRequest` for these problems. + pub fn invalid_request_without_action(diagnostic: SafeDiagnostic) -> Self { + Self::InvalidRequest { + diagnostic, + retry: RetryDirective::Never, + legal_actions: Vec::new(), + } + } + pub fn cancelled_before_admission() -> Self { Self::Cancelled { stage: CancellationStage::BeforeAdmission, diff --git a/crates/tracedecay-contracts/src/result/problem/tests.rs b/crates/tracedecay-contracts/src/result/problem/tests.rs index 3a75c79261..842178b22a 100644 --- a/crates/tracedecay-contracts/src/result/problem/tests.rs +++ b/crates/tracedecay-contracts/src/result/problem/tests.rs @@ -26,6 +26,26 @@ fn invalid_request_offers_correction_and_never_retries() { assert_eq!(problem.reason_code(), "application.invalid-request"); } +#[test] +fn invalid_request_without_action_offers_no_recovery() { + let diagnostic = SafeDiagnostic { + code: "application.invalid-request.closed".to_owned(), + message: "The request is invalid.".to_owned(), + }; + let problem = ApplicationProblem::invalid_request_without_action(diagnostic.clone()); + + assert_eq!( + problem, + ApplicationProblem::InvalidRequest { + diagnostic, + retry: RetryDirective::Never, + legal_actions: Vec::new(), + } + ); + assert_eq!(problem.safe_message(), "The request is invalid."); + assert!(problem.legal_actions().is_empty()); +} + #[test] fn reset_required_is_a_distinct_non_retryable_terminal() { let problem = ApplicationProblem::reset_required( diff --git a/crates/tracedecay-contracts/src/retrieval/callable_code_service.rs b/crates/tracedecay-contracts/src/retrieval/callable_code_service.rs index 29061705d7..a3095ae970 100644 --- a/crates/tracedecay-contracts/src/retrieval/callable_code_service.rs +++ b/crates/tracedecay-contracts/src/retrieval/callable_code_service.rs @@ -543,13 +543,11 @@ fn invalid_code_query_outcome_problem() -> ApplicationProblem { } fn invalid_code_query_problem() -> ApplicationProblem { - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic::new( + ApplicationProblem::invalid_request_without_action( + SafeDiagnostic::new( "application.code-query.invalid-request", "The callable code-intelligence request is invalid.", ) .expect("static safe diagnostic is valid"), - retry: RetryDirective::Never, - legal_actions: Vec::new(), - } + ) } diff --git a/crates/tracedecay-daemon-service/src/application_surface/problems.rs b/crates/tracedecay-daemon-service/src/application_surface/problems.rs index 351aacb6ad..32b767892d 100644 --- a/crates/tracedecay-daemon-service/src/application_surface/problems.rs +++ b/crates/tracedecay-daemon-service/src/application_surface/problems.rs @@ -54,14 +54,10 @@ pub(super) fn application_contract_error_response(error: ApplicationContractErro } fn invalid_surface_request_problem(message: String) -> ApplicationProblem { - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { - code: "application.surface.invalid_request".to_owned(), - message, - }, - retry: RetryDirective::Never, - legal_actions: Vec::new(), - } + ApplicationProblem::invalid_request_without_action(SafeDiagnostic { + code: "application.surface.invalid_request".to_owned(), + message, + }) } pub(super) fn http_adapter_problem( @@ -154,14 +150,10 @@ pub(super) fn invocation_problem( Ok(match problem { tracedecay_daemon_protocol::DaemonInvocationProblem::InvalidRequest | tracedecay_daemon_protocol::DaemonInvocationProblem::UnsupportedRevision => { - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic::new( - "application.surface.invalid_request", - "The daemon rejected the application request", - )?, - retry: RetryDirective::Never, - legal_actions: Vec::new(), - } + ApplicationProblem::invalid_request_without_action(SafeDiagnostic::new( + "application.surface.invalid_request", + "The daemon rejected the application request", + )?) } tracedecay_daemon_protocol::DaemonInvocationProblem::NotFoundOrNotAuthorized => { ApplicationProblem::not_found_or_not_authorized(RetryDirective::Never) @@ -201,14 +193,10 @@ pub(super) fn invocation_contract_problem( ApplicationProblem::timed_out_before_admission() } tracedecay_contracts::InvocationError::InvalidRequest => { - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic::new( - "application.surface.invalid_request", - "The daemon rejected the application request", - )?, - retry: RetryDirective::Never, - legal_actions: Vec::new(), - } + ApplicationProblem::invalid_request_without_action(SafeDiagnostic::new( + "application.surface.invalid_request", + "The daemon rejected the application request", + )?) } tracedecay_contracts::InvocationError::Conflict => ApplicationProblem::Conflict { diagnostic: SafeDiagnostic::new( diff --git a/crates/tracedecay-daemon-service/src/invocation/configuration.rs b/crates/tracedecay-daemon-service/src/invocation/configuration.rs index 425d0bd76f..c722abb78b 100644 --- a/crates/tracedecay-daemon-service/src/invocation/configuration.rs +++ b/crates/tracedecay-daemon-service/src/invocation/configuration.rs @@ -762,14 +762,10 @@ fn is_safe_configuration_validation_reason(reason: &str) -> bool { } fn invalid_configuration_request() -> ApplicationProblem { - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { - code: "configuration.invalid_request".to_owned(), - message: "The configuration request is invalid".to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: Vec::new(), - } + ApplicationProblem::invalid_request_without_action(SafeDiagnostic { + code: "configuration.invalid_request".to_owned(), + message: "The configuration request is invalid".to_owned(), + }) } pub(super) fn configuration_problem(error: ConfigurationError) -> ApplicationProblem { @@ -796,22 +792,18 @@ pub(super) fn configuration_problem(error: ConfigurationError) -> ApplicationPro message: "The configuration preview is stale".to_owned(), }) } - ConfigurationError::PolicyWideningForbidden => ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { + ConfigurationError::PolicyWideningForbidden => { + ApplicationProblem::invalid_request_without_action(SafeDiagnostic { code: "configuration.policy_widening_forbidden".to_owned(), message: "Configuration policy widening is forbidden".to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: Vec::new(), - }, - ConfigurationError::Validation(reason) => ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { + }) + } + ConfigurationError::Validation(reason) => { + ApplicationProblem::invalid_request_without_action(SafeDiagnostic { code: "configuration.invalid_request".to_owned(), message: safe_configuration_validation_message(&reason), - }, - retry: RetryDirective::Never, - legal_actions: Vec::new(), - }, + }) + } ConfigurationError::Unavailable => ApplicationProblem::unavailable(SafeDiagnostic { code: "configuration.unavailable".to_owned(), message: "The configuration authority is unavailable".to_owned(), diff --git a/crates/tracedecay-daemon-service/src/invocation/feedback.rs b/crates/tracedecay-daemon-service/src/invocation/feedback.rs index 4abd6ba406..c659c0045f 100644 --- a/crates/tracedecay-daemon-service/src/invocation/feedback.rs +++ b/crates/tracedecay-daemon-service/src/invocation/feedback.rs @@ -204,14 +204,12 @@ where .await } _ => { - return Err(ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { + return Err(ApplicationProblem::invalid_request_without_action( + SafeDiagnostic { code: "feedback.invalid_operation".to_owned(), message: "The feedback read operation is invalid".to_owned(), }, - retry: RetryDirective::Never, - legal_actions: Vec::new(), - }); + )); } } .map_err(feedback_owner_problem)?; diff --git a/crates/tracedecay-daemon-service/src/invocation/git.rs b/crates/tracedecay-daemon-service/src/invocation/git.rs index 52a58d09f7..506f6949c0 100644 --- a/crates/tracedecay-daemon-service/src/invocation/git.rs +++ b/crates/tracedecay-daemon-service/src/invocation/git.rs @@ -1068,14 +1068,10 @@ pub(super) fn stable_digest( } fn invalid_git_request() -> ApplicationProblem { - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { - code: "git_index.invalid_request".to_owned(), - message: "The Git index request is invalid".to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: Vec::new(), - } + ApplicationProblem::invalid_request_without_action(SafeDiagnostic { + code: "git_index.invalid_request".to_owned(), + message: "The Git index request is invalid".to_owned(), + }) } fn map_git_error(error: GitIndexTransactionApplicationError) -> ApplicationProblem { diff --git a/crates/tracedecay-daemon-service/src/invocation/observatory.rs b/crates/tracedecay-daemon-service/src/invocation/observatory.rs index 4598f28020..a62402440b 100644 --- a/crates/tracedecay-daemon-service/src/invocation/observatory.rs +++ b/crates/tracedecay-daemon-service/src/invocation/observatory.rs @@ -188,14 +188,10 @@ fn observatory_evidence( } fn invalid_observatory_request() -> ApplicationProblem { - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { - code: "application.observatory.invalid-request".to_owned(), - message: "The Observatory read request is invalid".to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: Vec::new(), - } + ApplicationProblem::invalid_request_without_action(SafeDiagnostic { + code: "application.observatory.invalid-request".to_owned(), + message: "The Observatory read request is invalid".to_owned(), + }) } fn observatory_unavailable(request_id: String, code: &str) -> DaemonInvocationResponse { diff --git a/crates/tracedecay-daemon-service/src/invocation/primitive.rs b/crates/tracedecay-daemon-service/src/invocation/primitive.rs index c0739d28d6..383cfdf63d 100644 --- a/crates/tracedecay-daemon-service/src/invocation/primitive.rs +++ b/crates/tracedecay-daemon-service/src/invocation/primitive.rs @@ -323,14 +323,10 @@ pub(super) async fn execute_callable_code( fn invalid_callable_code_request(wire_request_id: String) -> DaemonInvocationResponse { application_problem( wire_request_id, - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { - code: "callable_code.invalid_query".to_owned(), - message: "The callable code query is invalid".to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: Vec::new(), - }, + ApplicationProblem::invalid_request_without_action(SafeDiagnostic { + code: "callable_code.invalid_query".to_owned(), + message: "The callable code query is invalid".to_owned(), + }), ) } @@ -360,15 +356,12 @@ pub fn callable_code_request_context( RetryDirective::Never, )); } - let request_id = - RequestId::new(wire_request_id).map_err(|_| ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { - code: "callable_code.invalid_request_id".to_owned(), - message: "The callable code request identifier is invalid".to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: Vec::new(), - })?; + let request_id = RequestId::new(wire_request_id).map_err(|_| { + ApplicationProblem::invalid_request_without_action(SafeDiagnostic { + code: "callable_code.invalid_request_id".to_owned(), + message: "The callable code request identifier is invalid".to_owned(), + }) + })?; // Correlation IDs stay on the RequestContext. The route authority is a // function of the access and the operation, so the same authorized call // resolves the same grant from any surface and across durable retries. diff --git a/crates/tracedecay-daemon-service/src/invocation/tests/feedback_tests.rs b/crates/tracedecay-daemon-service/src/invocation/tests/feedback_tests.rs index cc96b8a536..807d7a56c1 100644 --- a/crates/tracedecay-daemon-service/src/invocation/tests/feedback_tests.rs +++ b/crates/tracedecay-daemon-service/src/invocation/tests/feedback_tests.rs @@ -47,14 +47,12 @@ impl DaemonAdvisoryCycleInvocationPort for MountedAdvisoryCycle { _request: DaemonAdvisoryCycleInvocationRequest, ) -> DaemonAdvisoryCycleInvocationFuture<'_> { Box::pin(async { - Err(ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { + Err(ApplicationProblem::invalid_request_without_action( + SafeDiagnostic { code: "feedback.test-mounted-advisory-owner".to_owned(), message: "The mounted advisory owner received the request".to_owned(), }, - retry: RetryDirective::Never, - legal_actions: Vec::new(), - }) + )) }) } @@ -63,14 +61,12 @@ impl DaemonAdvisoryCycleInvocationPort for MountedAdvisoryCycle { _request: DaemonFeedbackProximityInvocationRequest, ) -> DaemonFeedbackProximityInvocationFuture<'_> { Box::pin(async { - Err(ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { + Err(ApplicationProblem::invalid_request_without_action( + SafeDiagnostic { code: "feedback.test-mounted-proximity-owner".to_owned(), message: "The mounted proximity owner received the request".to_owned(), }, - retry: RetryDirective::Never, - legal_actions: Vec::new(), - }) + )) }) } } diff --git a/crates/tracedecay-daemon-service/src/invocation/tests/invocation_observability_tests.rs b/crates/tracedecay-daemon-service/src/invocation/tests/invocation_observability_tests.rs index a36227925d..fe8303e8e1 100644 --- a/crates/tracedecay-daemon-service/src/invocation/tests/invocation_observability_tests.rs +++ b/crates/tracedecay-daemon-service/src/invocation/tests/invocation_observability_tests.rs @@ -77,15 +77,13 @@ fn scoped_retained_invalid_request_preserves_rejection_classification() { let response = DaemonInvocationResponse::retained_application_problem( "request.retained.observability", scope, - ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic::new( + ApplicationProblem::invalid_request_without_action( + SafeDiagnostic::new( "retained.observability.invalid", "The retained observability fixture request is invalid", ) .expect("diagnostic"), - retry: RetryDirective::Never, - legal_actions: Vec::new(), - }, + ), ); assert_eq!( diff --git a/crates/tracedecay-mcp/src/handlers/retained_response.rs b/crates/tracedecay-mcp/src/handlers/retained_response.rs index e9586c0a4c..26b516be42 100644 --- a/crates/tracedecay-mcp/src/handlers/retained_response.rs +++ b/crates/tracedecay-mcp/src/handlers/retained_response.rs @@ -100,14 +100,10 @@ pub fn validated_retained_response( fn invocation_problem(problem: DaemonInvocationProblem) -> Result { Ok(match problem { DaemonInvocationProblem::InvalidRequest | DaemonInvocationProblem::UnsupportedRevision => { - ApplicationProblem::InvalidRequest { - diagnostic: retained_safe_diagnostic( - "application.surface.invalid_request", - "The daemon rejected the retained application request", - )?, - retry: RetryDirective::Never, - legal_actions: Vec::new(), - } + ApplicationProblem::invalid_request_without_action(retained_safe_diagnostic( + "application.surface.invalid_request", + "The daemon rejected the retained application request", + )?) } DaemonInvocationProblem::NotFoundOrNotAuthorized => { ApplicationProblem::not_found_or_not_authorized(RetryDirective::Never) diff --git a/crates/tracedecay/src/daemon/project_open_owners/advisory_runtime/model.rs b/crates/tracedecay/src/daemon/project_open_owners/advisory_runtime/model.rs index be83d06288..745c448df9 100644 --- a/crates/tracedecay/src/daemon/project_open_owners/advisory_runtime/model.rs +++ b/crates/tracedecay/src/daemon/project_open_owners/advisory_runtime/model.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use std::time::{Duration, Instant}; use tracedecay_application::lsp_runtime::DaemonLspSessionFactory; -use tracedecay_contracts::{ApplicationProblem, Deadline, RetryDirective, SafeDiagnostic}; +use tracedecay_contracts::{ApplicationProblem, Deadline, SafeDiagnostic}; use tracedecay_domain::UtcMicros; use tracedecay_lsp::analyzer::broker::{DiagnosticBroker, MountedLspProvider}; use tracedecay_session_memory::context::MonotonicDeadline; @@ -51,13 +51,11 @@ pub(super) fn advisory_monotonic_deadline_from_remaining( observed_at .checked_add(remaining) .map(MonotonicDeadline::at) - .ok_or_else(|| ApplicationProblem::InvalidRequest { - diagnostic: SafeDiagnostic { + .ok_or_else(|| { + ApplicationProblem::invalid_request_without_action(SafeDiagnostic { code: "feedback.advisory-cycle.deadline".to_owned(), message: "The advisory feedback cycle deadline is outside the supported horizon" .to_owned(), - }, - retry: RetryDirective::Never, - legal_actions: Vec::new(), + }) }) } From 4091e192439e6801aa071e781b2532b84269bdcf Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 20:02:14 +0000 Subject: [PATCH 168/182] simplify(runtime): drop uncalled repair and entry wrappers Co-authored-by: Zack Jackson --- .../src/primitives/runtime.rs | 6 -- crates/tracedecay-code-index/src/chunks.rs | 7 -- .../src/external_source.rs | 8 -- .../src/compile_diagnostics/cache.rs | 21 +---- .../src/retention/orphan_stores.rs | 9 -- .../src/retention/orphan_stores/pages.rs | 9 -- crates/tracedecay-project/Cargo.toml | 2 +- .../src/project/lifecycle/mod.rs | 5 -- .../tracedecay-runtime-core/src/db/access.rs | 8 -- .../src/db/engine/connection.rs | 9 -- .../src/db/migrations/tests.rs | 10 +-- .../src/shard_runtime/registry.rs | 11 --- .../src/exact_sql/command.rs | 67 +------------- .../src/exact_sql/guard.rs | 8 +- .../src/exact_sql/mod.rs | 28 ------ .../src/exact_sql/types.rs | 1 - .../src/runtime/store_access/transcript.rs | 89 +++++-------------- .../src/resolution.rs | 6 +- .../src/resolution/types.rs | 17 ---- 19 files changed, 35 insertions(+), 286 deletions(-) diff --git a/crates/tracedecay-application/src/primitives/runtime.rs b/crates/tracedecay-application/src/primitives/runtime.rs index 55ed5f7351..b78e36c1e3 100644 --- a/crates/tracedecay-application/src/primitives/runtime.rs +++ b/crates/tracedecay-application/src/primitives/runtime.rs @@ -342,12 +342,6 @@ impl PrimitiveProjectRuntime { pub fn database(&self) -> &Database { &self.database } - - /// Releases the project database, dispatch, and all Arc-backed - /// primitive authorities as one teardown unit. - pub fn teardown(self) { - drop(self); - } } impl PrimitiveDispatch for OwnedPrimitiveRuntime { diff --git a/crates/tracedecay-code-index/src/chunks.rs b/crates/tracedecay-code-index/src/chunks.rs index 15fd1f44b0..200d8572c7 100644 --- a/crates/tracedecay-code-index/src/chunks.rs +++ b/crates/tracedecay-code-index/src/chunks.rs @@ -585,13 +585,6 @@ impl DeterministicCodeChunker { } } - /// Pin the sensitivity level recorded on every chunk of this generation. - #[must_use] - pub fn with_sensitivity_level(mut self, level: SensitivityLevelV1) -> Self { - self.sensitivity_level = level; - self - } - /// The generation this chunker is bound to. pub fn generation_id(&self) -> &CodeGenerationId { &self.generation_id diff --git a/crates/tracedecay-contracts/src/external_source.rs b/crates/tracedecay-contracts/src/external_source.rs index 3565860310..874600b9a9 100644 --- a/crates/tracedecay-contracts/src/external_source.rs +++ b/crates/tracedecay-contracts/src/external_source.rs @@ -274,14 +274,6 @@ impl SourceCanonicalRefetchAuthorityV1 { self.binding == *refresh.binding() && self.original_refresh_digest == *refresh.receipt_digest() } - - /// Reports whether this opaque capability names the exact refresh. - /// - /// The capability still exposes no binding fields or constructor, so a - /// provider or transport cannot mint or retarget it. - pub fn authorizes(&self, refresh: &SourceRefreshReceiptV1) -> bool { - self.matches(refresh) - } } #[derive(Clone, Debug)] diff --git a/crates/tracedecay-lsp/src/compile_diagnostics/cache.rs b/crates/tracedecay-lsp/src/compile_diagnostics/cache.rs index 4963339652..ec53e0b99d 100644 --- a/crates/tracedecay-lsp/src/compile_diagnostics/cache.rs +++ b/crates/tracedecay-lsp/src/compile_diagnostics/cache.rs @@ -31,6 +31,7 @@ struct CachedDiagnostics { #[derive(Debug, Clone, PartialEq, Eq)] enum DiagnosticsCacheRevision { + #[cfg(test)] WorkspaceChange(u64), Recovery(DiagnosticsFingerprint), } @@ -100,25 +101,6 @@ impl DiagnosticsCache { .await } - /// Run diagnostics under the code index's worktree-change authority. - /// - /// A generation is exactly as fresh as the index used by search: hook - /// hints and Git metadata changes are observed immediately, while other - /// out-of-band edits are observed by the 30-second stat-signature ladder. - /// Until that ladder runs, diagnostics intentionally reuse the preceding - /// generation rather than deriving a second workspace-change authority. - pub async fn run_for_generation( - &self, - project_root: &Path, - scope: &Scope, - generation: u64, - ) -> Result> { - self.run_with_generation(project_root, scope, generation, || { - run_all(project_root, scope) - }) - .await - } - #[hotpath::measure(label = "compile_diagnostics.cache.run", future = true)] pub(crate) async fn run_with( &self, @@ -157,6 +139,7 @@ impl DiagnosticsCache { .await } + #[cfg(test)] pub(crate) async fn run_with_generation( &self, project_root: &Path, diff --git a/crates/tracedecay-maintenance/src/retention/orphan_stores.rs b/crates/tracedecay-maintenance/src/retention/orphan_stores.rs index 817a26cdb2..e374c02427 100644 --- a/crates/tracedecay-maintenance/src/retention/orphan_stores.rs +++ b/crates/tracedecay-maintenance/src/retention/orphan_stores.rs @@ -240,15 +240,6 @@ pub struct CollectionPlan { pub unverifiable: Vec, } -impl CollectionPlan { - /// Total bytes that collecting [`Self::collect`] would reclaim. - pub fn collectable_bytes(&self) -> u64 { - self.collect - .iter() - .fold(0u64, |acc, f| acc.saturating_add(f.size_bytes)) - } -} - /// Partition findings under a retention window. Live stores are dropped from /// the plan entirely, they are never a retention concern. Pure. pub fn plan_collection(findings: Vec, retention_secs: i64) -> CollectionPlan { diff --git a/crates/tracedecay-maintenance/src/retention/orphan_stores/pages.rs b/crates/tracedecay-maintenance/src/retention/orphan_stores/pages.rs index e6b2b1fa04..328395e94b 100644 --- a/crates/tracedecay-maintenance/src/retention/orphan_stores/pages.rs +++ b/crates/tracedecay-maintenance/src/retention/orphan_stores/pages.rs @@ -586,15 +586,6 @@ pub struct UnregisteredCollectionPlan { pub retained_immature: Vec, } -impl UnregisteredCollectionPlan { - /// Total bytes that collecting [`Self::collect`] would reclaim. - pub fn collectable_bytes(&self) -> u64 { - self.collect - .iter() - .fold(0u64, |acc, f| acc.saturating_add(f.size_bytes)) - } -} - /// Partition findings under a retention window. Pure. pub fn plan_unregistered_collection( findings: Vec, diff --git a/crates/tracedecay-project/Cargo.toml b/crates/tracedecay-project/Cargo.toml index 6fccb31a3c..93915b47fb 100644 --- a/crates/tracedecay-project/Cargo.toml +++ b/crates/tracedecay-project/Cargo.toml @@ -40,7 +40,7 @@ test-helpers = [ ] # Mirrors the composition root's `test-transport`: the standalone -# `TraceDecay::init` / `open` / `open_read_only` entry points route through +# `TraceDecay::init` / `open` / `open_read_only_with_options` entry points route through # the registered test runtime instead of the exclusive maintenance lease. test-transport = [ "test-helpers", diff --git a/crates/tracedecay-project/src/project/lifecycle/mod.rs b/crates/tracedecay-project/src/project/lifecycle/mod.rs index 5812e816f5..b89dae280b 100644 --- a/crates/tracedecay-project/src/project/lifecycle/mod.rs +++ b/crates/tracedecay-project/src/project/lifecycle/mod.rs @@ -713,11 +713,6 @@ impl TraceDecay { /// sentinels, clear markers, or rewrite corrupted DBs. It is intended for /// status/verification commands that must be able to inspect read-only /// stores without mutating them. - #[hotpath::skip] - pub async fn open_read_only(project_root: &Path) -> Result { - Self::open_read_only_with_options(project_root, TraceDecayOpenOptions::default()).await - } - #[hotpath::skip] pub async fn open_read_only_with_options( project_root: &Path, diff --git a/crates/tracedecay-runtime-core/src/db/access.rs b/crates/tracedecay-runtime-core/src/db/access.rs index 88f8614a94..56c8e826aa 100644 --- a/crates/tracedecay-runtime-core/src/db/access.rs +++ b/crates/tracedecay-runtime-core/src/db/access.rs @@ -582,14 +582,6 @@ impl ExactSqlWriteAuthority for DatabaseAuthority { ExactSqlWriteIntent::ExecuteBatch => { "execute registered global database statement batch" } - ExactSqlWriteIntent::Vacuum => { - if self.role() != DatabaseAuthorityRole::Maintenance { - return Err(ExactSqlError::AuthorityDenied( - "whole-database vacuum requires exclusive maintenance authority".to_owned(), - )); - } - "vacuum registered global database under exclusive maintenance" - } ExactSqlWriteIntent::BeginTransaction => "begin registered global database transaction", ExactSqlWriteIntent::Commit => "commit registered global database transaction", }; diff --git a/crates/tracedecay-runtime-core/src/db/engine/connection.rs b/crates/tracedecay-runtime-core/src/db/engine/connection.rs index c4ee65e924..8114d9f355 100644 --- a/crates/tracedecay-runtime-core/src/db/engine/connection.rs +++ b/crates/tracedecay-runtime-core/src/db/engine/connection.rs @@ -212,15 +212,6 @@ impl Connection { .map_err(Into::into) } - #[hotpath::skip] - pub async fn repair_incremental_auto_vacuum(&self) -> Result<()> { - let runtime = Arc::clone(&self.runtime); - runtime - .repair_incremental_auto_vacuum_async() - .await - .map_err(Into::into) - } - #[cfg(any(test, feature = "test-helpers"))] #[hotpath::skip] pub async fn prepare(&self, sql: &str) -> Result> { diff --git a/crates/tracedecay-runtime-core/src/db/migrations/tests.rs b/crates/tracedecay-runtime-core/src/db/migrations/tests.rs index 2e4bc6570b..4f24404cbf 100644 --- a/crates/tracedecay-runtime-core/src/db/migrations/tests.rs +++ b/crates/tracedecay-runtime-core/src/db/migrations/tests.rs @@ -23,14 +23,8 @@ mod fts; struct AllowSchemaWrites; impl ExactSqlWriteAuthority for AllowSchemaWrites { - fn verify(&self, intent: ExactSqlWriteIntent) -> Result<(), ExactSqlError> { - if intent == ExactSqlWriteIntent::Vacuum { - Err(ExactSqlError::AuthorityDenied( - "ordinary schema fixture cannot vacuum".to_owned(), - )) - } else { - Ok(()) - } + fn verify(&self, _intent: ExactSqlWriteIntent) -> Result<(), ExactSqlError> { + Ok(()) } } diff --git a/crates/tracedecay-runtime-core/src/shard_runtime/registry.rs b/crates/tracedecay-runtime-core/src/shard_runtime/registry.rs index 0bdfd1abc3..b218d83607 100644 --- a/crates/tracedecay-runtime-core/src/shard_runtime/registry.rs +++ b/crates/tracedecay-runtime-core/src/shard_runtime/registry.rs @@ -823,17 +823,6 @@ impl tracedecay_rusqlite_runtime::exact_sql::ExactSqlWriteAuthority tracedecay_rusqlite_runtime::exact_sql::ExactSqlWriteIntent::ExecuteBatch => { "execute registered exact SQL statement batch" } - tracedecay_rusqlite_runtime::exact_sql::ExactSqlWriteIntent::Vacuum => { - if self.authority.role() != crate::db::DatabaseAuthorityRole::Maintenance { - return Err( - tracedecay_rusqlite_runtime::exact_sql::ExactSqlError::AuthorityDenied( - "whole-database vacuum requires exclusive maintenance authority" - .to_owned(), - ), - ); - } - "vacuum registered database under exclusive maintenance" - } tracedecay_rusqlite_runtime::exact_sql::ExactSqlWriteIntent::BeginTransaction => { "begin registered exact SQL transaction" } diff --git a/crates/tracedecay-rusqlite-runtime/src/exact_sql/command.rs b/crates/tracedecay-rusqlite-runtime/src/exact_sql/command.rs index b1b09dde95..3d3c9b9020 100644 --- a/crates/tracedecay-rusqlite-runtime/src/exact_sql/command.rs +++ b/crates/tracedecay-rusqlite-runtime/src/exact_sql/command.rs @@ -15,12 +15,12 @@ use std::{ use rusqlite::{Connection, DropBehavior, ErrorCode, Transaction, TransactionBehavior}; -use super::guard::{AuthorizedDatabaseOperation, with_exact_sql_guard}; +use super::guard::with_exact_sql_guard; use super::{ EXACT_SQL_TRANSACTION_IDLE_LIMIT, EXACT_SQL_TRANSACTION_LIMIT, ExactSqlAttachment, ExactSqlCommitReceipt, ExactSqlError, ExactSqlRollbackReceipt, ExactSqlRows, ExactSqlStatement, ExactSqlWriteAuthority, ExactSqlWriteIntent, ExecutionPolicy, MAX_EXACT_SQL_ATTACHMENTS, - SqlRequest, SqlResult, TransactionPolicy, attach_database, detach_database, execute_batch, + SqlRequest, SqlResult, TransactionPolicy, attach_database, detach_database, execute_query_unchecked, execute_request, publish_last_insert_rowid, sqlite_error, verify_write_authority, }; @@ -46,10 +46,6 @@ pub(crate) enum WriterCommand { reply: async_channel::Sender>, authority: Option>, }, - Vacuum { - reply: async_channel::Sender>, - authority: Option>, - }, } /// Pause between busy-begin attempts so the acquire deadline is the real bound. @@ -362,62 +358,6 @@ pub(crate) fn run_writer_command( }); let _ = reply.try_send(result); } - WriterCommand::Vacuum { reply, authority } => { - let Some(authority) = authority else { - let _ = reply.try_send(Err(ExactSqlError::AuthorityDenied( - "exclusive-maintenance vacuum requires attached write authority".to_owned(), - ))); - return; - }; - if let Err(error) = - verify_write_authority(Some(authority.as_ref()), ExactSqlWriteIntent::Vacuum) - { - let _ = reply.try_send(Err(error)); - return; - } - let previous_attachment_limit = - match connection.set_limit(Limit::SQLITE_LIMIT_ATTACHED, 1) { - Ok(previous) => previous, - Err(error) => { - let _ = reply.try_send(Err(sqlite_error( - "open exclusive-maintenance vacuum attachment slot", - error, - ))); - return; - } - }; - let mut result = hotpath::measure_block!("rusqlite.exact_sql.vacuum", { - with_exact_sql_guard( - connection, - false, - true, - Some(Arc::clone(shutdown_requested)), - None, - true, - Some((Arc::clone(&authority), ExactSqlWriteIntent::Vacuum)), - crate::connection::authorize_writer, - true, - Some(AuthorizedDatabaseOperation::Vacuum), - None, - || { - execute_batch(connection, "PRAGMA auto_vacuum = INCREMENTAL; VACUUM") - .map(|_| ()) - }, - ) - }); - if let Err(error) = - connection.set_limit(Limit::SQLITE_LIMIT_ATTACHED, previous_attachment_limit) - { - shutdown_requested.store(true, Ordering::Release); - if result.is_ok() { - result = Err(sqlite_error( - "restore exclusive-maintenance vacuum attachment limit", - error, - )); - } - } - let _ = reply.try_send(result); - } } } @@ -432,9 +372,6 @@ pub(crate) fn reject_writer_command(command: WriterCommand) { WriterCommand::CheckpointWalTruncate { reply, .. } => { let _ = reply.try_send(Err(ExactSqlError::WriterUnavailable)); } - WriterCommand::Vacuum { reply, .. } => { - let _ = reply.try_send(Err(ExactSqlError::WriterUnavailable)); - } } } diff --git a/crates/tracedecay-rusqlite-runtime/src/exact_sql/guard.rs b/crates/tracedecay-rusqlite-runtime/src/exact_sql/guard.rs index 925ff8ed7e..0bfcc54e96 100644 --- a/crates/tracedecay-rusqlite-runtime/src/exact_sql/guard.rs +++ b/crates/tracedecay-rusqlite-runtime/src/exact_sql/guard.rs @@ -34,7 +34,6 @@ pub(super) struct InsertTracker { pub(super) enum AuthorizedDatabaseOperation { Attach, Detach(String), - Vacuum, } #[allow(clippy::too_many_arguments)] @@ -202,7 +201,7 @@ fn authorize_exact_sql_writer( AuthAction::Attach { .. } if matches!( database_operation, - Some(AuthorizedDatabaseOperation::Attach | AuthorizedDatabaseOperation::Vacuum) + Some(AuthorizedDatabaseOperation::Attach) ) => { return Authorization::Allow; @@ -216,7 +215,7 @@ fn authorize_exact_sql_writer( } if code == rusqlite::ffi::SQLITE_ATTACH && matches!( database_operation, - Some(AuthorizedDatabaseOperation::Attach | AuthorizedDatabaseOperation::Vacuum) + Some(AuthorizedDatabaseOperation::Attach) ) => { return Authorization::Allow; @@ -226,9 +225,6 @@ fn authorize_exact_sql_writer( database_operation, Some(AuthorizedDatabaseOperation::Detach(expected)) if database_name.eq_ignore_ascii_case(expected) - ) || matches!( - database_operation, - Some(AuthorizedDatabaseOperation::Vacuum) ) => { return Authorization::Allow; diff --git a/crates/tracedecay-rusqlite-runtime/src/exact_sql/mod.rs b/crates/tracedecay-rusqlite-runtime/src/exact_sql/mod.rs index 8f8cd4ab5c..5c05e3522b 100644 --- a/crates/tracedecay-rusqlite-runtime/src/exact_sql/mod.rs +++ b/crates/tracedecay-rusqlite-runtime/src/exact_sql/mod.rs @@ -486,34 +486,6 @@ impl ExactSqlHandle { Ok(merge_memory_release(readers, writer)) } - /// Enables incremental auto-vacuum through its fixed maintenance rebuild. - fn enqueue_repair_incremental_auto_vacuum( - &self, - ) -> Result>, ExactSqlError> { - let (reply, response) = async_channel::bounded(1); - self.writer - .as_ref() - .ok_or(ExactSqlError::WriterUnavailable)? - .try_send(WriterCommand::Vacuum { - reply, - authority: self.write_authority.clone(), - }) - .map_err(map_writer_send_error)?; - Ok(response) - } - - pub fn repair_incremental_auto_vacuum(&self) -> Result<(), ExactSqlError> { - recv_writer_reply(self.enqueue_repair_incremental_auto_vacuum()?) - .map_err(|_| ExactSqlError::WriterUnavailable)? - } - - pub async fn repair_incremental_auto_vacuum_async(&self) -> Result<(), ExactSqlError> { - self.enqueue_repair_incremental_auto_vacuum()? - .recv() - .await - .map_err(|_| ExactSqlError::WriterUnavailable)? - } - /// Interactive read snapshot. Admits against the whole general lane. pub fn begin_read_snapshot( &self, diff --git a/crates/tracedecay-rusqlite-runtime/src/exact_sql/types.rs b/crates/tracedecay-rusqlite-runtime/src/exact_sql/types.rs index eb74bee2a6..0e571abed0 100644 --- a/crates/tracedecay-rusqlite-runtime/src/exact_sql/types.rs +++ b/crates/tracedecay-rusqlite-runtime/src/exact_sql/types.rs @@ -210,7 +210,6 @@ pub enum ExactSqlWriteIntent { Execute, Query, ExecuteBatch, - Vacuum, BeginTransaction, Commit, } diff --git a/crates/tracedecay-sessions/src/runtime/store_access/transcript.rs b/crates/tracedecay-sessions/src/runtime/store_access/transcript.rs index 31372f64f5..b4c1c7c9fa 100644 --- a/crates/tracedecay-sessions/src/runtime/store_access/transcript.rs +++ b/crates/tracedecay-sessions/src/runtime/store_access/transcript.rs @@ -3,7 +3,6 @@ use tracedecay_store::{ParseOffset, SessionMessageRecord, SessionRecord, StoreSh use tracedecay_lcm::payload::PayloadFileRollback; use tracedecay_lcm::raw; -use tracedecay_lcm::retrieval_content::derived_text_for_index; use super::super::git_correlation::{ CommitSessionRecord, SpanObservation, enqueue_git_evidence_publication, @@ -13,12 +12,6 @@ use super::super::shared::{durable_project_path_key, path_identity_key}; use super::codex_goal_reconciliation::find_preceding_codex_goal_response; use super::types::{TranscriptBatch, TranscriptPersistenceError}; -#[derive(Debug, Clone, Copy)] -enum TranscriptWritePolicy { - Full { expected_offset: ParseOffset }, - ProjectionOnly, -} - /// Exact Git evidence staged atomically with one transcript write. #[derive(Debug, Clone, Copy)] pub struct TranscriptGitEvidence<'a> { @@ -533,7 +526,7 @@ impl SessionStoreAccess<'_, D> { std::slice::from_ref(&batch), parse_offset_path, parse_offset, - TranscriptWritePolicy::Full { expected_offset }, + expected_offset, None, ) .await @@ -573,7 +566,7 @@ impl SessionStoreAccess<'_, D> { std::slice::from_ref(&batch), parse_offset_path, parse_offset, - TranscriptWritePolicy::Full { expected_offset }, + expected_offset, Some(git_evidence), ) .await @@ -601,7 +594,7 @@ impl SessionStoreAccess<'_, D> { batches: &[TranscriptBatch], parse_offset_path: &str, parse_offset: ParseOffset, - policy: TranscriptWritePolicy, + expected_offset: ParseOffset, git_evidence: Option>, ) -> Result<(), TranscriptPersistenceError> { let storage_root = self @@ -609,25 +602,19 @@ impl SessionStoreAccess<'_, D> { .parent() .unwrap_or_else(|| std::path::Path::new(".")); let mut payload_rollback = PayloadFileRollback::begin_cancellation_safe(storage_root); - let staged_messages = match policy { - TranscriptWritePolicy::Full { .. } => { - stage_full_transcript_messages(storage_root, batches, &mut payload_rollback)? - } - TranscriptWritePolicy::ProjectionOnly => Vec::new(), - }; + let staged_messages = + stage_full_transcript_messages(storage_root, batches, &mut payload_rollback)?; let mut staged_messages = staged_messages.into_iter(); let transaction = self.begin_transcript_transaction().await?; let write_result: Result<(), TranscriptPersistenceError> = async { let mut projection_statements = Vec::with_capacity(TRANSCRIPT_STATEMENT_WINDOW); - if let TranscriptWritePolicy::Full { expected_offset } = policy { - // Full batches are one-winner compare-and-swap on the durable - // parse cursor. `actual == next_offset` is not a retry grant: - // a competing writer can share that destination while carrying - // different parse products. Post-commit publication retries - // must not re-enter this CAS with a stale expected cursor. - require_expected_offset(&transaction, parse_offset_path, expected_offset).await?; - } + // Full batches are one-winner compare-and-swap on the durable + // parse cursor. `actual == next_offset` is not a retry grant: + // a competing writer can share that destination while carrying + // different parse products. Post-commit publication retries + // must not re-enter this CAS with a stale expected cursor. + require_expected_offset(&transaction, parse_offset_path, expected_offset).await?; for batch in batches { if !Self::upsert_session_in_existing_tx(&transaction, &batch.session).await { return Err(TranscriptPersistenceError::message( @@ -650,32 +637,16 @@ impl SessionStoreAccess<'_, D> { .await?; reconcile_codex_goal_response(&transaction, message).await?; } - match policy { - TranscriptWritePolicy::Full { .. } => { - let staged = staged_messages.next().ok_or_else(|| { - TranscriptPersistenceError::message( - "upsert LCM raw message", - "staged transcript message count did not match the write batch", - ) - })?; - projection_statements.push( - self.upsert_session_message_in_existing_tx( - &transaction, - message, - staged, - ) - .await?, - ); - } - TranscriptWritePolicy::ProjectionOnly => { - let text = derived_text_for_index(&message.text); - projection_statements.push(Self::session_message_projection_statement( - message, - &text, - message.metadata_json.as_deref(), - )?); - } - } + let staged = staged_messages.next().ok_or_else(|| { + TranscriptPersistenceError::message( + "upsert LCM raw message", + "staged transcript message count did not match the write batch", + ) + })?; + projection_statements.push( + self.upsert_session_message_in_existing_tx(&transaction, message, staged) + .await?, + ); if projection_statements.len() >= TRANSCRIPT_STATEMENT_WINDOW { flush_transcript_statement_window(&transaction, &mut projection_statements) .await?; @@ -683,9 +654,7 @@ impl SessionStoreAccess<'_, D> { } } flush_transcript_statement_window(&transaction, &mut projection_statements).await?; - if matches!(policy, TranscriptWritePolicy::Full { .. }) - && staged_messages.next().is_some() - { + if staged_messages.next().is_some() { return Err(TranscriptPersistenceError::message( "upsert LCM raw message", "staged transcript message count exceeded the write batch", @@ -703,19 +672,7 @@ impl SessionStoreAccess<'_, D> { TranscriptPersistenceError::storage("stage transcript git evidence", error) })?; } - if matches!(policy, TranscriptWritePolicy::Full { .. }) { - set_parse_offset(&transaction, parse_offset_path, parse_offset).await?; - } else { - Self::set_parse_offset_monotonic_in_existing_tx( - &transaction, - parse_offset_path, - parse_offset, - ) - .await - .map_err(|message| { - TranscriptPersistenceError::message("advance projection parse offset", message) - })?; - } + set_parse_offset(&transaction, parse_offset_path, parse_offset).await?; Ok(()) } .await; diff --git a/crates/tracedecay-temporal-query/src/resolution.rs b/crates/tracedecay-temporal-query/src/resolution.rs index a35d685c4e..87b4c18ec1 100644 --- a/crates/tracedecay-temporal-query/src/resolution.rs +++ b/crates/tracedecay-temporal-query/src/resolution.rs @@ -12,7 +12,7 @@ pub use self::summary::{ evaluate_summary_lineage_eligibility, evaluate_summary_lineage_eligibility_controlled, }; pub use self::types::{ - ResolutionAssertion, ResolutionCertainty, ResolutionCheckpoint, ResolutionEvidence, - ResolutionInputError, ResolutionLineageEdge, ResolutionLineageEdgeKind, ResolutionOccurrence, - ResolvedOccurrence, TemporalResolution, ValidatedAuthorization, + ResolutionAssertion, ResolutionCheckpoint, ResolutionEvidence, ResolutionInputError, + ResolutionLineageEdge, ResolutionLineageEdgeKind, ResolutionOccurrence, ResolvedOccurrence, + TemporalResolution, ValidatedAuthorization, }; diff --git a/crates/tracedecay-temporal-query/src/resolution/types.rs b/crates/tracedecay-temporal-query/src/resolution/types.rs index 46de986132..3135cdad76 100644 --- a/crates/tracedecay-temporal-query/src/resolution/types.rs +++ b/crates/tracedecay-temporal-query/src/resolution/types.rs @@ -100,23 +100,6 @@ pub struct ResolvedOccurrence { pub supporting_anchor_ids: BTreeSet, } -impl ResolvedOccurrence { - #[hotpath::skip] - pub const fn certainty(&self) -> ResolutionCertainty { - if self.uncertain { - ResolutionCertainty::AuthorizedUnknown - } else { - ResolutionCertainty::Known - } - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum ResolutionCertainty { - Known, - AuthorizedUnknown, -} - #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum ResolutionLineageEdgeKind { Correction, From 3de32de4b9b59411a31b4f9746b0e53317a12f41 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 20:04:31 +0000 Subject: [PATCH 169/182] simplify(pass-3/5): share revalidate conflict constructor Co-authored-by: Zack Jackson --- .../src/operation_stream.rs | 22 +-- .../src/remote/protocol.rs | 10 +- .../src/result/problem.rs | 9 + .../src/result/problem/tests.rs | 23 +++ .../src/retained_surfaces/service.rs | 12 +- .../tracedecay-contracts/src/work_attempt.rs | 54 +++--- .../src/work_attempt/problem.rs | 43 ++--- .../src/work_attempt/product_admission.rs | 80 +++++--- .../product_synthesis_admission.rs | 18 +- .../src/work_attempt_effect.rs | 16 +- .../src/work_duplicate_adjudication.rs | 21 +-- .../src/work_leak_adjudication.rs | 48 +++-- .../src/work_placement.rs | 66 +++---- crates/tracedecay-contracts/src/work_retry.rs | 177 +++++++++--------- .../src/work_run_control.rs | 73 ++++---- .../application_surface/operation_events.rs | 24 +-- .../src/application_surface/problems.rs | 10 +- .../src/invocation/configuration.rs | 12 +- .../src/invocation/source_edit.rs | 11 +- .../src/invocation/work/attempt_operations.rs | 12 +- crates/tracedecay-mcp/src/handlers/edit.rs | 11 +- 21 files changed, 360 insertions(+), 392 deletions(-) diff --git a/crates/tracedecay-application/src/operation_stream.rs b/crates/tracedecay-application/src/operation_stream.rs index 9567f89773..f31d66829a 100644 --- a/crates/tracedecay-application/src/operation_stream.rs +++ b/crates/tracedecay-application/src/operation_stream.rs @@ -273,14 +273,10 @@ impl OperationEventError { retry: RetryDirective::AfterRevalidate, legal_actions: vec![LegalAction::Refresh], }, - Self::InvalidFrontier => ApplicationProblem::Conflict { - diagnostic: SafeDiagnostic::new( - "operation_event.invalid_frontier", - "The requested operation-event frontier is invalid", - )?, - retry: RetryDirective::AfterRevalidate, - legal_actions: vec![LegalAction::Refresh], - }, + Self::InvalidFrontier => ApplicationProblem::conflict(SafeDiagnostic::new( + "operation_event.invalid_frontier", + "The requested operation-event frontier is invalid", + )?), Self::RequestNotAdmitted => ApplicationProblem::timed_out_before_admission(), Self::Saturated => ApplicationProblem::Saturated { diagnostic: SafeDiagnostic::new( @@ -304,14 +300,12 @@ impl OperationEventError { // Idempotency facts: the identity or terminal receipt is already // published, so the client re-reads current state instead of // retrying the same publish. - Self::AlreadyBound | Self::TerminalAlreadyPublished => ApplicationProblem::Conflict { - diagnostic: SafeDiagnostic::new( + Self::AlreadyBound | Self::TerminalAlreadyPublished => { + ApplicationProblem::conflict(SafeDiagnostic::new( "operation_event.already_published", "The operation-event identity is already published", - )?, - retry: RetryDirective::AfterRevalidate, - legal_actions: vec![LegalAction::Refresh], - }, + )?) + } // A misconfigured authority is a deterministic, process-lifetime // failure. It is not the caller's request that is wrong and no // amount of retrying will change the outcome. diff --git a/crates/tracedecay-contracts/src/remote/protocol.rs b/crates/tracedecay-contracts/src/remote/protocol.rs index 9588335a5a..b5cef4c62d 100644 --- a/crates/tracedecay-contracts/src/remote/protocol.rs +++ b/crates/tracedecay-contracts/src/remote/protocol.rs @@ -555,14 +555,12 @@ pub fn remote_protocol_problem( retry: RetryDirective::AfterRevalidate, legal_actions: vec![LegalAction::Refresh], }, - RemoteProtocolFailureV1::AuthorityReachable => ApplicationProblem::Conflict { - diagnostic: safe_diagnostic( + RemoteProtocolFailureV1::AuthorityReachable => { + ApplicationProblem::conflict(safe_diagnostic( "remote.authority_reachable", "Offline capture is rejected while the owning authority is reachable", - )?, - retry: RetryDirective::AfterRevalidate, - legal_actions: vec![LegalAction::Refresh], - }, + )?) + } RemoteProtocolFailureV1::SpoolSaturated => ApplicationProblem::Saturated { diagnostic: safe_diagnostic( "remote.spool_saturated", diff --git a/crates/tracedecay-contracts/src/result/problem.rs b/crates/tracedecay-contracts/src/result/problem.rs index 80f627cfc5..0b80375eff 100644 --- a/crates/tracedecay-contracts/src/result/problem.rs +++ b/crates/tracedecay-contracts/src/result/problem.rs @@ -830,6 +830,15 @@ impl ApplicationProblem { } } + /// Conflict the caller resolves by refreshing. Retries only after revalidate. + pub fn conflict(diagnostic: SafeDiagnostic) -> Self { + Self::Conflict { + diagnostic, + retry: RetryDirective::AfterRevalidate, + legal_actions: vec![LegalAction::Refresh], + } + } + pub fn reset_required(diagnostic: SafeDiagnostic) -> Self { Self::ResetRequired { diagnostic, diff --git a/crates/tracedecay-contracts/src/result/problem/tests.rs b/crates/tracedecay-contracts/src/result/problem/tests.rs index 842178b22a..469847f59b 100644 --- a/crates/tracedecay-contracts/src/result/problem/tests.rs +++ b/crates/tracedecay-contracts/src/result/problem/tests.rs @@ -46,6 +46,29 @@ fn invalid_request_without_action_offers_no_recovery() { assert!(problem.legal_actions().is_empty()); } +#[test] +fn conflict_retries_only_after_revalidate_and_refresh() { + let diagnostic = SafeDiagnostic { + code: "application.conflict".to_owned(), + message: "The request conflicts with current state.".to_owned(), + }; + let problem = ApplicationProblem::conflict(diagnostic.clone()); + + assert_eq!( + problem, + ApplicationProblem::Conflict { + diagnostic, + retry: RetryDirective::AfterRevalidate, + legal_actions: vec![LegalAction::Refresh], + } + ); + assert_eq!( + problem.safe_message(), + "The request conflicts with current state." + ); + assert_eq!(problem.reason_code(), "application.conflict"); +} + #[test] fn reset_required_is_a_distinct_non_retryable_terminal() { let problem = ApplicationProblem::reset_required( diff --git a/crates/tracedecay-contracts/src/retained_surfaces/service.rs b/crates/tracedecay-contracts/src/retained_surfaces/service.rs index 4e5f540ec8..b0beacfe3f 100644 --- a/crates/tracedecay-contracts/src/retained_surfaces/service.rs +++ b/crates/tracedecay-contracts/src/retained_surfaces/service.rs @@ -541,14 +541,10 @@ pub fn retained_surface_execution_problem( RetainedSurfaceExecutionErrorV1::NotFoundOrNotAuthorized => { ApplicationProblem::not_found_or_not_authorized(RetryDirective::Never) } - RetainedSurfaceExecutionErrorV1::Conflict => ApplicationProblem::Conflict { - diagnostic: diagnostic( - "application.retained.conflict", - "The retained operation conflicts with current state.", - ), - retry: RetryDirective::AfterRevalidate, - legal_actions: vec![LegalAction::Refresh], - }, + RetainedSurfaceExecutionErrorV1::Conflict => ApplicationProblem::conflict(diagnostic( + "application.retained.conflict", + "The retained operation conflicts with current state.", + )), RetainedSurfaceExecutionErrorV1::PartialEffect { reason_code, committed_receipt, diff --git a/crates/tracedecay-contracts/src/work_attempt.rs b/crates/tracedecay-contracts/src/work_attempt.rs index 1535ac0a47..198485e5d4 100644 --- a/crates/tracedecay-contracts/src/work_attempt.rs +++ b/crates/tracedecay-contracts/src/work_attempt.rs @@ -34,8 +34,8 @@ pub use capacity::{ WorkAttemptCapacityVerdictV1, }; use problem::{ - conflict_problem, contract_problem, denied_problem, list_page_contract_problem, - not_found_problem, stale_cursor_problem, storage_problem, + contract_problem, denied_problem, list_page_contract_problem, not_found_problem, + stale_cursor_problem, storage_problem, }; pub use product_admission::WorkProductAttemptServiceV1; pub(crate) use product_admission::{ @@ -576,10 +576,10 @@ where return if request.request_id() == &command.request_id { Ok(attempt) } else { - Err(conflict_problem( - "application.work-attempt.cancellation-conflict", - "A different cancellation request is already recorded.", - )) + Err(ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-attempt.cancellation-conflict").to_owned(), + message: ("A different cancellation request is already recorded.").to_owned(), + })) }; } if !matches!( @@ -588,10 +588,11 @@ where | WorkAttemptStateV1::Running | WorkAttemptStateV1::RecoveryRequired ) { - return Err(conflict_problem( - "application.work-attempt.not-cancellable", - "Only an open Work attempt can accept a cancellation request.", - )); + return Err(ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-attempt.not-cancellable").to_owned(), + message: ("Only an open Work attempt can accept a cancellation request.") + .to_owned(), + })); } let request = WorkCancellationRequestV1::new(command.request_id, command.occurred_at) .map_err(contract_problem)?; @@ -751,10 +752,10 @@ where .load(&authority, identity) .map_err(storage_problem)?; let WorkCancellationStateV1::Requested(request) = attempt.cancellation().clone() else { - return Err(conflict_problem( - "application.work-attempt.cancellation-not-requested", - "There is no pending cancellation request to acknowledge.", - )); + return Err(ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-attempt.cancellation-not-requested").to_owned(), + message: ("There is no pending cancellation request to acknowledge.").to_owned(), + })); }; let acknowledgement = WorkCancellationAcknowledgementV1::new(request, acknowledged_at) .map_err(contract_problem)?; @@ -789,10 +790,10 @@ where .map_err(storage_problem)?; let WorkCancellationStateV1::Acknowledged(acknowledgement) = attempt.cancellation().clone() else { - return Err(conflict_problem( - "application.work-attempt.cancellation-not-acknowledged", - "There is no acknowledged cancellation to escalate.", - )); + return Err(ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-attempt.cancellation-not-acknowledged").to_owned(), + message: ("There is no acknowledged cancellation to escalate.").to_owned(), + })); }; let escalation = WorkCancellationEscalationV1::new(acknowledgement, escalated_at) .map_err(contract_problem)?; @@ -880,10 +881,10 @@ where .load(&authority, identity) .map_err(storage_problem)?; if attempt.state() != WorkAttemptStateV1::RecoveryRequired { - return Err(conflict_problem( - "application.work-attempt.not-recovery-required", - "Only an attempt awaiting recovery can be failed this way.", - )); + return Err(ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-attempt.not-recovery-required").to_owned(), + message: ("Only an attempt awaiting recovery can be failed this way.").to_owned(), + })); } let digest = evidence.digest()?; let terminal = WorkTerminalEvidenceV1::failed(digest, evidence.observed_at) @@ -1083,10 +1084,11 @@ pub fn require_registered_work_topology( if snapshot.topology() == registered_topology { return Ok(()); } - Err(conflict_problem( - "application.work-attempt.topology-conflict", - "The Work attempt topology differs from the registered runtime authority.", - )) + Err(ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-attempt.topology-conflict").to_owned(), + message: ("The Work attempt topology differs from the registered runtime authority.") + .to_owned(), + })) } fn terminal_for_outcome( diff --git a/crates/tracedecay-contracts/src/work_attempt/problem.rs b/crates/tracedecay-contracts/src/work_attempt/problem.rs index 6cdc2ecf49..2d4781bbc3 100644 --- a/crates/tracedecay-contracts/src/work_attempt/problem.rs +++ b/crates/tracedecay-contracts/src/work_attempt/problem.rs @@ -7,22 +7,22 @@ use super::WorkAttemptStorageError; pub(super) fn storage_problem(error: WorkAttemptStorageError) -> ApplicationProblem { match error { WorkAttemptStorageError::NotFoundOrNotAuthorized => not_found_problem(), - WorkAttemptStorageError::AttemptConflict => conflict_problem( - "application.work-attempt.identity-conflict", - "The Work attempt identity was already used with different content.", - ), - WorkAttemptStorageError::RunAdmissionConflict => conflict_problem( - "application.work-attempt.run-admission-conflict", - "The Work attempt differs from this run's first admitted deadline or topology.", - ), - WorkAttemptStorageError::ReservationFenced => conflict_problem( - "application.work-attempt.reservation-fenced", - "The Work run control authority fenced new attempt reservations.", - ), - WorkAttemptStorageError::FenceConflict => conflict_problem( - "application.work-attempt.fence-conflict", - "The Work attempt lease fence changed after this transition was prepared.", - ), + WorkAttemptStorageError::AttemptConflict => ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-attempt.identity-conflict").to_owned(), + message: ("The Work attempt identity was already used with different content.").to_owned(), + }), + WorkAttemptStorageError::RunAdmissionConflict => ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-attempt.run-admission-conflict").to_owned(), + message: ("The Work attempt differs from this run's first admitted deadline or topology.").to_owned(), + }), + WorkAttemptStorageError::ReservationFenced => ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-attempt.reservation-fenced").to_owned(), + message: ("The Work run control authority fenced new attempt reservations.").to_owned(), + }), + WorkAttemptStorageError::FenceConflict => ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-attempt.fence-conflict").to_owned(), + message: ("The Work attempt lease fence changed after this transition was prepared.").to_owned(), + }), WorkAttemptStorageError::CapacityExceeded => ApplicationProblem::Saturated { diagnostic: SafeDiagnostic { code: "application.work-attempt.capacity-exhausted".to_owned(), @@ -75,14 +75,3 @@ pub(super) fn denied_problem(code: &str, message: &str) -> ApplicationProblem { legal_actions: vec![LegalAction::Refresh], } } - -pub(super) fn conflict_problem(code: &str, message: &str) -> ApplicationProblem { - ApplicationProblem::Conflict { - diagnostic: SafeDiagnostic { - code: code.to_owned(), - message: message.to_owned(), - }, - retry: RetryDirective::AfterRevalidate, - legal_actions: vec![LegalAction::Refresh], - } -} diff --git a/crates/tracedecay-contracts/src/work_attempt/product_admission.rs b/crates/tracedecay-contracts/src/work_attempt/product_admission.rs index a06b59e175..be99f3d69c 100644 --- a/crates/tracedecay-contracts/src/work_attempt/product_admission.rs +++ b/crates/tracedecay-contracts/src/work_attempt/product_admission.rs @@ -11,7 +11,7 @@ use tracedecay_domain::{ }; use crate::{ - ApplicationProblem, RequestAdmission, RequestContext, WorkGraphReadPortV1, + ApplicationProblem, RequestAdmission, RequestContext, SafeDiagnostic, WorkGraphReadPortV1, WorkGraphReadRequestV1, WorkGraphReadV1, WorkProductApplicationErrorV1, WorkProductAttemptAdmissionErrorV1, WorkProductAttemptAdmissionOutcomeV1, WorkProductAttemptAdmissionPortV1, WorkProductAttemptAdmissionV1, WorkProductBindingV1, @@ -22,8 +22,7 @@ use crate::{ use super::{ StartWorkAttemptCommand, WorkAttemptAdmissionKind, WorkAttemptStorageError, - WorkAttemptStoragePort, conflict_problem, contract_problem, denied_problem, not_found_problem, - storage_problem, + WorkAttemptStoragePort, contract_problem, denied_problem, not_found_problem, storage_problem, }; const WORK_PRODUCT_START_INPUT_DIGEST_DOMAIN: &str = @@ -214,18 +213,28 @@ pub(crate) fn product_admission_problem( }) } WorkProductAttemptAdmissionErrorV1::NotFoundOrNotAuthorized => not_found_problem(), - WorkProductAttemptAdmissionErrorV1::VersionConflict => conflict_problem( - "application.work-attempt.product-version-conflict", - "The canonical Work product graph changed before attempt admission.", - ), - WorkProductAttemptAdmissionErrorV1::IdentityConflict => conflict_problem( - "application.work-attempt.identity-conflict", - "The Work attempt identity was already used with different content.", - ), - WorkProductAttemptAdmissionErrorV1::IdempotencyConflict => conflict_problem( - "application.work-attempt.idempotency-conflict", - "The Work attempt command identity was already used with different input.", - ), + WorkProductAttemptAdmissionErrorV1::VersionConflict => { + ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-attempt.product-version-conflict").to_owned(), + message: ("The canonical Work product graph changed before attempt admission.") + .to_owned(), + }) + } + WorkProductAttemptAdmissionErrorV1::IdentityConflict => { + ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-attempt.identity-conflict").to_owned(), + message: ("The Work attempt identity was already used with different content.") + .to_owned(), + }) + } + WorkProductAttemptAdmissionErrorV1::IdempotencyConflict => { + ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-attempt.idempotency-conflict").to_owned(), + message: + ("The Work attempt command identity was already used with different input.") + .to_owned(), + }) + } WorkProductAttemptAdmissionErrorV1::CapacityExceeded => ApplicationProblem::Saturated { diagnostic: crate::SafeDiagnostic { code: "application.work-attempt.capacity-exhausted".to_owned(), @@ -286,10 +295,12 @@ where ) -> Result { admit_product_attempt_request(context, binding, command.occurred_at)?; if command.execution_snapshot.topology() != topology { - return Err(conflict_problem( - "application.work-attempt.topology-conflict", - "The Work attempt topology does not match the registered runtime authority.", - )); + return Err(ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-attempt.topology-conflict").to_owned(), + message: + ("The Work attempt topology does not match the registered runtime authority.") + .to_owned(), + })); } let authority = crate::work::work_authority(context)?; let identity = WorkAttemptIdentityV1::new( @@ -307,10 +318,12 @@ where if admission_kind != WorkAttemptAdmissionKind::Ordinary || !replayed_attempt_matches_command(context, &command, &identity, &existing)? { - return Err(conflict_problem( - "application.work-attempt.identity-conflict", - "The Work attempt identity was already used with different content.", - )); + return Err(ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-attempt.identity-conflict").to_owned(), + message: + ("The Work attempt identity was already used with different content.") + .to_owned(), + })); } return Ok(existing); } @@ -465,10 +478,13 @@ fn product_problem(error: WorkProductApplicationErrorV1) -> ApplicationProblem { } WorkProductApplicationErrorV1::TimedOut => ApplicationProblem::timed_out_before_admission(), WorkProductApplicationErrorV1::VersionConflict - | WorkProductApplicationErrorV1::RevisionConflict => conflict_problem( - "application.work-attempt.product-version-conflict", - "The canonical Work product graph changed before attempt admission.", - ), + | WorkProductApplicationErrorV1::RevisionConflict => { + ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-attempt.product-version-conflict").to_owned(), + message: ("The canonical Work product graph changed before attempt admission.") + .to_owned(), + }) + } WorkProductApplicationErrorV1::EvidenceContinuationStale => { ApplicationProblem::stale(crate::SafeDiagnostic { code: "application.work-attempt.product-evidence-continuation-stale".to_owned(), @@ -477,10 +493,12 @@ fn product_problem(error: WorkProductApplicationErrorV1) -> ApplicationProblem { .to_owned(), }) } - WorkProductApplicationErrorV1::IdempotencyConflict => conflict_problem( - "application.work-attempt.product-idempotency-conflict", - "The canonical Work product admission identity conflicts.", - ), + WorkProductApplicationErrorV1::IdempotencyConflict => { + ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-attempt.product-idempotency-conflict").to_owned(), + message: ("The canonical Work product admission identity conflicts.").to_owned(), + }) + } WorkProductApplicationErrorV1::InvalidRequest => invalid_start_problem(), // Named separately from a generic invalid command because the cause // and the remedy are both specific: the selection covers a slice of diff --git a/crates/tracedecay-contracts/src/work_attempt/product_synthesis_admission.rs b/crates/tracedecay-contracts/src/work_attempt/product_synthesis_admission.rs index 3e3ecc7dea..9344fc6f6c 100644 --- a/crates/tracedecay-contracts/src/work_attempt/product_synthesis_admission.rs +++ b/crates/tracedecay-contracts/src/work_attempt/product_synthesis_admission.rs @@ -7,16 +7,16 @@ use tracedecay_domain::{ }; use crate::{ - ApplicationProblem, RequestContext, WorkGraphReadPortV1, WorkProductAttemptAdmissionPortV1, - WorkProductAttemptAdmissionV1, WorkProductBindingV1, WorkProductOwnerAuthorizationPortV1, - WorkProductRevisionPinsV1, WorkProductSynthesisAdmissionV1, WorkSynthesisAdmissionRecordV1, - WorkSynthesisAdmissionV1, + ApplicationProblem, RequestContext, SafeDiagnostic, WorkGraphReadPortV1, + WorkProductAttemptAdmissionPortV1, WorkProductAttemptAdmissionV1, WorkProductBindingV1, + WorkProductOwnerAuthorizationPortV1, WorkProductRevisionPinsV1, + WorkProductSynthesisAdmissionV1, WorkSynthesisAdmissionRecordV1, WorkSynthesisAdmissionV1, }; use super::{ CurrentWorkProductAttemptGraphV1, StartWorkAttemptCommand, WorkAttemptStorageError, WorkAttemptStoragePort, WorkSynthesisAdmissionStoragePort, WorkSynthesisInsertOutcome, - accepted_attempt_draft, admit_product_attempt_request, conflict_problem, contract_problem, + accepted_attempt_draft, admit_product_attempt_request, contract_problem, current_work_product_attempt_graph, denied_problem, not_found_problem, product_admission_problem, product_attempt_projection_binding, storage_problem, }; @@ -259,8 +259,8 @@ where } fn identity_conflict() -> ApplicationProblem { - conflict_problem( - "application.work-attempt.identity-conflict", - "The Work attempt identity was already used with different content.", - ) + ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-attempt.identity-conflict").to_owned(), + message: ("The Work attempt identity was already used with different content.").to_owned(), + }) } diff --git a/crates/tracedecay-contracts/src/work_attempt_effect.rs b/crates/tracedecay-contracts/src/work_attempt_effect.rs index 91d3f87b2a..364bb10f77 100644 --- a/crates/tracedecay-contracts/src/work_attempt_effect.rs +++ b/crates/tracedecay-contracts/src/work_attempt_effect.rs @@ -13,7 +13,7 @@ use thiserror::Error; use tracedecay_domain::{UtcMicros, WorkAttemptIdentityV1, WorkAuthority, WorkEffectStateV1}; use crate::work::work_authority; -use crate::{ApplicationProblem, LegalAction, RequestContext, RetryDirective, SafeDiagnostic}; +use crate::{ApplicationProblem, RequestContext, RetryDirective, SafeDiagnostic}; #[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] pub enum WorkAttemptEffectHolderErrorV1 { @@ -257,15 +257,11 @@ fn effect_problem(error: WorkAttemptEffectStorageErrorV1) -> ApplicationProblem WorkAttemptEffectStorageErrorV1::NotFoundOrNotAuthorized => { ApplicationProblem::not_found_or_not_authorized(RetryDirective::Never) } - WorkAttemptEffectStorageErrorV1::Conflict => ApplicationProblem::Conflict { - diagnostic: SafeDiagnostic { - code: "application.work-attempt-effect.conflict".to_owned(), - message: "The Work attempt effect receipt conflicts with its prior dispatch." - .to_owned(), - }, - retry: RetryDirective::AfterRevalidate, - legal_actions: vec![LegalAction::Refresh], - }, + WorkAttemptEffectStorageErrorV1::Conflict => ApplicationProblem::conflict(SafeDiagnostic { + code: "application.work-attempt-effect.conflict".to_owned(), + message: "The Work attempt effect receipt conflicts with its prior dispatch." + .to_owned(), + }), WorkAttemptEffectStorageErrorV1::Unavailable => { ApplicationProblem::unavailable(SafeDiagnostic { code: "application.work-attempt-effect.unavailable".to_owned(), diff --git a/crates/tracedecay-contracts/src/work_duplicate_adjudication.rs b/crates/tracedecay-contracts/src/work_duplicate_adjudication.rs index dc4feb7477..7087bc7f5c 100644 --- a/crates/tracedecay-contracts/src/work_duplicate_adjudication.rs +++ b/crates/tracedecay-contracts/src/work_duplicate_adjudication.rs @@ -14,10 +14,7 @@ use tracedecay_domain::{ }; use crate::work::work_authority; -use crate::{ - ApplicationProblem, LegalAction, RequestAdmission, RequestContext, RetryDirective, - SafeDiagnostic, -}; +use crate::{ApplicationProblem, RequestAdmission, RequestContext, RetryDirective, SafeDiagnostic}; pub fn work_duplicate_adjudication_input_digest( command: &WorkDuplicateAdjudicationCommandV1, @@ -436,25 +433,17 @@ fn storage_problem(error: WorkDuplicateAdjudicationStorageErrorV1) -> Applicatio WorkDuplicateAdjudicationStorageErrorV1::NotFoundOrNotAuthorized => { ApplicationProblem::not_found_or_not_authorized(RetryDirective::Never) } - WorkDuplicateAdjudicationStorageErrorV1::RevisionConflict => ApplicationProblem::Conflict { - diagnostic: SafeDiagnostic { + WorkDuplicateAdjudicationStorageErrorV1::RevisionConflict => ApplicationProblem::conflict(SafeDiagnostic { code: "application.work.duplicate-adjudication.revision-conflict".to_owned(), message: "The duplicate Work adjudication changed after this command was prepared." .to_owned(), - }, - retry: RetryDirective::AfterRevalidate, - legal_actions: vec![LegalAction::Refresh], - }, + }), WorkDuplicateAdjudicationStorageErrorV1::IdempotencyConflict => { - ApplicationProblem::Conflict { - diagnostic: SafeDiagnostic { + ApplicationProblem::conflict(SafeDiagnostic { code: "application.work.duplicate-adjudication.idempotency-conflict".to_owned(), message: "The duplicate Work adjudication command identity was already used with different input." .to_owned(), - }, - retry: RetryDirective::AfterRevalidate, - legal_actions: vec![LegalAction::Refresh], - } + }) } WorkDuplicateAdjudicationStorageErrorV1::Unavailable => { ApplicationProblem::unavailable(SafeDiagnostic { diff --git a/crates/tracedecay-contracts/src/work_leak_adjudication.rs b/crates/tracedecay-contracts/src/work_leak_adjudication.rs index d4fc1d643b..8cfc33c6b3 100644 --- a/crates/tracedecay-contracts/src/work_leak_adjudication.rs +++ b/crates/tracedecay-contracts/src/work_leak_adjudication.rs @@ -299,10 +299,10 @@ where .inspect(&authority, &command, scan_started_at, scan_deadline) .map_err(evidence_problem)?; if !evidence.validate_for(&command, scan_started_at, scan_deadline) { - return Err(conflict_problem( - "application.work-leak.evidence-conflict", - "The bounded leak scan did not prove a valid verdict.", - )); + return Err(ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-leak.evidence-conflict").to_owned(), + message: ("The bounded leak scan did not prove a valid verdict.").to_owned(), + })); } let canonical_input_digest = canonical_sha256(&(LEAK_INPUT_DIGEST_DOMAIN, &command, &evidence, scan_deadline)) @@ -355,26 +355,15 @@ fn admit(context: &RequestContext, observed_at: UtcMicros) -> Result<(), Applica } } -fn conflict_problem(code: &str, message: &str) -> ApplicationProblem { - ApplicationProblem::Conflict { - diagnostic: SafeDiagnostic { - code: code.to_owned(), - message: message.to_owned(), - }, - retry: RetryDirective::AfterRevalidate, - legal_actions: vec![LegalAction::Refresh], - } -} - fn evidence_problem(error: WorkLeakEvidenceErrorV1) -> ApplicationProblem { match error { WorkLeakEvidenceErrorV1::NotFoundOrNotAuthorized => { ApplicationProblem::not_found_or_not_authorized(RetryDirective::Never) } - WorkLeakEvidenceErrorV1::Conflict => conflict_problem( - "application.work-leak.evidence-conflict", - "The Work leak evidence changed during inspection.", - ), + WorkLeakEvidenceErrorV1::Conflict => ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-leak.evidence-conflict").to_owned(), + message: ("The Work leak evidence changed during inspection.").to_owned(), + }), WorkLeakEvidenceErrorV1::TimedOut => ApplicationProblem::TimedOut { stage: CancellationStage::DuringRead, retry: RetryDirective::AfterRevalidate, @@ -392,14 +381,19 @@ fn storage_problem(error: WorkLeakAdjudicationStorageErrorV1) -> ApplicationProb WorkLeakAdjudicationStorageErrorV1::NotFoundOrNotAuthorized => { ApplicationProblem::not_found_or_not_authorized(RetryDirective::Never) } - WorkLeakAdjudicationStorageErrorV1::RevisionConflict => conflict_problem( - "application.work-leak.revision-conflict", - "The Work leak adjudication changed before publication.", - ), - WorkLeakAdjudicationStorageErrorV1::IdempotencyConflict => conflict_problem( - "application.work-leak.idempotency-conflict", - "The Work leak command identity was already used with different input.", - ), + WorkLeakAdjudicationStorageErrorV1::RevisionConflict => { + ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-leak.revision-conflict").to_owned(), + message: ("The Work leak adjudication changed before publication.").to_owned(), + }) + } + WorkLeakAdjudicationStorageErrorV1::IdempotencyConflict => { + ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-leak.idempotency-conflict").to_owned(), + message: ("The Work leak command identity was already used with different input.") + .to_owned(), + }) + } WorkLeakAdjudicationStorageErrorV1::Unavailable => { ApplicationProblem::unavailable(SafeDiagnostic { code: "application.work-leak.unavailable".to_owned(), diff --git a/crates/tracedecay-contracts/src/work_placement.rs b/crates/tracedecay-contracts/src/work_placement.rs index ff69e0f8f3..0c7e8343f6 100644 --- a/crates/tracedecay-contracts/src/work_placement.rs +++ b/crates/tracedecay-contracts/src/work_placement.rs @@ -34,10 +34,7 @@ use tracedecay_domain::{ }; use crate::work::work_authority; -use crate::{ - ApplicationProblem, LegalAction, RequestAdmission, RequestContext, RetryDirective, - SafeDiagnostic, -}; +use crate::{ApplicationProblem, RequestAdmission, RequestContext, RetryDirective, SafeDiagnostic}; #[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] pub enum WorkPlacementStorageError { @@ -211,10 +208,10 @@ where { Ok(existing) } else { - Err(conflict_problem( - "application.work-placement.identity-conflict", - "The Work run already holds a different placement.", - )) + Err(ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-placement.identity-conflict").to_owned(), + message: ("The Work run already holds a different placement.").to_owned(), + })) }; } let preflight = self.evaluate(&authority, identity, command.target, observe)?; @@ -358,14 +355,19 @@ fn storage_problem(error: WorkPlacementStorageError) -> ApplicationProblem { fn contract_problem(error: WorkPlacementContractError) -> ApplicationProblem { match error { - WorkPlacementContractError::AlreadyReleased => conflict_problem( - "application.work-placement.already-released", - "The Work placement was already released.", - ), - WorkPlacementContractError::NonMonotonicTransition => conflict_problem( - "application.work-placement.non-monotonic", - "The Work placement transition is older than the published state.", - ), + WorkPlacementContractError::AlreadyReleased => { + ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-placement.already-released").to_owned(), + message: ("The Work placement was already released.").to_owned(), + }) + } + WorkPlacementContractError::NonMonotonicTransition => { + ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-placement.non-monotonic").to_owned(), + message: ("The Work placement transition is older than the published state.") + .to_owned(), + }) + } _ => ApplicationProblem::invalid_request(SafeDiagnostic { code: "application.work-placement.invalid-placement".to_owned(), message: "The Work placement command or stored state is invalid.".to_owned(), @@ -387,38 +389,24 @@ fn blocked_problem( .map(placement_blocker_name) .collect::>() .join(", "); - ApplicationProblem::Conflict { - diagnostic: SafeDiagnostic { - code: "application.work-placement.blocked".to_owned(), - message: format!("The Work placement is blocked by: {named}."), - }, - retry: RetryDirective::AfterRevalidate, - legal_actions: vec![LegalAction::Refresh], - } + ApplicationProblem::conflict(SafeDiagnostic { + code: "application.work-placement.blocked".to_owned(), + message: format!("The Work placement is blocked by: {named}."), + }) } fn authority_conflict_problem() -> ApplicationProblem { - conflict_problem( - "application.work-placement.authority-conflict", - "The Work placement authority version changed after this command was prepared.", - ) + ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-placement.authority-conflict").to_owned(), + message: ("The Work placement authority version changed after this command was prepared.") + .to_owned(), + }) } fn not_found_problem() -> ApplicationProblem { ApplicationProblem::not_found_or_not_authorized(RetryDirective::Never) } -fn conflict_problem(code: &str, message: &str) -> ApplicationProblem { - ApplicationProblem::Conflict { - diagnostic: SafeDiagnostic { - code: code.to_owned(), - message: message.to_owned(), - }, - retry: RetryDirective::AfterRevalidate, - legal_actions: vec![LegalAction::Refresh], - } -} - /// Wire names for the closed blocker vocabulary. Kept as a match so a new /// variant cannot silently become `"unknown"` through a JSON round-trip. const fn placement_blocker_name(blocker: WorkPlacementBlockerV1) -> &'static str { diff --git a/crates/tracedecay-contracts/src/work_retry.rs b/crates/tracedecay-contracts/src/work_retry.rs index ef0d79bb14..6bfad5598d 100644 --- a/crates/tracedecay-contracts/src/work_retry.rs +++ b/crates/tracedecay-contracts/src/work_retry.rs @@ -26,8 +26,8 @@ use crate::work_attempt_effect::{ WorkAttemptEffectResolutionV1, WorkAttemptEffectStorageErrorV1, WorkAttemptEffectStoragePortV1, }; use crate::{ - ApplicationContractError, ApplicationProblem, LegalAction, RequestAdmission, RequestContext, - RetryDirective, SafeDiagnostic, WorkGraphReadPortV1, WorkProductAttemptAdmissionPortV1, + ApplicationContractError, ApplicationProblem, RequestAdmission, RequestContext, RetryDirective, + SafeDiagnostic, WorkGraphReadPortV1, WorkProductAttemptAdmissionPortV1, WorkProductAttemptAdmissionV1, WorkProductBindingV1, WorkProductOwnerAuthorizationPortV1, WorkProductRetryAdmissionV1, WorkProductRevisionPinsV1, WorkflowFanOutAttemptBindingV1, WorkflowRunAppendRequest, @@ -406,10 +406,12 @@ where .map_err(storage_problem)? { if replayed.receipt().canonical_input_digest != input_digest { - return Err(conflict_problem( - "application.work-retry.idempotency-conflict", - "The Work retry command identity was already used with different input.", - )); + return Err(ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-retry.idempotency-conflict").to_owned(), + message: + ("The Work retry command identity was already used with different input.") + .to_owned(), + })); } let attempt = match &replayed { WorkRetryAttemptOutcomeV1::Created { attempt, .. } @@ -460,10 +462,10 @@ where .map_err(evidence_problem)?; validate_failure(&command, &original, &failure)?; if failure.observed_at.0 > restarted_at.0 { - return Err(conflict_problem( - "application.work-retry.failure-conflict", - "The retry failure was observed after retry admission.", - )); + return Err(ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-retry.failure-conflict").to_owned(), + message: ("The retry failure was observed after retry admission.").to_owned(), + })); } require_product_retry_admission(&product, &original)?; let attempt = prepare_product_retry_attempt( @@ -534,10 +536,10 @@ fn prepare_workflow_rebind( .get(&rebind.binding.step_id) .is_none_or(|plan| plan.plan_digest != rebind.binding.plan_digest) { - return Err(conflict_problem( - "application.work-retry.workflow-binding-conflict", - "The workflow child binding no longer matches its admitted plan.", - )); + return Err(ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-retry.workflow-binding-conflict").to_owned(), + message: ("The workflow child binding no longer matches its admitted plan.").to_owned(), + })); } if let Some(event) = rebind .projection @@ -557,10 +559,11 @@ fn prepare_workflow_rebind( && retry_receipt_digest == &receipt.owner_receipt_digest ); if !exact_replay { - return Err(conflict_problem( - "application.work-retry.workflow-binding-conflict", - "The workflow command identity was already used by another transition.", - )); + return Err(ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-retry.workflow-binding-conflict").to_owned(), + message: ("The workflow command identity was already used by another transition.") + .to_owned(), + })); } return Ok(Some(WorkflowRunAppendRequest { expected_sequence: event.sequence().checked_sub(1), @@ -571,10 +574,10 @@ fn prepare_workflow_rebind( .projection .planned_fan_out_attempt(&receipt.command.original_attempt) .ok_or_else(|| { - conflict_problem( - "application.work-retry.workflow-binding-conflict", - "The original Work attempt is not the active workflow child.", - ) + ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-retry.workflow-binding-conflict").to_owned(), + message: ("The original Work attempt is not the active workflow child.").to_owned(), + }) })? .clone(); let event = rebind @@ -594,10 +597,11 @@ fn prepare_workflow_rebind( }, ) .map_err(|_| { - conflict_problem( - "application.work-retry.workflow-binding-conflict", - "The workflow child retry transition is no longer authorized.", - ) + ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-retry.workflow-binding-conflict").to_owned(), + message: ("The workflow child retry transition is no longer authorized.") + .to_owned(), + }) })?; Ok(Some(WorkflowRunAppendRequest { expected_sequence: Some(rebind.projection.sequence()), @@ -616,10 +620,10 @@ fn require_product_retry_admission( if !item.is_execution_admitted() || item.accepted_proposal() != Some(attempt.projection_binding().accepted_proposal()) { - return Err(conflict_problem( - "application.work-retry.product-conflict", - "The canonical Work product graph no longer admits this retry.", - )); + return Err(ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-retry.product-conflict").to_owned(), + message: ("The canonical Work product graph no longer admits this retry.").to_owned(), + })); } Ok(()) } @@ -644,10 +648,11 @@ where { Ok(()) } else { - Err(conflict_problem( - "application.work-retry.effect-unknown", - "The original Work attempt has an unresolved non-repeatable effect.", - )) + Err(ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-retry.effect-unknown").to_owned(), + message: ("The original Work attempt has an unresolved non-repeatable effect.") + .to_owned(), + })) } } @@ -669,10 +674,10 @@ where if original.execution().execution_snapshot().topology() != topology || restarted_at.0 >= original.execution().deadline().0 { - return Err(conflict_problem( - "application.work-retry.admission-conflict", - "The original Work admission no longer permits this retry.", - )); + return Err(ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-retry.admission-conflict").to_owned(), + message: ("The original Work admission no longer permits this retry.").to_owned(), + })); } let identity = WorkAttemptIdentityV1::new( original.identity().task_id().clone(), @@ -764,33 +769,35 @@ fn validate_failure( failure: &VerifiedWorkRetryFailureV1, ) -> Result<(), ApplicationProblem> { if failure.selector != command.failure || failure.evidence_digest.validate().is_err() { - return Err(conflict_problem( - "application.work-retry.failure-conflict", - "The resolved failure does not authorize this Work retry.", - )); + return Err(ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-retry.failure-conflict").to_owned(), + message: ("The resolved failure does not authorize this Work retry.").to_owned(), + })); } if command.failure.cause == WorkRetryCauseV1::RestartRecoveryRequired { let WorkRecoveryStateV1::RecoveryRequired { observed_at, .. } = original.recovery() else { - return Err(conflict_problem( - "application.work-retry.recovery-conflict", - "The original Work attempt no longer requires restart recovery.", - )); + return Err(ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-retry.recovery-conflict").to_owned(), + message: ("The original Work attempt no longer requires restart recovery.") + .to_owned(), + })); }; if original.state() != WorkAttemptStateV1::RecoveryRequired || observed_at != &failure.observed_at { - return Err(conflict_problem( - "application.work-retry.recovery-conflict", - "The restart recovery evidence no longer matches the original attempt.", - )); + return Err(ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-retry.recovery-conflict").to_owned(), + message: ("The restart recovery evidence no longer matches the original attempt.") + .to_owned(), + })); } return Ok(()); } let Some(terminal) = original.terminal() else { - return Err(conflict_problem( - "application.work-retry.original-not-terminal", - "A runtime failure retry requires terminal evidence.", - )); + return Err(ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-retry.original-not-terminal").to_owned(), + message: ("A runtime failure retry requires terminal evidence.").to_owned(), + })); }; let (digest, observed_at, eligible) = match terminal { WorkTerminalEvidenceV1::Failed { @@ -811,10 +818,11 @@ fn validate_failure( } => (evidence_digest, observed_at, false), }; if !eligible || digest != &failure.evidence_digest || observed_at != &failure.observed_at { - return Err(conflict_problem( - "application.work-retry.runtime-evidence-conflict", - "The runtime failure no longer matches the original terminal receipt.", - )); + return Err(ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-retry.runtime-evidence-conflict").to_owned(), + message: ("The runtime failure no longer matches the original terminal receipt.") + .to_owned(), + })); } Ok(()) } @@ -834,17 +842,6 @@ fn retry_receipt_problem(_error: ApplicationContractError) -> ApplicationProblem }) } -fn conflict_problem(code: &str, message: &str) -> ApplicationProblem { - ApplicationProblem::Conflict { - diagnostic: SafeDiagnostic { - code: code.to_owned(), - message: message.to_owned(), - }, - retry: RetryDirective::AfterRevalidate, - legal_actions: vec![LegalAction::Refresh], - } -} - fn not_found_problem() -> ApplicationProblem { ApplicationProblem::not_found_or_not_authorized(RetryDirective::Never) } @@ -852,10 +849,10 @@ fn not_found_problem() -> ApplicationProblem { fn evidence_problem(error: WorkRetryEvidenceErrorV1) -> ApplicationProblem { match error { WorkRetryEvidenceErrorV1::NotFoundOrNotAuthorized => not_found_problem(), - WorkRetryEvidenceErrorV1::Conflict => conflict_problem( - "application.work-retry.failure-conflict", - "The Work retry failure evidence changed.", - ), + WorkRetryEvidenceErrorV1::Conflict => ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-retry.failure-conflict").to_owned(), + message: ("The Work retry failure evidence changed.").to_owned(), + }), WorkRetryEvidenceErrorV1::Unavailable => ApplicationProblem::unavailable(SafeDiagnostic { code: "application.work-retry.evidence-unavailable".to_owned(), message: "The Work retry failure evidence authority is unavailable.".to_owned(), @@ -866,20 +863,22 @@ fn evidence_problem(error: WorkRetryEvidenceErrorV1) -> ApplicationProblem { fn storage_problem(error: WorkAttemptStorageError) -> ApplicationProblem { match error { WorkAttemptStorageError::NotFoundOrNotAuthorized => not_found_problem(), - WorkAttemptStorageError::CapacityExceeded => conflict_problem( - "application.work-retry.capacity-exhausted", - "Work retry capacity is exhausted.", - ), - WorkAttemptStorageError::ReservationFenced => conflict_problem( - "application.work-retry.reservation-fenced", - "The Work run does not currently admit a retry reservation.", - ), + WorkAttemptStorageError::CapacityExceeded => ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-retry.capacity-exhausted").to_owned(), + message: ("Work retry capacity is exhausted.").to_owned(), + }), + WorkAttemptStorageError::ReservationFenced => { + ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-retry.reservation-fenced").to_owned(), + message: ("The Work run does not currently admit a retry reservation.").to_owned(), + }) + } WorkAttemptStorageError::AttemptConflict | WorkAttemptStorageError::RunAdmissionConflict - | WorkAttemptStorageError::FenceConflict => conflict_problem( - "application.work-retry.conflict", - "The Work retry authority changed.", - ), + | WorkAttemptStorageError::FenceConflict => ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-retry.conflict").to_owned(), + message: ("The Work retry authority changed.").to_owned(), + }), WorkAttemptStorageError::Unavailable => ApplicationProblem::unavailable(SafeDiagnostic { code: "application.work-retry.unavailable".to_owned(), message: "The Work retry authority is unavailable.".to_owned(), @@ -892,10 +891,10 @@ fn effect_storage_problem(error: WorkAttemptEffectStorageErrorV1) -> Application WorkAttemptEffectStorageErrorV1::NotFoundOrNotAuthorized => { ApplicationProblem::not_found_or_not_authorized(RetryDirective::Never) } - WorkAttemptEffectStorageErrorV1::Conflict => conflict_problem( - "application.work-retry.effect-conflict", - "The original Work attempt effect receipt changed.", - ), + WorkAttemptEffectStorageErrorV1::Conflict => ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-retry.effect-conflict").to_owned(), + message: ("The original Work attempt effect receipt changed.").to_owned(), + }), WorkAttemptEffectStorageErrorV1::Unavailable => { ApplicationProblem::unavailable(SafeDiagnostic { code: "application.work-retry.effect-unavailable".to_owned(), diff --git a/crates/tracedecay-contracts/src/work_run_control.rs b/crates/tracedecay-contracts/src/work_run_control.rs index 45e1d25945..c9e47d418c 100644 --- a/crates/tracedecay-contracts/src/work_run_control.rs +++ b/crates/tracedecay-contracts/src/work_run_control.rs @@ -39,10 +39,7 @@ use tracedecay_domain::{ }; use crate::work::work_authority; -use crate::{ - ApplicationProblem, LegalAction, RequestAdmission, RequestContext, RetryDirective, - SafeDiagnostic, -}; +use crate::{ApplicationProblem, RequestAdmission, RequestContext, RetryDirective, SafeDiagnostic}; #[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] pub enum WorkRunControlStorageError { @@ -409,10 +406,10 @@ where let current = frontier.control.clone().ok_or_else(|| { // A run that was never paused has nothing to resume, and // answering "resumed" would be a false receipt. - conflict_problem( - "application.work-run-control.not-paused", - "The Work run has no published control state to resume.", - ) + ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-run-control.not-paused").to_owned(), + message: ("The Work run has no published control state to resume.").to_owned(), + }) })?; let expected = WorkRunControlAuthorityV1::new(command.expected_authority_version) .map_err(contract_problem)?; @@ -501,10 +498,12 @@ where // are indistinguishable from an idle one. hotpath::gauge!("application.work.run_control.reservation.denied_paused") .inc(1u64); - Err(conflict_problem( - "application.work-run-control.paused", - "The Work run is paused, so no new attempt reservation is admitted.", - )) + Err(ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-run-control.paused").to_owned(), + message: + ("The Work run is paused, so no new attempt reservation is admitted.") + .to_owned(), + })) } Some(_) | None => Ok(()), } @@ -606,18 +605,23 @@ fn storage_problem(error: WorkRunControlStorageError) -> ApplicationProblem { fn contract_problem(error: WorkRunControlContractError) -> ApplicationProblem { match error { - WorkRunControlContractError::AlreadyPaused => conflict_problem( - "application.work-run-control.already-paused", - "The Work run is already paused.", - ), - WorkRunControlContractError::NotPaused => conflict_problem( - "application.work-run-control.not-paused", - "The Work run is not paused.", - ), - WorkRunControlContractError::NonMonotonicTransition => conflict_problem( - "application.work-run-control.non-monotonic", - "The Work run control transition is older than the published state.", - ), + WorkRunControlContractError::AlreadyPaused => { + ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-run-control.already-paused").to_owned(), + message: ("The Work run is already paused.").to_owned(), + }) + } + WorkRunControlContractError::NotPaused => ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-run-control.not-paused").to_owned(), + message: ("The Work run is not paused.").to_owned(), + }), + WorkRunControlContractError::NonMonotonicTransition => { + ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-run-control.non-monotonic").to_owned(), + message: ("The Work run control transition is older than the published state.") + .to_owned(), + }) + } WorkRunControlContractError::InvalidAuthorityVersion | WorkRunControlContractError::AuthorityVersionOverflow | WorkRunControlContractError::InvalidDeadlineCheckpoint @@ -672,23 +676,14 @@ fn invalid_open_interval_durable_problem() -> ApplicationProblem { } fn authority_conflict_problem() -> ApplicationProblem { - conflict_problem( - "application.work-run-control.authority-conflict", - "The Work run control authority version changed after this command was prepared.", - ) + ApplicationProblem::conflict(SafeDiagnostic { + code: ("application.work-run-control.authority-conflict").to_owned(), + message: + ("The Work run control authority version changed after this command was prepared.") + .to_owned(), + }) } fn not_found_problem() -> ApplicationProblem { ApplicationProblem::not_found_or_not_authorized(RetryDirective::Never) } - -fn conflict_problem(code: &str, message: &str) -> ApplicationProblem { - ApplicationProblem::Conflict { - diagnostic: SafeDiagnostic { - code: code.to_owned(), - message: message.to_owned(), - }, - retry: RetryDirective::AfterRevalidate, - legal_actions: vec![LegalAction::Refresh], - } -} diff --git a/crates/tracedecay-daemon-service/src/application_surface/operation_events.rs b/crates/tracedecay-daemon-service/src/application_surface/operation_events.rs index a4a0249744..bae74ea533 100644 --- a/crates/tracedecay-daemon-service/src/application_surface/operation_events.rs +++ b/crates/tracedecay-daemon-service/src/application_surface/operation_events.rs @@ -831,14 +831,10 @@ pub(super) fn operation_event_problem( legal_actions: vec![LegalAction::Refresh], } } - OperationEventError::InvalidFrontier => ApplicationProblem::Conflict { - diagnostic: SafeDiagnostic { - code: "operation_event.invalid_frontier".to_owned(), - message: "The requested operation-event frontier is invalid".to_owned(), - }, - retry: RetryDirective::AfterRevalidate, - legal_actions: vec![LegalAction::Refresh], - }, + OperationEventError::InvalidFrontier => ApplicationProblem::conflict(SafeDiagnostic { + code: "operation_event.invalid_frontier".to_owned(), + message: "The requested operation-event frontier is invalid".to_owned(), + }), OperationEventError::RequestNotAdmitted => ApplicationProblem::TimedOut { stage: CancellationStage::BeforeAdmission, retry: RetryDirective::Never, @@ -867,14 +863,10 @@ pub(super) fn operation_event_problem( // published, so the client re-reads current state instead of retrying // the same publish. OperationEventError::AlreadyBound | OperationEventError::TerminalAlreadyPublished => { - ApplicationProblem::Conflict { - diagnostic: SafeDiagnostic { - code: "operation_event.already_published".to_owned(), - message: "The operation-event identity is already published".to_owned(), - }, - retry: RetryDirective::AfterRevalidate, - legal_actions: vec![LegalAction::Refresh], - } + ApplicationProblem::conflict(SafeDiagnostic { + code: "operation_event.already_published".to_owned(), + message: "The operation-event identity is already published".to_owned(), + }) } // A misconfigured authority is a deterministic, process-lifetime // failure. It is not the caller's request that is wrong and no amount diff --git a/crates/tracedecay-daemon-service/src/application_surface/problems.rs b/crates/tracedecay-daemon-service/src/application_surface/problems.rs index 32b767892d..bfa6ef99c4 100644 --- a/crates/tracedecay-daemon-service/src/application_surface/problems.rs +++ b/crates/tracedecay-daemon-service/src/application_surface/problems.rs @@ -198,14 +198,12 @@ pub(super) fn invocation_contract_problem( "The daemon rejected the application request", )?) } - tracedecay_contracts::InvocationError::Conflict => ApplicationProblem::Conflict { - diagnostic: SafeDiagnostic::new( + tracedecay_contracts::InvocationError::Conflict => { + ApplicationProblem::conflict(SafeDiagnostic::new( "application.surface.conflict", "The application request conflicts with current state", - )?, - retry: RetryDirective::AfterRevalidate, - legal_actions: vec![LegalAction::Refresh], - }, + )?) + } tracedecay_contracts::InvocationError::Unavailable => { ApplicationProblem::unavailable(SafeDiagnostic::new( "application.surface.unavailable", diff --git a/crates/tracedecay-daemon-service/src/invocation/configuration.rs b/crates/tracedecay-daemon-service/src/invocation/configuration.rs index c722abb78b..e03fbbae6d 100644 --- a/crates/tracedecay-daemon-service/src/invocation/configuration.rs +++ b/crates/tracedecay-daemon-service/src/invocation/configuration.rs @@ -777,14 +777,10 @@ pub(super) fn configuration_problem(error: ConfigurationError) -> ApplicationPro ApplicationProblem::not_found_or_not_authorized(RetryDirective::Never) } ConfigurationError::RevisionConflict | ConfigurationError::IdempotencyConflict => { - ApplicationProblem::Conflict { - diagnostic: SafeDiagnostic { - code: "configuration.conflict".to_owned(), - message: "The configuration request conflicts with current state".to_owned(), - }, - retry: RetryDirective::AfterRevalidate, - legal_actions: vec![tracedecay_contracts::LegalAction::Refresh], - } + ApplicationProblem::conflict(SafeDiagnostic { + code: "configuration.conflict".to_owned(), + message: "The configuration request conflicts with current state".to_owned(), + }) } ConfigurationError::PlanExpired | ConfigurationError::PlanStale => { ApplicationProblem::stale(SafeDiagnostic { diff --git a/crates/tracedecay-daemon-service/src/invocation/source_edit.rs b/crates/tracedecay-daemon-service/src/invocation/source_edit.rs index 8c6a43e759..f8d52f3fe1 100644 --- a/crates/tracedecay-daemon-service/src/invocation/source_edit.rs +++ b/crates/tracedecay-daemon-service/src/invocation/source_edit.rs @@ -4,10 +4,13 @@ use std::sync::Arc; use tracedecay_contracts::{ ApplicationExecutionFailureClassV1, ApplicationProblem, CancellationContext, Deadline, - LegalAction, RequestId, RetryDirective, SafeDiagnostic, SourceEditInvocationV1, + RequestId, SafeDiagnostic, SourceEditInvocationV1, SourceEditReconciliationInvocationV1, SourceEditRollbackInvocationV1, }; use tracedecay_daemon_protocol::DaemonInvocationProblem; + +#[cfg(test)] +use tracedecay_contracts::RetryDirective; use tracedecay_domain::UtcMicros; use tracedecay_domain::errors::TraceDecayError; @@ -261,11 +264,7 @@ fn source_edit_execution_problem( let diagnostic = source_edit_safe_diagnostic(code, message)?; match diagnostic.code.as_str() { SOURCE_EDIT_EXPECTED_STATE_MISMATCH => Ok(ApplicationProblem::stale(diagnostic)), - SOURCE_EDIT_IDEMPOTENCY_CONFLICT => Ok(ApplicationProblem::Conflict { - diagnostic, - retry: RetryDirective::AfterRevalidate, - legal_actions: vec![LegalAction::Refresh], - }), + SOURCE_EDIT_IDEMPOTENCY_CONFLICT => Ok(ApplicationProblem::conflict(diagnostic)), SOURCE_EDIT_SYMBOL_EVIDENCE_UNAVAILABLE | SOURCE_EDIT_DIAGNOSTICS_UNAVAILABLE => { Ok(ApplicationProblem::unavailable(diagnostic)) } diff --git a/crates/tracedecay-daemon-service/src/invocation/work/attempt_operations.rs b/crates/tracedecay-daemon-service/src/invocation/work/attempt_operations.rs index 69ec2950f0..7952ae6c95 100644 --- a/crates/tracedecay-daemon-service/src/invocation/work/attempt_operations.rs +++ b/crates/tracedecay-daemon-service/src/invocation/work/attempt_operations.rs @@ -7,8 +7,8 @@ use tracedecay_application::observability::BoundedObservabilityProducerV1; use tracedecay_contracts::{ AdmitWorkSynthesisCommand, ApplicationProblem, CancelWorkAttemptCommand, Deadline, RequestContext, RequestId, ResumeWorkAttemptsCommand, RetryWorkAttemptCommandV1, - SafeDiagnostic, StartWorkAttemptCommand, WorkAttemptStatusRequestV1, - WorkSynthesisAttemptV1, WorkflowArtifactStorePort, + SafeDiagnostic, StartWorkAttemptCommand, WorkAttemptStatusRequestV1, WorkSynthesisAttemptV1, + WorkflowArtifactStorePort, }; use tracedecay_domain::{ManifestDigest, UtcMicros, WorkAttemptStateV1, WorkAttemptV1}; use tracedecay_tool_catalog::UseCaseId; @@ -457,15 +457,11 @@ pub(super) fn resume_attempts( // still owns would strand the durable attempt on a new epoch while the old // task can no longer settle it. let report = if attempt_processes.holds_worktree(&context.scope().worktree_id) { - Err(ApplicationProblem::Conflict { - diagnostic: SafeDiagnostic { + Err(ApplicationProblem::conflict(SafeDiagnostic { code: "application.work-attempt.live-holder".to_owned(), message: "Work attempt recovery requires the current worktree to have no live provider holder." .to_owned(), - }, - retry: tracedecay_contracts::RetryDirective::AfterRevalidate, - legal_actions: vec![tracedecay_contracts::LegalAction::Refresh], - }) + })) } else { services.attempts().resume(context, &command) }; diff --git a/crates/tracedecay-mcp/src/handlers/edit.rs b/crates/tracedecay-mcp/src/handlers/edit.rs index 68ffab992c..3d31aec796 100644 --- a/crates/tracedecay-mcp/src/handlers/edit.rs +++ b/crates/tracedecay-mcp/src/handlers/edit.rs @@ -682,8 +682,7 @@ mod tests { }; use tracedecay_contracts::{ ApplicationInvocation, ApplicationInvocationExecutor, ApplicationInvocationFuture, - ApplicationProblem, ApplicationResponse, InvocationError, LegalAction, RetryDirective, - SafeDiagnostic, + ApplicationProblem, ApplicationResponse, InvocationError, RetryDirective, SafeDiagnostic, }; use tracedecay_daemon_protocol::{ DaemonInvocationError, DaemonInvocationExecutorFuture, DaemonInvocationPayload, @@ -1025,15 +1024,13 @@ mod tests { #[tokio::test] async fn kernel_conflict_reaches_mcp_with_reason_code_and_retryability() { let error = source_edit_refusal(DaemonInvocationOutcome::ApplicationProblem { - problem: ApplicationProblem::Conflict { - diagnostic: SafeDiagnostic::new( + problem: ApplicationProblem::conflict( + SafeDiagnostic::new( "source_edit.idempotency_conflict", "source edit idempotency key conflicts with a prior input", ) .unwrap(), - retry: RetryDirective::AfterRevalidate, - legal_actions: vec![LegalAction::Refresh], - }, + ), }) .await; let (reason_code, retryable, _) = error From 7659d57237cf1082aff1123989f1e6d0faf8cba0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 20:05:57 +0000 Subject: [PATCH 170/182] test(fixtures): share isolated profile harness Daemon journeys copied the home/profile environment, env restore guard, and successful-command check. One harness now owns those. Co-authored-by: Zack Jackson --- crates/tracedecay-cli/src/agent_cmd.rs | 26 +----- .../tracedecay-cli/tests/work_loop_journey.rs | 52 +----------- .../tests/work_route_exposure_conformance.rs | 53 +----------- .../src/service/tests.rs | 38 +-------- .../src/service/update_restore_tests.rs | 32 +------- crates/tracedecay-sdk/tests/sdk_suite/main.rs | 15 ++++ .../tests/sdk_suite/production_daemon.rs | 32 +------- .../tests/sdk_suite/semantic_replay.rs | 32 +------- .../advisory_runtime/scout_journey_tests.rs | 3 +- crates/tracedecay/src/daemon/tests.rs | 29 +------ .../tools/handlers/dispatch_test_support.rs | 30 +------ .../handlers/stack_snapshot_behavior_tests.rs | 4 +- crates/tracedecay/tests/common/mod.rs | 42 +--------- .../daemon_suite/invocation_observability.rs | 2 +- .../api_application_parity.rs | 4 +- .../daemon_runtime_acceptance.rs | 3 +- .../runtime_surface_acceptance.rs | 3 +- .../session_suite/anchor_tombstone_expiry.rs | 12 +-- .../v2_surface_mount_conformance.rs | 15 +--- tests/support/isolated_profile.rs | 80 +++++++++++++++++++ 20 files changed, 142 insertions(+), 365 deletions(-) create mode 100644 tests/support/isolated_profile.rs diff --git a/crates/tracedecay-cli/src/agent_cmd.rs b/crates/tracedecay-cli/src/agent_cmd.rs index 59f9767387..bf1f239b95 100644 --- a/crates/tracedecay-cli/src/agent_cmd.rs +++ b/crates/tracedecay-cli/src/agent_cmd.rs @@ -7,6 +7,9 @@ use tracedecay_session_memory::user_config::UserConfig; mod automation; #[cfg(test)] mod host_cli_fixture; +#[cfg(test)] +#[path = "../../../tests/support/isolated_profile.rs"] +mod isolated_profile; pub(crate) use automation::CodexAutomationInstall; #[cfg(test)] use automation::broker_codex_daemon_automation_project; @@ -1809,7 +1812,6 @@ pub(crate) async fn handle_uninstall_command( #[cfg(test)] mod tests { - use std::ffi::{OsStr, OsString}; use std::path::{Path, PathBuf}; use std::sync::{ Arc, @@ -2427,27 +2429,7 @@ mod tests { } } - struct EnvVarGuard { - key: &'static str, - previous: Option, - } - - impl EnvVarGuard { - fn set(key: &'static str, value: impl AsRef) -> Self { - let previous = std::env::var_os(key); - unsafe { std::env::set_var(key, value) }; - Self { key, previous } - } - } - - impl Drop for EnvVarGuard { - fn drop(&mut self) { - match self.previous.take() { - Some(previous) => unsafe { std::env::set_var(self.key, previous) }, - None => unsafe { std::env::remove_var(self.key) }, - } - } - } + use super::isolated_profile::EnvVarGuard; /// Keep Kiro lifecycle tests on the native `kiro-cli` route. The compiled /// fixture is a real executable so Windows runners do not rename a shell diff --git a/crates/tracedecay-cli/tests/work_loop_journey.rs b/crates/tracedecay-cli/tests/work_loop_journey.rs index 4bd09768c0..d158e47e66 100644 --- a/crates/tracedecay-cli/tests/work_loop_journey.rs +++ b/crates/tracedecay-cli/tests/work_loop_journey.rs @@ -40,7 +40,6 @@ //! actually owns: the forwarded argv, the instructions on stdin, the sealed //! terminal state, and the requested-versus-actual route. -use std::ffi::{OsStr, OsString}; use std::fs; use std::io::Read; use std::path::{Path, PathBuf}; @@ -98,32 +97,9 @@ fn lock_env() -> MutexGuard<'static, ()> { .unwrap_or_else(std::sync::PoisonError::into_inner) } -/// Restores a process environment variable when the guard drops. -struct EnvVarGuard { - key: &'static str, - previous: Option, -} - -impl EnvVarGuard { - fn set(key: &'static str, value: impl AsRef) -> Self { - let previous = std::env::var_os(key); - // The fixture holds the binary-wide environment lock for its whole - // life, so no other thread reads the environment while it is pinned. - unsafe { std::env::set_var(key, value) }; - Self { key, previous } - } -} - -impl Drop for EnvVarGuard { - fn drop(&mut self) { - unsafe { - match self.previous.take() { - Some(previous) => std::env::set_var(self.key, previous), - None => std::env::remove_var(self.key), - } - } - } -} +#[path = "../../../tests/support/isolated_profile.rs"] +mod isolated_profile; +use isolated_profile::{EnvVarGuard, apply_isolated_profile_env, run_ok}; /// A live daemon over a registered project under a throwaway profile, plus the /// credentials it published for its own HTTP application endpoint. @@ -468,28 +444,8 @@ fn daemon_authority_path(profile_root: &Path) -> PathBuf { fn isolated(home: &Path, profile: &Path) -> Command { let mut command = Command::new(env!("CARGO_BIN_EXE_tracedecay")); + apply_isolated_profile_env(&mut command, home, profile); command - .env("HOME", home) - .env("USERPROFILE", home) - .env("XDG_CONFIG_HOME", home.join(".config")) - .env(USER_DATA_DIR_ENV, profile) - .env(GLOBAL_DB_ENV, profile.join("global.db")) - .env("TRACEDECAY_TEST_ALLOW_INCOMPLETE_HOLDER_SCAN", "1"); - command -} - -fn run_ok(command: &mut Command, label: &str) -> Vec { - let output = command - .output() - .unwrap_or_else(|error| panic!("{label} could not run: {error}")); - assert!( - output.status.success(), - "{label} failed with {}\nstdout:\n{}\nstderr:\n{}", - output.status, - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - output.stdout } fn wait_for_authority(daemon: &mut Child, path: &Path) -> Value { diff --git a/crates/tracedecay-cli/tests/work_route_exposure_conformance.rs b/crates/tracedecay-cli/tests/work_route_exposure_conformance.rs index 528d70eb4c..4d7030ac42 100644 --- a/crates/tracedecay-cli/tests/work_route_exposure_conformance.rs +++ b/crates/tracedecay-cli/tests/work_route_exposure_conformance.rs @@ -36,6 +36,7 @@ #[path = "../../tracedecay/tests/common/mod.rs"] mod common; +use common::{EnvVarGuard, apply_isolated_profile_env, run_ok}; #[path = "work_route_exposure_conformance/work_evidence.rs"] mod work_evidence; @@ -45,7 +46,6 @@ mod work_evidence; mod work_task_session; use std::collections::{BTreeMap, BTreeSet}; -use std::ffi::{OsStr, OsString}; use std::fs; use std::io::Read; use std::path::{Path, PathBuf}; @@ -91,35 +91,6 @@ const UNKNOWN_PROJECT_ID: &str = "project.route-exposure-conformance-unknown"; /// Guards against a malformed schema cycle producing an unbounded instance. const MAX_SCHEMA_DEPTH: usize = 32; -/// Restores a process environment variable when the guard drops. -struct EnvVarGuard { - key: &'static str, - previous: Option, -} - -impl EnvVarGuard { - fn set(key: &'static str, value: impl AsRef) -> Self { - let previous = std::env::var_os(key); - // Every test in this binary pins the environment through - // `ProductionDaemon::start`, which holds the shared env lock for the - // fixture's whole life, so no other thread reads the environment while - // it is being pinned. - unsafe { std::env::set_var(key, value) }; - Self { key, previous } - } -} - -impl Drop for EnvVarGuard { - fn drop(&mut self) { - unsafe { - match self.previous.take() { - Some(previous) => std::env::set_var(self.key, previous), - None => std::env::remove_var(self.key), - } - } - } -} - /// A live daemon over a registered project under a throwaway profile, plus the /// credentials it published for its own HTTP application endpoint. struct ProductionDaemon { @@ -410,28 +381,8 @@ fn read_listening_url(stdout: std::process::ChildStdout, process: &mut Child) -> fn isolated(home: &Path, profile: &Path) -> Command { let mut command = Command::new(env!("CARGO_BIN_EXE_tracedecay")); + apply_isolated_profile_env(&mut command, home, profile); command - .env("HOME", home) - .env("USERPROFILE", home) - .env("XDG_CONFIG_HOME", home.join(".config")) - .env(USER_DATA_DIR_ENV, profile) - .env(GLOBAL_DB_ENV, profile.join("global.db")) - .env("TRACEDECAY_TEST_ALLOW_INCOMPLETE_HOLDER_SCAN", "1"); - command -} - -fn run_ok(command: &mut Command, label: &str) -> Vec { - let output = command - .output() - .unwrap_or_else(|error| panic!("{label} could not run: {error}")); - assert!( - output.status.success(), - "{label} failed with {}\nstdout:\n{}\nstderr:\n{}", - output.status, - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - output.stdout } fn wait_for_authority(daemon: &mut Child, path: &Path) -> Value { diff --git a/crates/tracedecay-daemon-control/src/service/tests.rs b/crates/tracedecay-daemon-control/src/service/tests.rs index 8ccb333339..d2d9b745e1 100644 --- a/crates/tracedecay-daemon-control/src/service/tests.rs +++ b/crates/tracedecay-daemon-control/src/service/tests.rs @@ -1,4 +1,3 @@ -use std::ffi::{OsStr, OsString}; use std::io::Write; use std::path::PathBuf; use std::sync::{Arc, Mutex}; @@ -23,40 +22,9 @@ use tracedecay_runtime_core::config::{ const TEST_BUILD_VERSION: &str = "0.1.0-test+service-probe"; -struct EnvVarGuard { - key: &'static str, - previous: Option, -} - -impl EnvVarGuard { - fn set(key: &'static str, value: impl AsRef) -> Self { - let previous = std::env::var_os(key); - unsafe { - std::env::set_var(key, value); - } - Self { key, previous } - } - - fn unset(key: &'static str) -> Self { - let previous = std::env::var_os(key); - unsafe { - std::env::remove_var(key); - } - Self { key, previous } - } -} - -impl Drop for EnvVarGuard { - fn drop(&mut self) { - unsafe { - if let Some(previous) = self.previous.take() { - std::env::set_var(self.key, previous); - } else { - std::env::remove_var(self.key); - } - } - } -} +#[path = "../../../../tests/support/isolated_profile.rs"] +mod isolated_profile; +use isolated_profile::EnvVarGuard; #[cfg(target_os = "linux")] fn systemctl_log_contains_sequence(log: &str, expected: &[&str]) -> bool { diff --git a/crates/tracedecay-daemon-control/src/service/update_restore_tests.rs b/crates/tracedecay-daemon-control/src/service/update_restore_tests.rs index 5a84ee931f..b1f65df980 100644 --- a/crates/tracedecay-daemon-control/src/service/update_restore_tests.rs +++ b/crates/tracedecay-daemon-control/src/service/update_restore_tests.rs @@ -7,8 +7,6 @@ //! and the readiness probe keeps failing closed with a typed identity //! mismatch when the daemon that answers is not the expected version. -#[cfg(unix)] -use std::ffi::{OsStr, OsString}; #[cfg(unix)] use std::io::{BufRead, Write}; #[cfg(unix)] @@ -42,34 +40,10 @@ fn quiesced_guard() -> QuiescedDaemonLifecycle { } #[cfg(unix)] -struct EnvVarGuard { - key: &'static str, - previous: Option, -} - -#[cfg(unix)] -impl EnvVarGuard { - fn set(key: &'static str, value: impl AsRef) -> Self { - let previous = std::env::var_os(key); - unsafe { - std::env::set_var(key, value); - } - Self { key, previous } - } -} - +#[path = "../../../../tests/support/isolated_profile.rs"] +mod isolated_profile; #[cfg(unix)] -impl Drop for EnvVarGuard { - fn drop(&mut self) { - unsafe { - if let Some(previous) = self.previous.take() { - std::env::set_var(self.key, previous); - } else { - std::env::remove_var(self.key); - } - } - } -} +use isolated_profile::EnvVarGuard; #[cfg(unix)] fn serve_initialize_identity( diff --git a/crates/tracedecay-sdk/tests/sdk_suite/main.rs b/crates/tracedecay-sdk/tests/sdk_suite/main.rs index 68726569b7..20a9de5547 100644 --- a/crates/tracedecay-sdk/tests/sdk_suite/main.rs +++ b/crates/tracedecay-sdk/tests/sdk_suite/main.rs @@ -7,7 +7,22 @@ //! `production_daemon::` module by that prefix). `remote_client_proxy` stays //! its own binary because it mutates the process-wide proxy environment. +#[path = "../../../../tests/support/isolated_profile.rs"] +mod isolated_profile; + mod client; mod facade; mod production_daemon; mod semantic_replay; + +fn production_binary() -> std::path::PathBuf { + let path = std::env::var_os("TRACEDECAY_TEST_BIN") + .map(std::path::PathBuf::from) + .unwrap_or_else(|| std::path::PathBuf::from("../../target/debug/tracedecay")); + std::fs::canonicalize(&path) + .unwrap_or_else(|error| panic!("missing production daemon {}: {error}", path.display())) +} + +fn run(command: &mut std::process::Command) -> Vec { + isolated_profile::run_ok(command, "command") +} diff --git a/crates/tracedecay-sdk/tests/sdk_suite/production_daemon.rs b/crates/tracedecay-sdk/tests/sdk_suite/production_daemon.rs index 6e2ead1c76..28b3d9d933 100644 --- a/crates/tracedecay-sdk/tests/sdk_suite/production_daemon.rs +++ b/crates/tracedecay-sdk/tests/sdk_suite/production_daemon.rs @@ -3,7 +3,7 @@ use std::fs; use std::io::{Read, Write}; use std::net::{SocketAddr, TcpStream}; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::process::{Child, Command, Stdio}; use std::sync::Arc; use std::thread; @@ -578,34 +578,8 @@ fn assert_workflow_get_definition_route_conceals_missing_definition(client: &Cli ); } -fn production_binary() -> PathBuf { - let path = std::env::var_os("TRACEDECAY_TEST_BIN") - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from("../../target/debug/tracedecay")); - fs::canonicalize(&path) - .unwrap_or_else(|error| panic!("missing production daemon {}: {error}", path.display())) -} - -fn isolated(command: &mut Command, home: &Path, profile: &Path) { - command - .env("HOME", home) - .env("USERPROFILE", home) - .env("XDG_CONFIG_HOME", home.join(".config")) - .env("TRACEDECAY_DATA_DIR", profile) - .env("TRACEDECAY_GLOBAL_DB", profile.join("global.db")) - .env("TRACEDECAY_TEST_ALLOW_INCOMPLETE_HOLDER_SCAN", "1"); -} - -fn run(command: &mut Command) -> Vec { - let output = command.output().unwrap(); - assert!( - output.status.success(), - "command failed: {}\n{}", - output.status, - String::from_utf8_lossy(&output.stderr) - ); - output.stdout -} +use crate::isolated_profile::apply_isolated_profile_env as isolated; +use crate::{production_binary, run}; fn wait_for_authority(child: &mut Child, path: &Path) -> Value { wait_for_authority_record(child, path, false) diff --git a/crates/tracedecay-sdk/tests/sdk_suite/semantic_replay.rs b/crates/tracedecay-sdk/tests/sdk_suite/semantic_replay.rs index e073ce9bfb..246b922b28 100644 --- a/crates/tracedecay-sdk/tests/sdk_suite/semantic_replay.rs +++ b/crates/tracedecay-sdk/tests/sdk_suite/semantic_replay.rs @@ -1,7 +1,7 @@ #![cfg(unix)] use std::fs; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::process::{Child, Command, Stdio}; use std::thread; use std::time::{Duration, Instant}; @@ -343,34 +343,8 @@ fn application_run_record_count(profile: &Path, run_id: &str) -> usize { .count() } -fn production_binary() -> PathBuf { - let path = std::env::var_os("TRACEDECAY_TEST_BIN") - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from("../../target/debug/tracedecay")); - fs::canonicalize(&path) - .unwrap_or_else(|error| panic!("missing production daemon {}: {error}", path.display())) -} - -fn isolated(command: &mut Command, home: &Path, profile: &Path) { - command - .env("HOME", home) - .env("USERPROFILE", home) - .env("XDG_CONFIG_HOME", home.join(".config")) - .env("TRACEDECAY_DATA_DIR", profile) - .env("TRACEDECAY_GLOBAL_DB", profile.join("global.db")) - .env("TRACEDECAY_TEST_ALLOW_INCOMPLETE_HOLDER_SCAN", "1"); -} - -fn run(command: &mut Command) -> Vec { - let output = command.output().expect("run subprocess"); - assert!( - output.status.success(), - "command failed: {}\n{}", - output.status, - String::from_utf8_lossy(&output.stderr) - ); - output.stdout -} +use crate::isolated_profile::apply_isolated_profile_env as isolated; +use crate::{production_binary, run}; fn wait_for_authority(child: &mut Child, path: &Path, prior_epoch: Option) -> Value { let deadline = Instant::now() + Duration::from_secs(15); diff --git a/crates/tracedecay/src/daemon/project_open_owners/advisory_runtime/scout_journey_tests.rs b/crates/tracedecay/src/daemon/project_open_owners/advisory_runtime/scout_journey_tests.rs index 5a1ec53457..b366b21c85 100644 --- a/crates/tracedecay/src/daemon/project_open_owners/advisory_runtime/scout_journey_tests.rs +++ b/crates/tracedecay/src/daemon/project_open_owners/advisory_runtime/scout_journey_tests.rs @@ -23,8 +23,7 @@ use tracedecay_contracts::{ use tracedecay_domain::configuration::ConfigurationValueV1; use tracedecay_domain::feedback::FeedbackContentIdentityV1; use tracedecay_domain::{ - CodeGenerationId, ComponentVersion, ManifestDigest, ProjectId, RefId, RetrievalAnchorId, - TemporalModeV1, + CodeGenerationId, ComponentVersion, ProjectId, RefId, RetrievalAnchorId, TemporalModeV1, }; use tracedecay_runtime_core::cancellation::CancellationToken; diff --git a/crates/tracedecay/src/daemon/tests.rs b/crates/tracedecay/src/daemon/tests.rs index e4a0ec963c..1a5edc774c 100644 --- a/crates/tracedecay/src/daemon/tests.rs +++ b/crates/tracedecay/src/daemon/tests.rs @@ -288,32 +288,9 @@ fn test_daemon_engine_for_profile(profile_root: &std::path::Path) -> DaemonEngin engine } -struct EnvVarGuard { - key: &'static str, - previous: Option, -} - -impl EnvVarGuard { - fn set(key: &'static str, value: impl AsRef) -> Self { - let previous = std::env::var_os(key); - unsafe { - std::env::set_var(key, value); - } - Self { key, previous } - } -} - -impl Drop for EnvVarGuard { - fn drop(&mut self) { - unsafe { - if let Some(previous) = self.previous.take() { - std::env::set_var(self.key, previous); - } else { - std::env::remove_var(self.key); - } - } - } -} +#[path = "../../../../tests/support/isolated_profile.rs"] +mod isolated_profile; +use isolated_profile::EnvVarGuard; /// Pins the codex app-server launcher to a path that cannot exist so any /// automation tick reached during the test fails with the typed spawn error diff --git a/crates/tracedecay/src/mcp/tools/handlers/dispatch_test_support.rs b/crates/tracedecay/src/mcp/tools/handlers/dispatch_test_support.rs index 506ce3408c..3d2354e710 100644 --- a/crates/tracedecay/src/mcp/tools/handlers/dispatch_test_support.rs +++ b/crates/tracedecay/src/mcp/tools/handlers/dispatch_test_support.rs @@ -1,5 +1,4 @@ use std::collections::BTreeSet; -use std::ffi::{OsStr, OsString}; use std::path::Path; use std::sync::Arc; @@ -305,32 +304,9 @@ pub(super) async fn init_sibling_registered_fixture( (graph, sibling) } -struct EnvVarGuard { - key: &'static str, - previous: Option, -} - -impl EnvVarGuard { - fn set(key: &'static str, value: impl AsRef) -> Self { - let previous = std::env::var_os(key); - unsafe { - std::env::set_var(key, value); - } - Self { key, previous } - } -} - -impl Drop for EnvVarGuard { - fn drop(&mut self) { - unsafe { - if let Some(previous) = self.previous.take() { - std::env::set_var(self.key, previous); - } else { - std::env::remove_var(self.key); - } - } - } -} +#[path = "../../../../../../tests/support/isolated_profile.rs"] +mod isolated_profile; +use isolated_profile::EnvVarGuard; pub(super) struct SelectorEnv { _home: EnvVarGuard, diff --git a/crates/tracedecay/src/mcp/tools/handlers/stack_snapshot_behavior_tests.rs b/crates/tracedecay/src/mcp/tools/handlers/stack_snapshot_behavior_tests.rs index 6dd4266c3c..79d1e29695 100644 --- a/crates/tracedecay/src/mcp/tools/handlers/stack_snapshot_behavior_tests.rs +++ b/crates/tracedecay/src/mcp/tools/handlers/stack_snapshot_behavior_tests.rs @@ -17,8 +17,8 @@ use tracedecay_contracts::{ native_integration_surface_operation, }; use tracedecay_domain::{ - ActorId, ManifestDigest, ProjectId, RefId, RepositoryId, ScopeSetId, ScopeSetRevision, - UtcMicros, WorktreeId, WorktreeInventoryEpoch, WorktreeInventorySnapshotId, + ActorId, ProjectId, RefId, RepositoryId, ScopeSetId, ScopeSetRevision, UtcMicros, WorktreeId, + WorktreeInventoryEpoch, WorktreeInventorySnapshotId, }; use tracedecay_mcp::McpTransport; use tracedecay_runtime_core::git::try_git_program; diff --git a/crates/tracedecay/tests/common/mod.rs b/crates/tracedecay/tests/common/mod.rs index 08bed31dcc..dd5f9713d5 100644 --- a/crates/tracedecay/tests/common/mod.rs +++ b/crates/tracedecay/tests/common/mod.rs @@ -117,44 +117,10 @@ pub async fn open_test_database( Database::publish_test_runtime(path, &authority, TestDatabaseRuntimeMode::Existing).await } -/// Sets (or removes) an environment variable for its lifetime, restoring the -/// previous value on drop. -pub struct EnvVarGuard { - key: &'static str, - previous: Option, -} - -impl EnvVarGuard { - pub fn set(key: &'static str, value: impl AsRef) -> Self { - let previous = std::env::var_os(key); - unsafe { - std::env::set_var(key, value); - } - Self { key, previous } - } - - /// Removes `key` for the guard's lifetime, so tests can exercise the - /// no-override path. - pub fn unset(key: &'static str) -> Self { - let previous = std::env::var_os(key); - unsafe { - std::env::remove_var(key); - } - Self { key, previous } - } -} - -impl Drop for EnvVarGuard { - fn drop(&mut self) { - unsafe { - if let Some(previous) = self.previous.take() { - std::env::set_var(self.key, previous); - } else { - std::env::remove_var(self.key); - } - } - } -} +#[path = "../../../../tests/support/isolated_profile.rs"] +mod isolated_profile; +#[allow(unused_imports)] // each suite binary uses a subset +pub use isolated_profile::{EnvVarGuard, apply_isolated_profile_env, run_ok}; /// Query lanes a terminal code-index answer must report as `"complete"`. /// Daemon journeys and the MCP readiness wait share this set. diff --git a/crates/tracedecay/tests/daemon_suite/invocation_observability.rs b/crates/tracedecay/tests/daemon_suite/invocation_observability.rs index 43c2956650..47124287e6 100644 --- a/crates/tracedecay/tests/daemon_suite/invocation_observability.rs +++ b/crates/tracedecay/tests/daemon_suite/invocation_observability.rs @@ -16,7 +16,7 @@ use tracedecay_daemon_service::{ }; use tracedecay_domain::{ CoverageStateV1, DeliveryChannelIdentityV1, DeliveryEventClassV1, DeliverySettlementAttemptV1, - DeliverySettlementOutcomeV1, DeliverySettlementV1, DeliverySurfaceFamilyV1, ManifestDigest, + DeliverySettlementOutcomeV1, DeliverySettlementV1, DeliverySurfaceFamilyV1, ObservabilityEnvelopeV1, ObservabilityPayloadV1, ObservabilityRetentionClassV1, ObservabilityTerminalResultV1, ProjectId, RepositoryId, RetrievalQueryObservedV1, UtcMicros, WorktreeId, canonical_sha256, diff --git a/crates/tracedecay/tests/product_surface_suite/api_application_parity.rs b/crates/tracedecay/tests/product_surface_suite/api_application_parity.rs index 383bd019bf..c7885901ce 100644 --- a/crates/tracedecay/tests/product_surface_suite/api_application_parity.rs +++ b/crates/tracedecay/tests/product_surface_suite/api_application_parity.rs @@ -26,8 +26,8 @@ use tracedecay_daemon_service::application_surface::{ use tracedecay_domain::{ GitCommitIdentityV1, GitCoverageV1, GitDiffScopeV1, GitHeadStateV1, GitIndexCommitIntentV1, GitIndexPreviewDispositionV1, GitIndexPreviewId, GitIndexPreviewV1, GitIndexSigningPolicyV1, - GitIndexTransactionOperationV1, GitObjectFormatV1, GitOidV1, ManifestDigest, ProjectId, - RepositoryId, RepositoryIndexSnapshotV1, RepositoryIndexStateV1, RepositoryStateSnapshotV1, + GitIndexTransactionOperationV1, GitObjectFormatV1, GitOidV1, ProjectId, RepositoryId, + RepositoryIndexSnapshotV1, RepositoryIndexStateV1, RepositoryStateSnapshotV1, RepositoryWorkingTreeSnapshotV1, RepositoryWorkingTreeStateV1, UtcMicros, WorktreeId, }; use tracedecay_mcp::tools::dispatch::resolve_mcp_application_surface_dispatch; diff --git a/crates/tracedecay/tests/runtime_acceptance_suite/daemon_runtime_acceptance.rs b/crates/tracedecay/tests/runtime_acceptance_suite/daemon_runtime_acceptance.rs index 2354a935a3..5cd9274f40 100644 --- a/crates/tracedecay/tests/runtime_acceptance_suite/daemon_runtime_acceptance.rs +++ b/crates/tracedecay/tests/runtime_acceptance_suite/daemon_runtime_acceptance.rs @@ -24,8 +24,7 @@ use tracedecay_contracts::{ }; use tracedecay_domain::feedback::{FeedbackContentIdentityV1, FeedbackScopeV1}; use tracedecay_domain::{ - CodeGenerationId, ComponentVersion, ManifestDigest, RefId, RetrievalAnchorId, TemporalModeV1, - UtcMicros, + CodeGenerationId, ComponentVersion, RefId, RetrievalAnchorId, TemporalModeV1, UtcMicros, }; use tracedecay_hooks::{ HookConfigurationFileReaderV1, HookConfigurationReadOutcomeV1, HookConfigurationSubscriberV1, diff --git a/crates/tracedecay/tests/runtime_acceptance_suite/runtime_surface_acceptance.rs b/crates/tracedecay/tests/runtime_acceptance_suite/runtime_surface_acceptance.rs index 337ed9d97e..036556beae 100644 --- a/crates/tracedecay/tests/runtime_acceptance_suite/runtime_surface_acceptance.rs +++ b/crates/tracedecay/tests/runtime_acceptance_suite/runtime_surface_acceptance.rs @@ -58,8 +58,7 @@ use tracedecay_domain::configuration::{ AuthorityRef, ConfigurationRevisionId, ScopeSourceBinding, SourceBindingId, SourceKindV1, }; use tracedecay_domain::{ - ActorId, CommitId, LocatorDigest, ManifestDigest, ProjectId, RefId, RepositoryId, UtcMicros, - WorktreeId, + ActorId, CommitId, LocatorDigest, ProjectId, RefId, RepositoryId, UtcMicros, WorktreeId, }; #[cfg(all(unix, feature = "test-transport"))] use tracedecay_domain::{ diff --git a/crates/tracedecay/tests/session_suite/anchor_tombstone_expiry.rs b/crates/tracedecay/tests/session_suite/anchor_tombstone_expiry.rs index f680db7daf..9999341391 100644 --- a/crates/tracedecay/tests/session_suite/anchor_tombstone_expiry.rs +++ b/crates/tracedecay/tests/session_suite/anchor_tombstone_expiry.rs @@ -7,12 +7,12 @@ use std::sync::Arc; use tempfile::TempDir; use tracedecay_domain::{ AccessPolicyDigest, AnchorDurabilityClass, AnchorSourceGenerationV2, CapabilityId, - ComponentVersion, Confidence, CoverageReportV1, DomainError, EntityId, EntityKind, EntityRef, - EvidenceClass, FactAssertionKindV1, FactAssertionV1, FactCategoryV1, FactEventId, - FactEvidenceRefV1, FactEvidenceRelationV1, FactId, FactIdentityMaterialV1, - FactIdentitySourceV1, FactLineageEventKindV1, FactLineageEventV1, FactOwnerV1, FactPayloadV1, - ObservationScopeV1, PayloadAccessState, PayloadReferenceV1, PrivacyDomainBoundLocatorDigest, - PrivacyDomainId, ProjectId, ProjectionGenerationId, ResolutionAuthorizationV1, RetentionClass, + ComponentVersion, Confidence, CoverageReportV1, EntityId, EntityKind, EntityRef, EvidenceClass, + FactAssertionKindV1, FactAssertionV1, FactCategoryV1, FactEventId, FactEvidenceRefV1, + FactEvidenceRelationV1, FactId, FactIdentityMaterialV1, FactIdentitySourceV1, + FactLineageEventKindV1, FactLineageEventV1, FactOwnerV1, FactPayloadV1, ObservationScopeV1, + PayloadAccessState, PayloadReferenceV1, PrivacyDomainBoundLocatorDigest, PrivacyDomainId, + ProjectId, ProjectionGenerationId, ResolutionAuthorizationV1, RetentionClass, RetrievalAnchorId, RetrievalAnchorRecordV2, RetrievalAnchorRecordV2Parts, RetrievalAnchorTargetV2, SanitizationReceiptId, SanitizationReceiptRefV1, SanitizationReceiptV1, SanitizerDispositionV1, ScopeResolutionId, SensitivityV1, UtcMicros, diff --git a/crates/tracedecay/tests/transport_acceptance_suite/v2_surface_mount_conformance.rs b/crates/tracedecay/tests/transport_acceptance_suite/v2_surface_mount_conformance.rs index 7b872770f3..38163ea872 100644 --- a/crates/tracedecay/tests/transport_acceptance_suite/v2_surface_mount_conformance.rs +++ b/crates/tracedecay/tests/transport_acceptance_suite/v2_surface_mount_conformance.rs @@ -39,6 +39,7 @@ //! product call. use crate::common; +use crate::common::run_ok; use std::collections::{BTreeMap, BTreeSet}; use std::fs; @@ -259,20 +260,6 @@ fn isolated_command(home: &Path) -> Command { command } -fn run_ok(command: &mut Command, label: &str) -> Vec { - let output = command - .output() - .unwrap_or_else(|error| panic!("{label} could not run: {error}")); - assert!( - output.status.success(), - "{label} failed with {}\nstdout:\n{}\nstderr:\n{}", - output.status, - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - output.stdout -} - fn wait_for_http_authority(path: &Path) -> Value { common::poll_until( Instant::now() + Duration::from_secs(90), diff --git a/tests/support/isolated_profile.rs b/tests/support/isolated_profile.rs new file mode 100644 index 0000000000..78b99eb8fa --- /dev/null +++ b/tests/support/isolated_profile.rs @@ -0,0 +1,80 @@ +#![allow(dead_code)] // each suite uses a subset of this shared harness + +//! Isolated profile environment shared by crate integration suites. +//! +//! The shipped binary must not see the operator's `HOME` or profile. Suites +//! used to copy the same environment set, success check, and env restore. + +use std::ffi::{OsStr, OsString}; +use std::path::Path; +use std::process::Command; + +/// Restores one process environment variable when the guard drops. +pub struct EnvVarGuard { + key: &'static str, + previous: Option, +} + +impl EnvVarGuard { + pub fn set(key: &'static str, value: impl AsRef) -> Self { + let previous = std::env::var_os(key); + // SAFETY: callers that pin process-wide env hold that binary's env + // lock for the guard's whole life. + unsafe { + std::env::set_var(key, value); + } + Self { key, previous } + } + + /// Removes `key` for the guard's lifetime, so tests can exercise the + /// no-override path. + pub fn unset(key: &'static str) -> Self { + let previous = std::env::var_os(key); + unsafe { + std::env::remove_var(key); + } + Self { key, previous } + } +} + +impl Drop for EnvVarGuard { + fn drop(&mut self) { + unsafe { + if let Some(previous) = self.previous.take() { + std::env::set_var(self.key, previous); + } else { + std::env::remove_var(self.key); + } + } + } +} + +/// Points a child process at a throwaway home and profile. +/// +/// This is the command-env subset shared by daemon journeys. It does not +/// detach the process group or pin `XDG_RUNTIME_DIR`; callers that need the +/// full hermetic daemon environment still use `apply_tracedecay_home_env`. +pub fn apply_isolated_profile_env(command: &mut Command, home: &Path, profile: &Path) { + command + .env("HOME", home) + .env("USERPROFILE", home) + .env("XDG_CONFIG_HOME", home.join(".config")) + .env("TRACEDECAY_DATA_DIR", profile) + .env("TRACEDECAY_GLOBAL_DB", profile.join("global.db")) + .env("TRACEDECAY_TEST_ALLOW_INCOMPLETE_HOLDER_SCAN", "1"); +} + +/// Runs a command and returns stdout, panicking with both streams on failure. +pub fn run_ok(command: &mut Command, label: &str) -> Vec { + let output = command + .output() + .unwrap_or_else(|error| panic!("{label} could not run: {error}")); + assert!( + output.status.success(), + "{label} failed with {}\nstdout:\n{}\nstderr:\n{}", + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + output.stdout +} From 0aac97f2f1afaa322ca850fff27277fd0276bfba Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 20:09:28 +0000 Subject: [PATCH 171/182] simplify(pass-4/5): share delayed-retry saturated constructor Co-authored-by: Zack Jackson --- .../src/operation_stream.rs | 12 ++++------ .../src/primitives/runtime.rs | 14 ++++------- .../src/remote/protocol.rs | 12 ++++------ .../src/result/problem.rs | 9 ++++++++ .../src/result/problem/tests.rs | 23 +++++++++++++++++++ .../src/retained_surfaces/service.rs | 12 ++++------ .../src/work_attempt/problem.rs | 8 ++----- .../src/work_attempt/product_admission.rs | 10 ++++---- .../application_surface/operation_events.rs | 12 ++++------ .../tracedecay/src/daemon/core_admission.rs | 14 ++++------- .../tracedecay/src/daemon/http_application.rs | 8 ++----- 11 files changed, 66 insertions(+), 68 deletions(-) diff --git a/crates/tracedecay-application/src/operation_stream.rs b/crates/tracedecay-application/src/operation_stream.rs index f31d66829a..08a7b43fcb 100644 --- a/crates/tracedecay-application/src/operation_stream.rs +++ b/crates/tracedecay-application/src/operation_stream.rs @@ -278,14 +278,10 @@ impl OperationEventError { "The requested operation-event frontier is invalid", )?), Self::RequestNotAdmitted => ApplicationProblem::timed_out_before_admission(), - Self::Saturated => ApplicationProblem::Saturated { - diagnostic: SafeDiagnostic::new( - "operation_event.saturated", - "Operation-event capacity is temporarily saturated", - )?, - retry: RetryDirective::AfterDelay, - legal_actions: vec![LegalAction::Retry], - }, + Self::Saturated => ApplicationProblem::saturated(SafeDiagnostic::new( + "operation_event.saturated", + "Operation-event capacity is temporarily saturated", + )?), // Permanently invalid input: the same request can never succeed, so // the client must correct it rather than retry. Self::InvalidContext(_) diff --git a/crates/tracedecay-application/src/primitives/runtime.rs b/crates/tracedecay-application/src/primitives/runtime.rs index 43451aa35b..73547d17a8 100644 --- a/crates/tracedecay-application/src/primitives/runtime.rs +++ b/crates/tracedecay-application/src/primitives/runtime.rs @@ -31,7 +31,7 @@ use tracedecay_contracts::{ ApplicationProblem, ApplicationProblemEnvelope, ApplicationResult, AuthorityReceipt, CancellationContext, CancellationObservation, CancellationStage, CapabilityGrantId, CapabilityGrantSnapshot, CoverageCompleteness, CoverageDomainState, Deadline, DisclosureClass, - EvidenceCoverage, EvidenceDomain, EvidencePacket, FreshnessState, LegalAction, OmissionReason, + EvidenceCoverage, EvidenceDomain, EvidencePacket, FreshnessState, OmissionReason, OpaqueCursor, OperationBudgetUsage, OperationReceipt, OperationTermination, PageCursor, PageRequest, PageState, PolicyDecisionRef, RequestAdmission, RequestContext, RequestId, ResolvedScope, RetrievalEvidence, RetryDirective, SafeDiagnostic, TemporalState, @@ -1846,14 +1846,10 @@ fn saturated( problem( context, operation, - ApplicationProblem::Saturated { - diagnostic: SafeDiagnostic::new( - "application.retrieval.saturated", - "The admitted primitive authority has reached its bounded capacity.", - )?, - retry: RetryDirective::AfterDelay, - legal_actions: vec![LegalAction::Retry], - }, + ApplicationProblem::saturated(SafeDiagnostic::new( + "application.retrieval.saturated", + "The admitted primitive authority has reached its bounded capacity.", + )?), ) } diff --git a/crates/tracedecay-contracts/src/remote/protocol.rs b/crates/tracedecay-contracts/src/remote/protocol.rs index b5cef4c62d..61401ff996 100644 --- a/crates/tracedecay-contracts/src/remote/protocol.rs +++ b/crates/tracedecay-contracts/src/remote/protocol.rs @@ -561,14 +561,10 @@ pub fn remote_protocol_problem( "Offline capture is rejected while the owning authority is reachable", )?) } - RemoteProtocolFailureV1::SpoolSaturated => ApplicationProblem::Saturated { - diagnostic: safe_diagnostic( - "remote.spool_saturated", - "The remote offline-capture spool has no remaining capacity", - )?, - retry: RetryDirective::AfterDelay, - legal_actions: vec![LegalAction::Retry], - }, + RemoteProtocolFailureV1::SpoolSaturated => ApplicationProblem::saturated(safe_diagnostic( + "remote.spool_saturated", + "The remote offline-capture spool has no remaining capacity", + )?), RemoteProtocolFailureV1::AuthorityUnavailable => ApplicationProblem::Unavailable { classification: crate::ApplicationUnavailableClassV1::Authority, diagnostic: safe_diagnostic( diff --git a/crates/tracedecay-contracts/src/result/problem.rs b/crates/tracedecay-contracts/src/result/problem.rs index 0b80375eff..a6f7a3a115 100644 --- a/crates/tracedecay-contracts/src/result/problem.rs +++ b/crates/tracedecay-contracts/src/result/problem.rs @@ -830,6 +830,15 @@ impl ApplicationProblem { } } + /// Capacity refusal. Retry only after a delay. + pub fn saturated(diagnostic: SafeDiagnostic) -> Self { + Self::Saturated { + diagnostic, + retry: RetryDirective::AfterDelay, + legal_actions: vec![LegalAction::Retry], + } + } + /// Conflict the caller resolves by refreshing. Retries only after revalidate. pub fn conflict(diagnostic: SafeDiagnostic) -> Self { Self::Conflict { diff --git a/crates/tracedecay-contracts/src/result/problem/tests.rs b/crates/tracedecay-contracts/src/result/problem/tests.rs index 469847f59b..606b1f13e1 100644 --- a/crates/tracedecay-contracts/src/result/problem/tests.rs +++ b/crates/tracedecay-contracts/src/result/problem/tests.rs @@ -46,6 +46,29 @@ fn invalid_request_without_action_offers_no_recovery() { assert!(problem.legal_actions().is_empty()); } +#[test] +fn saturated_retries_after_delay() { + let diagnostic = SafeDiagnostic { + code: "application.saturated".to_owned(), + message: "The authority has no remaining capacity.".to_owned(), + }; + let problem = ApplicationProblem::saturated(diagnostic.clone()); + + assert_eq!( + problem, + ApplicationProblem::Saturated { + diagnostic, + retry: RetryDirective::AfterDelay, + legal_actions: vec![LegalAction::Retry], + } + ); + assert_eq!( + problem.safe_message(), + "The authority has no remaining capacity." + ); + assert_eq!(problem.reason_code(), "application.saturated"); +} + #[test] fn conflict_retries_only_after_revalidate_and_refresh() { let diagnostic = SafeDiagnostic { diff --git a/crates/tracedecay-contracts/src/retained_surfaces/service.rs b/crates/tracedecay-contracts/src/retained_surfaces/service.rs index b0beacfe3f..ec11e19be1 100644 --- a/crates/tracedecay-contracts/src/retained_surfaces/service.rs +++ b/crates/tracedecay-contracts/src/retained_surfaces/service.rs @@ -570,14 +570,10 @@ pub fn retained_surface_execution_problem( retry: RetryDirective::Never, legal_actions: vec![LegalAction::CorrectRequest], }, - RetainedSurfaceExecutionErrorV1::Saturated => ApplicationProblem::Saturated { - diagnostic: diagnostic( - "application.retained.saturated", - "The retained authority cannot admit more work right now.", - ), - retry: RetryDirective::AfterDelay, - legal_actions: vec![LegalAction::Retry], - }, + RetainedSurfaceExecutionErrorV1::Saturated => ApplicationProblem::saturated(diagnostic( + "application.retained.saturated", + "The retained authority cannot admit more work right now.", + )), // Structural budget refusal uses InvalidRequest so the wire kind stays // unchanged and non-retryable. Callers must narrow scope or limit. RetainedSurfaceExecutionErrorV1::Unavailable { detail } => { diff --git a/crates/tracedecay-contracts/src/work_attempt/problem.rs b/crates/tracedecay-contracts/src/work_attempt/problem.rs index 2d4781bbc3..36576e9202 100644 --- a/crates/tracedecay-contracts/src/work_attempt/problem.rs +++ b/crates/tracedecay-contracts/src/work_attempt/problem.rs @@ -23,15 +23,11 @@ pub(super) fn storage_problem(error: WorkAttemptStorageError) -> ApplicationProb code: ("application.work-attempt.fence-conflict").to_owned(), message: ("The Work attempt lease fence changed after this transition was prepared.").to_owned(), }), - WorkAttemptStorageError::CapacityExceeded => ApplicationProblem::Saturated { - diagnostic: SafeDiagnostic { + WorkAttemptStorageError::CapacityExceeded => ApplicationProblem::saturated(SafeDiagnostic { code: "application.work-attempt.capacity-exhausted".to_owned(), message: "The registered Work topology has no parallel attempt capacity." .to_owned(), - }, - retry: RetryDirective::AfterDelay, - legal_actions: vec![LegalAction::Retry], - }, + }), WorkAttemptStorageError::Unavailable => ApplicationProblem::unavailable(SafeDiagnostic { code: "application.work-attempt.storage-unavailable".to_owned(), message: "The Work attempt authority is unavailable.".to_owned(), diff --git a/crates/tracedecay-contracts/src/work_attempt/product_admission.rs b/crates/tracedecay-contracts/src/work_attempt/product_admission.rs index be99f3d69c..916508a5bf 100644 --- a/crates/tracedecay-contracts/src/work_attempt/product_admission.rs +++ b/crates/tracedecay-contracts/src/work_attempt/product_admission.rs @@ -235,15 +235,13 @@ pub(crate) fn product_admission_problem( .to_owned(), }) } - WorkProductAttemptAdmissionErrorV1::CapacityExceeded => ApplicationProblem::Saturated { - diagnostic: crate::SafeDiagnostic { + WorkProductAttemptAdmissionErrorV1::CapacityExceeded => { + ApplicationProblem::saturated(crate::SafeDiagnostic { code: "application.work-attempt.capacity-exhausted".to_owned(), message: "The registered Work topology has no parallel attempt capacity." .to_owned(), - }, - retry: crate::RetryDirective::AfterDelay, - legal_actions: vec![crate::LegalAction::Retry], - }, + }) + } WorkProductAttemptAdmissionErrorV1::Unavailable => { ApplicationProblem::unavailable(crate::SafeDiagnostic { code: "application.work-attempt.product-admission-unavailable".to_owned(), diff --git a/crates/tracedecay-daemon-service/src/application_surface/operation_events.rs b/crates/tracedecay-daemon-service/src/application_surface/operation_events.rs index bae74ea533..cf668ac377 100644 --- a/crates/tracedecay-daemon-service/src/application_surface/operation_events.rs +++ b/crates/tracedecay-daemon-service/src/application_surface/operation_events.rs @@ -840,14 +840,10 @@ pub(super) fn operation_event_problem( retry: RetryDirective::Never, legal_actions: Vec::new(), }, - OperationEventError::Saturated => ApplicationProblem::Saturated { - diagnostic: SafeDiagnostic { - code: "operation_event.saturated".to_owned(), - message: "Operation-event capacity is temporarily saturated".to_owned(), - }, - retry: RetryDirective::AfterDelay, - legal_actions: vec![LegalAction::Retry], - }, + OperationEventError::Saturated => ApplicationProblem::saturated(SafeDiagnostic { + code: "operation_event.saturated".to_owned(), + message: "Operation-event capacity is temporarily saturated".to_owned(), + }), // Permanently invalid input: the same request can never succeed, so the // client must correct it rather than retry. OperationEventError::InvalidContext(_) diff --git a/crates/tracedecay/src/daemon/core_admission.rs b/crates/tracedecay/src/daemon/core_admission.rs index f2bdd1d2eb..5a3d6a7872 100644 --- a/crates/tracedecay/src/daemon/core_admission.rs +++ b/crates/tracedecay/src/daemon/core_admission.rs @@ -15,7 +15,7 @@ use super::{ binary_version, classify_mcp_method, parse_daemon_invocation_request, read_line_handling_wire_oversized, write_json_rpc_response, }; -use tracedecay_contracts::{ApplicationProblem, LegalAction, RetryDirective, SafeDiagnostic}; +use tracedecay_contracts::{ApplicationProblem, SafeDiagnostic}; use tracedecay_daemon_protocol::DAEMON_SHUTDOWN_METHOD; use tracedecay_mcp::ErrorCode; use tracedecay_runtime_core::logging::log_daemon_event; @@ -459,14 +459,10 @@ fn invocation_saturation_response( }; Some(super::DaemonInvocationResponse::application_problem( request.request_id, - ApplicationProblem::Saturated { - diagnostic: SafeDiagnostic { - code: code.to_owned(), - message: "The owning TraceDecay daemon has no request capacity".to_owned(), - }, - retry: RetryDirective::AfterDelay, - legal_actions: vec![LegalAction::Retry], - }, + ApplicationProblem::saturated(SafeDiagnostic { + code: code.to_owned(), + message: "The owning TraceDecay daemon has no request capacity".to_owned(), + }), )) } diff --git a/crates/tracedecay/src/daemon/http_application.rs b/crates/tracedecay/src/daemon/http_application.rs index 543477933f..6128e0469d 100644 --- a/crates/tracedecay/src/daemon/http_application.rs +++ b/crates/tracedecay/src/daemon/http_application.rs @@ -29,7 +29,7 @@ use tower::ServiceExt; use tracedecay_contracts::remote::auth::RemoteEnrollmentAdmissionEvidenceV1; use tracedecay_contracts::remote::status::RemoteOperationalStatusReadV1; use tracedecay_contracts::{ - APPLICATION_REQUEST_ID_HEADER, ApplicationProblem, LegalAction, RequestId, RetryDirective, + APPLICATION_REQUEST_ID_HEADER, ApplicationProblem, RequestId, RetryDirective, SafeDiagnostic, }; use tracedecay_daemon_control::RemoteBrainTlsConfig; @@ -587,11 +587,7 @@ fn project_router_problem_response( ) else { return StatusCode::INTERNAL_SERVER_ERROR.into_response(); }; - ApplicationProblem::Saturated { - diagnostic, - retry: RetryDirective::AfterDelay, - legal_actions: vec![LegalAction::Retry], - } + ApplicationProblem::saturated(diagnostic) } ProjectRouterProblem::TimedOut => ApplicationProblem::timed_out_before_admission(), ProjectRouterProblem::Unavailable => { From a945cd4302ed38e9d37ddc00a6f10283a3177f18 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 20:16:33 +0000 Subject: [PATCH 172/182] simplify(pass-5/5): share canonical application HTTP status Co-authored-by: Zack Jackson --- crates/tracedecay-api/src/configuration.rs | 20 ++----- crates/tracedecay-api/src/http.rs | 2 +- crates/tracedecay-api/src/http/tests.rs | 60 +++++++++++++++++-- crates/tracedecay-api/src/lib.rs | 7 ++- crates/tracedecay-api/src/remote.rs | 19 +----- .../src/automation_authority.rs | 23 +------ crates/tracedecay-sdk/src/remote_client.rs | 17 +----- 7 files changed, 70 insertions(+), 78 deletions(-) diff --git a/crates/tracedecay-api/src/configuration.rs b/crates/tracedecay-api/src/configuration.rs index 4db256c463..ba3f7edaab 100644 --- a/crates/tracedecay-api/src/configuration.rs +++ b/crates/tracedecay-api/src/configuration.rs @@ -218,20 +218,12 @@ pub fn configuration_authority_unavailable_error() -> DashboardConfigurationRout pub fn configuration_application_problem_error( problem: ApplicationProblemEnvelope, ) -> DashboardConfigurationRouteErrorV1 { - let status = match problem.problem.kind { - ApplicationProblemKind::InvalidRequest => StatusCode::BAD_REQUEST, - ApplicationProblemKind::NotFoundOrNotAuthorized => StatusCode::NOT_FOUND, - ApplicationProblemKind::Conflict - | ApplicationProblemKind::PartialEffect - | ApplicationProblemKind::Stale => StatusCode::CONFLICT, - ApplicationProblemKind::Unsupported => StatusCode::UNPROCESSABLE_ENTITY, - ApplicationProblemKind::ResetRequired | ApplicationProblemKind::Unavailable => { - StatusCode::SERVICE_UNAVAILABLE - } - ApplicationProblemKind::ExecutionFailed => StatusCode::INTERNAL_SERVER_ERROR, - ApplicationProblemKind::Saturated => StatusCode::TOO_MANY_REQUESTS, - ApplicationProblemKind::Cancelled => StatusCode::CONFLICT, - ApplicationProblemKind::TimedOut => StatusCode::GATEWAY_TIMEOUT, + // Cancellation stays a conflict on this historic configuration route. + // Every other kind uses the canonical application status. + let status = if problem.problem.kind == ApplicationProblemKind::Cancelled { + StatusCode::CONFLICT + } else { + crate::application_problem_status(problem.problem.kind) }; let payload = serde_json::to_value(problem) .unwrap_or_else(|_| json!({ "detail": "configuration mutation was rejected" })); diff --git a/crates/tracedecay-api/src/http.rs b/crates/tracedecay-api/src/http.rs index 6dfb4d36f5..411ca894c4 100644 --- a/crates/tracedecay-api/src/http.rs +++ b/crates/tracedecay-api/src/http.rs @@ -334,7 +334,7 @@ where } } -fn application_problem_status(kind: ApplicationProblemKind) -> StatusCode { +pub fn application_problem_status(kind: ApplicationProblemKind) -> StatusCode { match kind { ApplicationProblemKind::InvalidRequest => StatusCode::BAD_REQUEST, ApplicationProblemKind::NotFoundOrNotAuthorized => StatusCode::NOT_FOUND, diff --git a/crates/tracedecay-api/src/http/tests.rs b/crates/tracedecay-api/src/http/tests.rs index 5d8ef74d92..ce175ad696 100644 --- a/crates/tracedecay-api/src/http/tests.rs +++ b/crates/tracedecay-api/src/http/tests.rs @@ -1,17 +1,67 @@ use std::collections::BTreeSet; +use axum::http::StatusCode; + use super::{ - HttpApplicationOwnerKind, http_application_full_route_path, http_application_owner_kind, - is_http_application_operation_exposed, parse_callable_code_operation, - parse_configuration_operation, parse_context_scout_operation, parse_feedback_read_operation, - parse_git_read_operation, parse_native_integration_operation, + HttpApplicationOwnerKind, application_problem_status, http_application_full_route_path, + http_application_owner_kind, is_http_application_operation_exposed, + parse_callable_code_operation, parse_configuration_operation, parse_context_scout_operation, + parse_feedback_read_operation, parse_git_read_operation, parse_native_integration_operation, }; use tracedecay_contracts::{ - application_http_executable_binding_registry, + ApplicationProblemKind, application_http_executable_binding_registry, configuration::configuration_surface_operation_names, }; use tracedecay_tool_catalog::{ApplicationSurfaceOperation, OperationId, RouteExposureV1}; +#[test] +fn application_problem_status_keeps_the_canonical_http_codes() { + let cases = [ + ( + ApplicationProblemKind::InvalidRequest, + StatusCode::BAD_REQUEST, + ), + ( + ApplicationProblemKind::NotFoundOrNotAuthorized, + StatusCode::NOT_FOUND, + ), + (ApplicationProblemKind::Conflict, StatusCode::CONFLICT), + (ApplicationProblemKind::PartialEffect, StatusCode::CONFLICT), + (ApplicationProblemKind::Stale, StatusCode::CONFLICT), + ( + ApplicationProblemKind::Unsupported, + StatusCode::UNPROCESSABLE_ENTITY, + ), + ( + ApplicationProblemKind::ResetRequired, + StatusCode::SERVICE_UNAVAILABLE, + ), + ( + ApplicationProblemKind::Unavailable, + StatusCode::SERVICE_UNAVAILABLE, + ), + ( + ApplicationProblemKind::ExecutionFailed, + StatusCode::INTERNAL_SERVER_ERROR, + ), + ( + ApplicationProblemKind::Saturated, + StatusCode::TOO_MANY_REQUESTS, + ), + ( + ApplicationProblemKind::Cancelled, + StatusCode::REQUEST_TIMEOUT, + ), + ( + ApplicationProblemKind::TimedOut, + StatusCode::GATEWAY_TIMEOUT, + ), + ]; + for (kind, status) in cases { + assert_eq!(application_problem_status(kind), status, "{kind:?}"); + } +} + #[test] fn git_read_operation_parser_is_exact_and_read_only() { for (route, operation) in [ diff --git a/crates/tracedecay-api/src/lib.rs b/crates/tracedecay-api/src/lib.rs index 87dcae89d2..ed07e10283 100644 --- a/crates/tracedecay-api/src/lib.rs +++ b/crates/tracedecay-api/src/lib.rs @@ -45,9 +45,10 @@ pub use handoff::{ pub use http::{ HttpApplicationControls, HttpApplicationInvocationFuture, HttpApplicationOwnerKind, HttpApplicationOwners, HttpApplicationRequest, HttpRouteDocumentV1, adapter_problem_response, - application_problem_response, application_router, configuration_application_router, - feedback_application_router, http_application_full_route_path, http_application_owner_kind, - http_application_route_path, http_route_documents, is_http_application_operation_exposed, + application_problem_response, application_problem_status, application_router, + configuration_application_router, feedback_application_router, + http_application_full_route_path, http_application_owner_kind, http_application_route_path, + http_route_documents, is_http_application_operation_exposed, }; pub use multi_root::{ MultiRootApplicationOwner, MultiRootHttpOperation, MultiRootHttpRequest, diff --git a/crates/tracedecay-api/src/remote.rs b/crates/tracedecay-api/src/remote.rs index 1f3bfbfe6e..034af4926a 100644 --- a/crates/tracedecay-api/src/remote.rs +++ b/crates/tracedecay-api/src/remote.rs @@ -38,8 +38,7 @@ use tracedecay_contracts::remote::recovery::{ use tracedecay_contracts::remote::replay::RemoteReplayRequestV1; use tracedecay_contracts::remote::transfer::RemoteFrameTransferRequestV1; use tracedecay_contracts::{ - ApplicationContractError, ApplicationProblemKind, CancellationSignal, RequestId, - ResultContractRef, + ApplicationContractError, CancellationSignal, RequestId, ResultContractRef, }; use tracedecay_domain::UtcMicros; use tracedecay_tool_catalog::SchemaId; @@ -452,21 +451,7 @@ fn remote_protocol_response(response: RemoteHttpResponseV1) -> Err(problem) => { let kind = problem.problem.kind(); crate::observe::record_error_class(kind); - match kind { - ApplicationProblemKind::InvalidRequest => StatusCode::BAD_REQUEST, - ApplicationProblemKind::NotFoundOrNotAuthorized => StatusCode::NOT_FOUND, - ApplicationProblemKind::Conflict - | ApplicationProblemKind::PartialEffect - | ApplicationProblemKind::Stale => StatusCode::CONFLICT, - ApplicationProblemKind::Unsupported => StatusCode::UNPROCESSABLE_ENTITY, - ApplicationProblemKind::ResetRequired | ApplicationProblemKind::Unavailable => { - StatusCode::SERVICE_UNAVAILABLE - } - ApplicationProblemKind::ExecutionFailed => StatusCode::INTERNAL_SERVER_ERROR, - ApplicationProblemKind::Saturated => StatusCode::TOO_MANY_REQUESTS, - ApplicationProblemKind::Cancelled => StatusCode::REQUEST_TIMEOUT, - ApplicationProblemKind::TimedOut => StatusCode::GATEWAY_TIMEOUT, - } + crate::application_problem_status(kind) } }; crate::observe::json_response(status, &response) diff --git a/crates/tracedecay-dashboard-api/src/automation_authority.rs b/crates/tracedecay-dashboard-api/src/automation_authority.rs index 9fe4ff5cb4..bc1a745bd8 100644 --- a/crates/tracedecay-dashboard-api/src/automation_authority.rs +++ b/crates/tracedecay-dashboard-api/src/automation_authority.rs @@ -68,28 +68,7 @@ impl DashboardAutomationAuthorityErrorV1 { } fn application_problem_status(problem: &ApplicationProblemEnvelope) -> StatusCode { - match problem.problem.kind() { - tracedecay_contracts::ApplicationProblemKind::PartialEffect - | tracedecay_contracts::ApplicationProblemKind::Conflict - | tracedecay_contracts::ApplicationProblemKind::Stale => StatusCode::CONFLICT, - tracedecay_contracts::ApplicationProblemKind::InvalidRequest => StatusCode::BAD_REQUEST, - tracedecay_contracts::ApplicationProblemKind::NotFoundOrNotAuthorized => { - StatusCode::NOT_FOUND - } - tracedecay_contracts::ApplicationProblemKind::Unsupported => { - StatusCode::UNPROCESSABLE_ENTITY - } - tracedecay_contracts::ApplicationProblemKind::ResetRequired - | tracedecay_contracts::ApplicationProblemKind::Unavailable => { - StatusCode::SERVICE_UNAVAILABLE - } - tracedecay_contracts::ApplicationProblemKind::ExecutionFailed => { - StatusCode::INTERNAL_SERVER_ERROR - } - tracedecay_contracts::ApplicationProblemKind::Saturated => StatusCode::TOO_MANY_REQUESTS, - tracedecay_contracts::ApplicationProblemKind::Cancelled => StatusCode::REQUEST_TIMEOUT, - tracedecay_contracts::ApplicationProblemKind::TimedOut => StatusCode::GATEWAY_TIMEOUT, - } + tracedecay_api::application_problem_status(problem.problem.kind()) } pub(crate) fn automation_authority_error_response( diff --git a/crates/tracedecay-sdk/src/remote_client.rs b/crates/tracedecay-sdk/src/remote_client.rs index 96d738d6d8..4ee6e73b60 100644 --- a/crates/tracedecay-sdk/src/remote_client.rs +++ b/crates/tracedecay-sdk/src/remote_client.rs @@ -564,22 +564,7 @@ fn take_response_field( } fn status_matches_problem(status: reqwest::StatusCode, kind: ApplicationProblemKind) -> bool { - let expected = match kind { - ApplicationProblemKind::InvalidRequest => reqwest::StatusCode::BAD_REQUEST, - ApplicationProblemKind::NotFoundOrNotAuthorized => reqwest::StatusCode::NOT_FOUND, - ApplicationProblemKind::Conflict - | ApplicationProblemKind::PartialEffect - | ApplicationProblemKind::Stale => reqwest::StatusCode::CONFLICT, - ApplicationProblemKind::Unsupported => reqwest::StatusCode::UNPROCESSABLE_ENTITY, - ApplicationProblemKind::Unavailable | ApplicationProblemKind::ResetRequired => { - reqwest::StatusCode::SERVICE_UNAVAILABLE - } - ApplicationProblemKind::ExecutionFailed => reqwest::StatusCode::INTERNAL_SERVER_ERROR, - ApplicationProblemKind::Saturated => reqwest::StatusCode::TOO_MANY_REQUESTS, - ApplicationProblemKind::Cancelled => reqwest::StatusCode::REQUEST_TIMEOUT, - ApplicationProblemKind::TimedOut => reqwest::StatusCode::GATEWAY_TIMEOUT, - }; - status == expected + status.as_u16() == tracedecay_api::application_problem_status(kind).as_u16() } #[cfg(test)] From 2071daff7119da9df8aa9bb2860576261672616a Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sun, 20 Sep 2026 22:51:51 +0000 Subject: [PATCH 173/182] fix(mcp-catalog): bind memory schema closure to 'static operations The tabled memory tool schemas took a higher-ranked `FnMut(&str)`, but `definitions.rs` passes a closure annotated `|operation: &'static str|` and every `MemoryTool::operation` is a `&'static str` literal, so the closure could never satisfy the `for<'a>` bound and the crate failed to compile. Match the bound to the operations the callers actually supply. Co-Authored-By: Claude Fable 5.1 --- crates/tracedecay-mcp-catalog/src/definitions/memory.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/tracedecay-mcp-catalog/src/definitions/memory.rs b/crates/tracedecay-mcp-catalog/src/definitions/memory.rs index 66828a7d64..36eac3be9c 100644 --- a/crates/tracedecay-mcp-catalog/src/definitions/memory.rs +++ b/crates/tracedecay-mcp-catalog/src/definitions/memory.rs @@ -100,7 +100,7 @@ const MEMORY_TOOLS: &[MemoryTool] = &[ ]; pub(super) fn memory_definitions( - mut schema: impl FnMut(&str) -> Result, + mut schema: impl FnMut(&'static str) -> Result, ) -> Result, McpCatalogError> { MEMORY_TOOLS .iter() From 95720f34e81ed7d922a6f4dc8845507d9e2a55d8 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sun, 20 Sep 2026 23:45:53 +0000 Subject: [PATCH 174/182] style: rustfmt files earlier merges in this batch left unformatted `cargo fmt --all -- --check` reported drift in eleven files carried in by merges made before this batch. The change is rustfmt output only; no statement, signature, or control flow differs. Co-Authored-By: Claude Fable 5.1 --- .../src/feedback/cycle_runtime.rs | 13 +++++++++---- .../src/primitives/symbol_graph.rs | 7 ++++--- .../tracedecay-daemon-identity/src/authority.rs | 2 +- crates/tracedecay-graph-query/src/queries.rs | 4 +--- crates/tracedecay-graph-query/src/test_risk.rs | 4 +--- .../src/handlers/analysis/unmounted_files.rs | 5 +---- crates/tracedecay-mcp/src/handlers/grep.rs | 4 +--- crates/tracedecay-mcp/src/handlers/info/body.rs | 5 ++--- .../src/test_support/host_admission.rs | 16 +++++++--------- .../tracedecay/src/daemon/connection_serving.rs | 6 +----- .../src/daemon/hook_v2_replay_consumer.rs | 6 +++++- 11 files changed, 33 insertions(+), 39 deletions(-) diff --git a/crates/tracedecay-application/src/feedback/cycle_runtime.rs b/crates/tracedecay-application/src/feedback/cycle_runtime.rs index 7f247c26f9..5a94bd0a94 100644 --- a/crates/tracedecay-application/src/feedback/cycle_runtime.rs +++ b/crates/tracedecay-application/src/feedback/cycle_runtime.rs @@ -639,7 +639,9 @@ impl FeedbackCycleRuntimePort for FeedbackCycleRuntime { let invocation = (runtime.lsp_input)(request).await?; if !lsp_trigger_matches_invocation(trigger, &invocation) { let duration_micros = - tracedecay_runtime_core::tracedecay::saturating_duration_micros(started_at.elapsed()); + tracedecay_runtime_core::tracedecay::saturating_duration_micros( + started_at.elapsed(), + ); runtime.source_observations.observe_source_event( &invocation.request.input, FeedbackSourceEventV1::ArgumentRejected { @@ -660,7 +662,9 @@ impl FeedbackCycleRuntimePort for FeedbackCycleRuntime { } let input = invocation.request.input.clone(); let admission_duration_micros = - tracedecay_runtime_core::tracedecay::saturating_duration_micros(started_at.elapsed()); + tracedecay_runtime_core::tracedecay::saturating_duration_micros( + started_at.elapsed(), + ); runtime.source_observations.observe_source_event( &input, lsp_method_state_event( @@ -671,8 +675,9 @@ impl FeedbackCycleRuntimePort for FeedbackCycleRuntime { ), ); let result = Box::pin(runtime.run_once(invocation)).await; - let duration_micros = - tracedecay_runtime_core::tracedecay::saturating_duration_micros(started_at.elapsed()); + let duration_micros = tracedecay_runtime_core::tracedecay::saturating_duration_micros( + started_at.elapsed(), + ); let outcome = if result.is_ok() { FeedbackOutcomeV1::Completed } else { diff --git a/crates/tracedecay-application/src/primitives/symbol_graph.rs b/crates/tracedecay-application/src/primitives/symbol_graph.rs index 99fc7a0ac4..f878dff9e1 100644 --- a/crates/tracedecay-application/src/primitives/symbol_graph.rs +++ b/crates/tracedecay-application/src/primitives/symbol_graph.rs @@ -1094,9 +1094,10 @@ fn in_scope_parts(binding: Option<&CodeGraphSymbolBindingV1>, scope: &SymbolGrap let Some(file) = binding.and_then(|binding| binding.logical_path.as_deref()) else { return false; }; - scope.path_prefix.as_deref().is_none_or(|path_prefix| { - tracedecay_domain::path_matches_scope(file, Some(path_prefix)) - }) + scope + .path_prefix + .as_deref() + .is_none_or(|path_prefix| tracedecay_domain::path_matches_scope(file, Some(path_prefix))) } fn contains_ignore_ascii_case(value: &str, query: &str) -> bool { diff --git a/crates/tracedecay-daemon-identity/src/authority.rs b/crates/tracedecay-daemon-identity/src/authority.rs index fbd1cebe02..043dc2edff 100644 --- a/crates/tracedecay-daemon-identity/src/authority.rs +++ b/crates/tracedecay-daemon-identity/src/authority.rs @@ -1,3 +1,4 @@ +use serde::{Deserialize, Deserializer, Serialize}; use std::fmt; use std::fs::File; #[cfg(not(windows))] @@ -5,7 +6,6 @@ use std::fs::OpenOptions; use std::io::{Read, Seek, SeekFrom, Write}; use std::net::SocketAddr; use std::path::{Path, PathBuf}; -use serde::{Deserialize, Deserializer, Serialize}; use tracedecay_domain::{BrainId, UserProfileId}; use tracedecay_runtime_core::path_safety::{ canonicalize_existing_prefix, collapse_relative_components, diff --git a/crates/tracedecay-graph-query/src/queries.rs b/crates/tracedecay-graph-query/src/queries.rs index 0f58e0224d..62f04641e8 100644 --- a/crates/tracedecay-graph-query/src/queries.rs +++ b/crates/tracedecay-graph-query/src/queries.rs @@ -237,9 +237,7 @@ impl<'a> GraphQueryManager<'a> { .binding .as_ref() .and_then(|binding| binding.logical_path.as_deref()) - .is_some_and(|path| { - tracedecay_domain::path_matches_scope(path, path_prefix) - }) + .is_some_and(|path| tracedecay_domain::path_matches_scope(path, path_prefix)) && (kind_filter.is_empty() || kind_filter.contains(metadata.kind.as_str())) && (include_public || metadata.visibility != "public") && metadata.simple_name != "main" diff --git a/crates/tracedecay-graph-query/src/test_risk.rs b/crates/tracedecay-graph-query/src/test_risk.rs index 9992510eee..a23b07d2c5 100644 --- a/crates/tracedecay-graph-query/src/test_risk.rs +++ b/crates/tracedecay-graph-query/src/test_risk.rs @@ -341,9 +341,7 @@ pub fn verified_test_evidence( files .into_iter() .map(|file| file.logical_path) - .filter(|path| { - tracedecay_domain::path_matches_scope(path, Some(prefix)) - }) + .filter(|path| tracedecay_domain::path_matches_scope(path, Some(prefix))) .collect::>() }) }) diff --git a/crates/tracedecay-mcp/src/handlers/analysis/unmounted_files.rs b/crates/tracedecay-mcp/src/handlers/analysis/unmounted_files.rs index a50d0ec4ae..ee9668ea03 100644 --- a/crates/tracedecay-mcp/src/handlers/analysis/unmounted_files.rs +++ b/crates/tracedecay-mcp/src/handlers/analysis/unmounted_files.rs @@ -75,10 +75,7 @@ pub async fn handle_unmounted_files( .map(move |entry| (ecosystem.ecosystem, entry)) }) .filter(|(_, entry)| { - tracedecay_domain::path_matches_scope( - &entry.file, - path_filter.as_deref(), - ) + tracedecay_domain::path_matches_scope(&entry.file, path_filter.as_deref()) }) .collect::>(); let unmounted_file_count = matching.len(); diff --git a/crates/tracedecay-mcp/src/handlers/grep.rs b/crates/tracedecay-mcp/src/handlers/grep.rs index b4635f8a13..5f636878db 100644 --- a/crates/tracedecay-mcp/src/handlers/grep.rs +++ b/crates/tracedecay-mcp/src/handlers/grep.rs @@ -137,9 +137,7 @@ pub async fn handle_grep( .hits .into_iter() .map(GrepHit::from) - .filter(|hit| { - tracedecay_domain::path_matches_scope(hit.file.as_str(), scope_prefix) - }) + .filter(|hit| tracedecay_domain::path_matches_scope(hit.file.as_str(), scope_prefix)) .collect::>(); let truncated = scan.truncated || hits.len() > max_results; hits.truncate(max_results); diff --git a/crates/tracedecay-mcp/src/handlers/info/body.rs b/crates/tracedecay-mcp/src/handlers/info/body.rs index ecbfe737e2..d382ec63eb 100644 --- a/crates/tracedecay-mcp/src/handlers/info/body.rs +++ b/crates/tracedecay-mcp/src/handlers/info/body.rs @@ -173,9 +173,8 @@ fn body_candidates( for candidate in candidates { let path = required_file_path(&candidate)?; let metadata = required_metadata(&candidate)?; - if scope_prefix.is_none_or(|scope| { - tracedecay_domain::path_matches_scope(path, Some(scope)) - }) { + if scope_prefix.is_none_or(|scope| tracedecay_domain::path_matches_scope(path, Some(scope))) + { let preference = NodeKind::from_str(&metadata.kind) .map_or(u8::MAX, |kind| body_kind_preference(&kind)); scoped.push((preference, candidate)); diff --git a/crates/tracedecay-project/src/test_support/host_admission.rs b/crates/tracedecay-project/src/test_support/host_admission.rs index bf29ae9b83..d1e0cd45e8 100644 --- a/crates/tracedecay-project/src/test_support/host_admission.rs +++ b/crates/tracedecay-project/src/test_support/host_admission.rs @@ -952,15 +952,13 @@ impl HostAdmissionTestRuntimeV1 { pub fn facade(&self) -> HostAdmissionFacade<'_> { let authorities = match (self.project_id.as_ref(), self.project_registered.as_ref()) { - (Some(project_id), Some(project_registered)) => { - HostAdmissionAuthorities::for_project( - self.brain_id.clone(), - self.profile_id.clone(), - project_id.clone(), - project_registered, - ) - .with_profile_registered(self.profile_id.clone(), self.profile_registered.as_ref()) - } + (Some(project_id), Some(project_registered)) => HostAdmissionAuthorities::for_project( + self.brain_id.clone(), + self.profile_id.clone(), + project_id.clone(), + project_registered, + ) + .with_profile_registered(self.profile_id.clone(), self.profile_registered.as_ref()), _ => HostAdmissionAuthorities::for_profile( self.brain_id.clone(), self.profile_id.clone(), diff --git a/crates/tracedecay/src/daemon/connection_serving.rs b/crates/tracedecay/src/daemon/connection_serving.rs index a8cc39d044..18ad9346c3 100644 --- a/crates/tracedecay/src/daemon/connection_serving.rs +++ b/crates/tracedecay/src/daemon/connection_serving.rs @@ -840,11 +840,7 @@ async fn drive_retained_invocation_responses<'a>( Ok(request) => execute(request).await, Err(response) => response, }; - update_connection_lsp_sessions( - owned_lsp_sessions, - session_transition.as_ref(), - &response, - ); + update_connection_lsp_sessions(owned_lsp_sessions, session_transition.as_ref(), &response); let delivery = delivery.filter(|delivery| delivery.is_successful_delivery(&response)); // Resolve fan-out bindings before the socket response crosses // the wire. The same immutable attempts are used for a diff --git a/crates/tracedecay/src/daemon/hook_v2_replay_consumer.rs b/crates/tracedecay/src/daemon/hook_v2_replay_consumer.rs index a921594c90..cfb0b8e52b 100644 --- a/crates/tracedecay/src/daemon/hook_v2_replay_consumer.rs +++ b/crates/tracedecay/src/daemon/hook_v2_replay_consumer.rs @@ -246,7 +246,11 @@ async fn drain_admitted_host_spool( } fn hook_replay_now() -> UtcMicros { - UtcMicros(tracedecay_runtime_core::tracedecay::saturating_utc_now().0.max(1)) + UtcMicros( + tracedecay_runtime_core::tracedecay::saturating_utc_now() + .0 + .max(1), + ) } struct RegisteredReplayConsumer { From debc48cee8109d876b43c75396eba8fc39c18aca Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sun, 20 Sep 2026 23:57:33 +0000 Subject: [PATCH 175/182] fix: clear clippy and rustfmt failures across the merged batch `cargo clippy --workspace --all-targets -- -D warnings` did not pass on this branch. Six findings, four inherited from earlier merges and two introduced by this batch: - `decode_with_canonical_digest` returned `Result`. No caller read the error, so it now returns `Option` and the five call sites use `ok_or`/`ok_or_else`. - `schema.rs` declared a test module before a later item. - `score_lexical_row` takes eight arguments, so it carries the same `too_many_arguments` allow the rest of the tree uses. - The `Default` impl for `LanguageRegistry` that #1910 removed as uncalled is required by `new_without_default`; it is back with a note saying so. - The shared `isolated_profile` harness #1921 introduced was loaded as two modules in `tracedecay` and in `tracedecay-daemon-control`. Each crate now declares it once and the test modules import it from there. - The two-argument problem helpers take `impl Into`, which made five borrows in `automation_jobs_api` needless. Co-Authored-By: Claude Fable 5.1 --- crates/tracedecay-code-extraction/src/lib.rs | 9 +++++++++ .../src/work_leak_adjudication.rs | 2 +- crates/tracedecay-contracts/src/work_retry.rs | 2 +- crates/tracedecay-daemon-control/src/service.rs | 6 ++++++ .../src/service/tests.rs | 4 +--- .../src/service/update_restore_tests.rs | 5 +---- .../src/automation_jobs_api.rs | 16 ++++++---------- .../tracedecay-domain/src/research/canonical.rs | 8 ++++---- .../src/research/canonical_tests.rs | 4 ++-- crates/tracedecay-graph-db/src/schema.rs | 12 ++++++------ .../src/retrieval/lexical/projection.rs | 1 + .../tracedecay-rusqlite-runtime/src/workflow.rs | 4 ++-- .../src/workflow/census.rs | 4 ++-- .../src/workflow/run_journal.rs | 2 +- crates/tracedecay/src/daemon/tests.rs | 4 +--- crates/tracedecay/src/lib.rs | 5 +++++ .../mcp/tools/handlers/dispatch_test_support.rs | 4 +--- 17 files changed, 50 insertions(+), 42 deletions(-) diff --git a/crates/tracedecay-code-extraction/src/lib.rs b/crates/tracedecay-code-extraction/src/lib.rs index 0dab251a9c..df019ba1b5 100644 --- a/crates/tracedecay-code-extraction/src/lib.rs +++ b/crates/tracedecay-code-extraction/src/lib.rs @@ -365,6 +365,15 @@ pub struct LanguageRegistry { by_extension: HashMap, } +/// Required by `clippy::new_without_default` for the argument-less `new` +/// below, so this is API surface the lint owns rather than an uncalled entry +/// point a dead-surface pass may drop. +impl Default for LanguageRegistry { + fn default() -> Self { + Self::new() + } +} + impl LanguageRegistry { /// Creates a new registry with all built-in language extractors. pub fn new() -> Self { diff --git a/crates/tracedecay-contracts/src/work_leak_adjudication.rs b/crates/tracedecay-contracts/src/work_leak_adjudication.rs index fb7e3dbe2d..6a0a7525b0 100644 --- a/crates/tracedecay-contracts/src/work_leak_adjudication.rs +++ b/crates/tracedecay-contracts/src/work_leak_adjudication.rs @@ -308,7 +308,7 @@ where .expected_revision .unwrap_or(0) .checked_add(1) - .ok_or_else(|| invalid_problem())?; + .ok_or_else(invalid_problem)?; self.storage .compare_and_record_leak( &authority, diff --git a/crates/tracedecay-contracts/src/work_retry.rs b/crates/tracedecay-contracts/src/work_retry.rs index a06e678202..fcaaf8e954 100644 --- a/crates/tracedecay-contracts/src/work_retry.rs +++ b/crates/tracedecay-contracts/src/work_retry.rs @@ -671,7 +671,7 @@ where .execution() .cancellation_generation() .checked_add(1) - .ok_or_else(|| invalid_problem())?; + .ok_or_else(invalid_problem)?; let envelope = WorkExecutionEnvelopeV1::new( identity.clone(), binding.clone(), diff --git a/crates/tracedecay-daemon-control/src/service.rs b/crates/tracedecay-daemon-control/src/service.rs index aa5bbff3c8..bb3b28d1d2 100644 --- a/crates/tracedecay-daemon-control/src/service.rs +++ b/crates/tracedecay-daemon-control/src/service.rs @@ -18,6 +18,12 @@ mod runner; mod unit_file; mod windows_task; +/// Declared once for the whole module: both test children below need the +/// shared harness, and loading the same file as two modules is a clippy error. +#[cfg(test)] +#[path = "../../../tests/support/isolated_profile.rs"] +mod isolated_profile; + #[cfg(test)] #[allow(clippy::expect_used)] mod tests; diff --git a/crates/tracedecay-daemon-control/src/service/tests.rs b/crates/tracedecay-daemon-control/src/service/tests.rs index d2d9b745e1..82f47fc72b 100644 --- a/crates/tracedecay-daemon-control/src/service/tests.rs +++ b/crates/tracedecay-daemon-control/src/service/tests.rs @@ -22,9 +22,7 @@ use tracedecay_runtime_core::config::{ const TEST_BUILD_VERSION: &str = "0.1.0-test+service-probe"; -#[path = "../../../../tests/support/isolated_profile.rs"] -mod isolated_profile; -use isolated_profile::EnvVarGuard; +use super::isolated_profile::EnvVarGuard; #[cfg(target_os = "linux")] fn systemctl_log_contains_sequence(log: &str, expected: &[&str]) -> bool { diff --git a/crates/tracedecay-daemon-control/src/service/update_restore_tests.rs b/crates/tracedecay-daemon-control/src/service/update_restore_tests.rs index b1f65df980..6fd2610130 100644 --- a/crates/tracedecay-daemon-control/src/service/update_restore_tests.rs +++ b/crates/tracedecay-daemon-control/src/service/update_restore_tests.rs @@ -40,10 +40,7 @@ fn quiesced_guard() -> QuiescedDaemonLifecycle { } #[cfg(unix)] -#[path = "../../../../tests/support/isolated_profile.rs"] -mod isolated_profile; -#[cfg(unix)] -use isolated_profile::EnvVarGuard; +use super::isolated_profile::EnvVarGuard; #[cfg(unix)] fn serve_initialize_identity( diff --git a/crates/tracedecay-dashboard-api/src/automation_jobs_api.rs b/crates/tracedecay-dashboard-api/src/automation_jobs_api.rs index 86fedba618..e2696ced13 100644 --- a/crates/tracedecay-dashboard-api/src/automation_jobs_api.rs +++ b/crates/tracedecay-dashboard-api/src/automation_jobs_api.rs @@ -97,7 +97,7 @@ pub async fn list(State(state): State) -> ApiResult { #[hotpath::measure(label = "dashboard_api.jobs.create", future = true)] pub async fn create(State(state): State, Json(body): Json) -> ApiResult { let body = serde_json::from_value::(body) - .map_err(|err| json_error(StatusCode::BAD_REQUEST, &format!("invalid job: {err}")))?; + .map_err(|err| json_error(StatusCode::BAD_REQUEST, format!("invalid job: {err}")))?; let now = current_timestamp(); let job = AutomationJob { id: match body.id { @@ -117,7 +117,7 @@ pub async fn create(State(state): State, Json(body): Json updated_at: now, extra: BTreeMap::new(), }; - validate_job(&job).map_err(|err| json_error(StatusCode::BAD_REQUEST, &err.to_string()))?; + validate_job(&job).map_err(|err| json_error(StatusCode::BAD_REQUEST, err.to_string()))?; let job_for_write = job.clone(); let result = super::automation_run_service::execute_dashboard_automation_write( &state, @@ -141,7 +141,7 @@ pub async fn create(State(state): State, Json(body): Json if result["conflict"] == true { return Err(json_error( StatusCode::BAD_REQUEST, - &format!("job '{}' already exists", job.id), + format!("job '{}' already exists", job.id), )); } Ok(Json(json!({ "job": job }))) @@ -162,12 +162,8 @@ pub async fn update( AxumPath(job_id): AxumPath, Json(body): Json, ) -> ApiResult { - let patch = serde_json::from_value::(body).map_err(|err| { - json_error( - StatusCode::BAD_REQUEST, - &format!("invalid job patch: {err}"), - ) - })?; + let patch = serde_json::from_value::(body) + .map_err(|err| json_error(StatusCode::BAD_REQUEST, format!("invalid job patch: {err}")))?; let job_id_for_write = job_id.clone(); let result = super::automation_run_service::execute_dashboard_automation_write( &state, @@ -300,7 +296,7 @@ async fn load_job_or_404( state: &DashboardState, job_id: &str, ) -> std::result::Result { - validate_job_id(job_id).map_err(|err| json_error(StatusCode::BAD_REQUEST, &err.to_string()))?; + validate_job_id(job_id).map_err(|err| json_error(StatusCode::BAD_REQUEST, err.to_string()))?; match find_job(&state.dashboard_root, job_id).await { Ok(Some(job)) => Ok(job), Ok(None) => Err(not_found(job_id)), diff --git a/crates/tracedecay-domain/src/research/canonical.rs b/crates/tracedecay-domain/src/research/canonical.rs index 5b668d67cd..63e9d232d6 100644 --- a/crates/tracedecay-domain/src/research/canonical.rs +++ b/crates/tracedecay-domain/src/research/canonical.rs @@ -122,14 +122,14 @@ pub fn canonical_json_bytes_and_sha256( /// Decode a stored JSON payload and require its canonical digest to match the /// column that was persisted with it. Journal rows use this so a rewritten /// payload cannot reuse another row's digest. -pub fn decode_with_canonical_digest(payload: &str, stored_digest: &str) -> Result +pub fn decode_with_canonical_digest(payload: &str, stored_digest: &str) -> Option where T: Serialize + DeserializeOwned, { - let value: T = serde_json::from_str(payload).map_err(|_| ())?; + let value: T = serde_json::from_str(payload).ok()?; match canonical_sha256(&value) { - Ok(digest) if digest.as_str() == stored_digest => Ok(value), - _ => Err(()), + Ok(digest) if digest.as_str() == stored_digest => Some(value), + _ => None, } } diff --git a/crates/tracedecay-domain/src/research/canonical_tests.rs b/crates/tracedecay-domain/src/research/canonical_tests.rs index 4d8a626a64..b4ce6ff4b7 100644 --- a/crates/tracedecay-domain/src/research/canonical_tests.rs +++ b/crates/tracedecay-domain/src/research/canonical_tests.rs @@ -21,8 +21,8 @@ fn decode_with_canonical_digest_accepts_matching_payload_and_rejects_tamper() { let decoded: Value = decode_with_canonical_digest(&payload, digest.as_str()).expect("matching digest"); assert_eq!(decoded, value); - assert!(decode_with_canonical_digest::(&payload, "sha256:dead").is_err()); - assert!(decode_with_canonical_digest::("{", digest.as_str()).is_err()); + assert!(decode_with_canonical_digest::(&payload, "sha256:dead").is_none()); + assert!(decode_with_canonical_digest::("{", digest.as_str()).is_none()); } #[test] diff --git a/crates/tracedecay-graph-db/src/schema.rs b/crates/tracedecay-graph-db/src/schema.rs index 87a645c61d..2d8130b3c1 100644 --- a/crates/tracedecay-graph-db/src/schema.rs +++ b/crates/tracedecay-graph-db/src/schema.rs @@ -830,6 +830,12 @@ fn decode_utf8(value: &str, description: &str) -> Result { }) } +fn persisted_validation_error(description: &str, error: GraphDbError) -> GraphDbError { + GraphDbError::Corrupt { + message: format!("invalid persisted {description}: {error}"), + } +} + #[cfg(test)] mod graph_stable_identity_tests { use super::graph_stable_identity; @@ -846,9 +852,3 @@ mod graph_stable_identity_tests { ); } } - -fn persisted_validation_error(description: &str, error: GraphDbError) -> GraphDbError { - GraphDbError::Corrupt { - message: format!("invalid persisted {description}: {error}"), - } -} diff --git a/crates/tracedecay-query/src/retrieval/lexical/projection.rs b/crates/tracedecay-query/src/retrieval/lexical/projection.rs index c91356a7c2..3b61929551 100644 --- a/crates/tracedecay-query/src/retrieval/lexical/projection.rs +++ b/crates/tracedecay-query/src/retrieval/lexical/projection.rs @@ -866,6 +866,7 @@ fn add_score(scores: &mut BTreeMap, field: LexicalFieldV1, /// Shared exact/fuzzy/phrase/proximity scoring for the in-memory projection /// and the artifact reader. Callers supply term frequencies and BM25 inputs; /// the loop, fuzzy discount, phrase boost, and echo penalty stay one place. +#[allow(clippy::too_many_arguments)] fn score_lexical_row( row: &impl LexicalFieldTextV1, exact_terms: &[ExactTechnicalTermV1], diff --git a/crates/tracedecay-rusqlite-runtime/src/workflow.rs b/crates/tracedecay-rusqlite-runtime/src/workflow.rs index 42ee8fba61..6533275355 100644 --- a/crates/tracedecay-rusqlite-runtime/src/workflow.rs +++ b/crates/tracedecay-rusqlite-runtime/src/workflow.rs @@ -98,7 +98,7 @@ impl WorkflowSqliteAuthority { }; let definition: WorkflowDefinition = tracedecay_domain::decode_with_canonical_digest(payload, stored_digest) - .map_err(|_| WorkflowSqliteAuthorityBuildError::ResetRequired)?; + .ok_or(WorkflowSqliteAuthorityBuildError::ResetRequired)?; if definition.definition_id() != definition_id || definition.definition_version() != definition_version { @@ -276,7 +276,7 @@ fn decode_definition_source_row( return Err(definition_authority_unavailable()); }; tracedecay_domain::decode_with_canonical_digest(payload, stored_digest) - .map_err(|_| definition_authority_unavailable()) + .ok_or_else(definition_authority_unavailable) } fn definition_authority_unavailable() -> WorkflowDefinitionAuthorityError { diff --git a/crates/tracedecay-rusqlite-runtime/src/workflow/census.rs b/crates/tracedecay-rusqlite-runtime/src/workflow/census.rs index 5533104e4a..f087ad2e68 100644 --- a/crates/tracedecay-rusqlite-runtime/src/workflow/census.rs +++ b/crates/tracedecay-rusqlite-runtime/src/workflow/census.rs @@ -24,7 +24,7 @@ fn decode_census( ) -> Result { let census: WorkflowFanOutCensusV1 = tracedecay_domain::decode_with_canonical_digest(payload, stored_digest) - .map_err(|_| WorkflowFanOutCensusError::InvalidHistory)?; + .ok_or(WorkflowFanOutCensusError::InvalidHistory)?; census .validate() .map_err(|_| WorkflowFanOutCensusError::InvalidHistory)?; @@ -114,7 +114,7 @@ fn projection_through_tx( let stored_digest = sql_text(&row.values, 1).ok_or(WorkflowFanOutCensusError::InvalidHistory)?; tracedecay_domain::decode_with_canonical_digest(payload, stored_digest) - .map_err(|_| WorkflowFanOutCensusError::InvalidHistory) + .ok_or(WorkflowFanOutCensusError::InvalidHistory) }) .collect::, _>>()?; let projection = WorkflowRunProjection::rebuild(&history) diff --git a/crates/tracedecay-rusqlite-runtime/src/workflow/run_journal.rs b/crates/tracedecay-rusqlite-runtime/src/workflow/run_journal.rs index 8430a906ec..d8334c1a16 100644 --- a/crates/tracedecay-rusqlite-runtime/src/workflow/run_journal.rs +++ b/crates/tracedecay-rusqlite-runtime/src/workflow/run_journal.rs @@ -31,7 +31,7 @@ fn decode_event( stored_digest: &str, ) -> Result { tracedecay_domain::decode_with_canonical_digest(payload, stored_digest) - .map_err(|_| WorkflowRunStorageError::InvalidHistory) + .ok_or(WorkflowRunStorageError::InvalidHistory) } fn history_tx( diff --git a/crates/tracedecay/src/daemon/tests.rs b/crates/tracedecay/src/daemon/tests.rs index 1416db774c..09798889ae 100644 --- a/crates/tracedecay/src/daemon/tests.rs +++ b/crates/tracedecay/src/daemon/tests.rs @@ -299,9 +299,7 @@ fn test_daemon_engine_for_profile(profile_root: &std::path::Path) -> DaemonEngin engine } -#[path = "../../../../tests/support/isolated_profile.rs"] -mod isolated_profile; -use isolated_profile::EnvVarGuard; +use crate::isolated_profile::EnvVarGuard; /// Pins the codex app-server launcher to a path that cannot exist so any /// automation tick reached during the test fails with the typed spawn error diff --git a/crates/tracedecay/src/lib.rs b/crates/tracedecay/src/lib.rs index f35b2e9d0a..a8cdd44416 100644 --- a/crates/tracedecay/src/lib.rs +++ b/crates/tracedecay/src/lib.rs @@ -49,6 +49,11 @@ pub use tracedecay_application::git_query; mod hooks; #[cfg(test)] mod host_admission_test; +/// Declared once for the crate: several test modules need the shared harness, +/// and loading the same file as two modules is a clippy error. +#[cfg(test)] +#[path = "../../../tests/support/isolated_profile.rs"] +mod isolated_profile; pub mod mcp; pub use tracedecay_project::product_runtime::{ ProductRuntimeError, ProductRuntimeProvider, ProductSourceProvenance, product_runtime, diff --git a/crates/tracedecay/src/mcp/tools/handlers/dispatch_test_support.rs b/crates/tracedecay/src/mcp/tools/handlers/dispatch_test_support.rs index 8ac58298f0..56ad2cc904 100644 --- a/crates/tracedecay/src/mcp/tools/handlers/dispatch_test_support.rs +++ b/crates/tracedecay/src/mcp/tools/handlers/dispatch_test_support.rs @@ -306,9 +306,7 @@ pub(super) async fn init_sibling_registered_fixture( (graph, sibling) } -#[path = "../../../../../../tests/support/isolated_profile.rs"] -mod isolated_profile; -use isolated_profile::EnvVarGuard; +use crate::isolated_profile::EnvVarGuard; pub(super) struct SelectorEnv { _home: EnvVarGuard, From 42597b2c6910d3fb44c10376de85b94a894063e1 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Mon, 21 Sep 2026 01:20:22 +0000 Subject: [PATCH 176/182] fix: restore the pinned corpus fixture and a transport test import Two CI failures on the batch, neither caught by the workspace clippy lens. The search-eval workload pins each corpus document to a blob at commit 8312618fee and refuses to validate when the checked-in fixture copy drifts. #1921's TryFrom codemod rewrote the runtime-root copy of `repository.rs` along with the live sources, so `validate` failed with "corpus fixture bytes differ from pinned source blob: repository". The copy is restored byte-for-byte from the pinned blob. `graph_rebuild_status_test.rs` lost its `JsonRpcResponse` import in the #1580 merge while keeping the `tool_payload` helper that needs it, which only compiles under the test-transport feature CI's feature gates and root-transport lenses exercise. The import is back. Co-Authored-By: Claude Fable 5.1 --- .../corpus/crates/tracedecay-domain/src/repository.rs | 7 ++++++- .../graph_rebuild_status_test.rs | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/tracedecay-query/assets/runtime-root/tests/fixtures/search_quality/corpus/crates/tracedecay-domain/src/repository.rs b/crates/tracedecay-query/assets/runtime-root/tests/fixtures/search_quality/corpus/crates/tracedecay-domain/src/repository.rs index 5fc420ee66..816ce1a55f 100644 --- a/crates/tracedecay-query/assets/runtime-root/tests/fixtures/search_quality/corpus/crates/tracedecay-domain/src/repository.rs +++ b/crates/tracedecay-query/assets/runtime-root/tests/fixtures/search_quality/corpus/crates/tracedecay-domain/src/repository.rs @@ -501,7 +501,12 @@ mod tests { const COMMIT: &str = "0123456789abcdef0123456789abcdef01234567"; const TREE: &str = "89abcdef0123456789abcdef0123456789abcdef"; - use crate::test_fixtures::id; + fn id(value: &str) -> T + where + T: TryFrom, + { + T::try_from(value.to_owned()).expect("valid fixture identity") + } fn evidence() -> RepositoryEvidenceV1 { RepositoryEvidenceV1::new( diff --git a/crates/tracedecay/tests/transport_acceptance_suite/graph_rebuild_status_test.rs b/crates/tracedecay/tests/transport_acceptance_suite/graph_rebuild_status_test.rs index ba427ff5a5..8af6721fad 100644 --- a/crates/tracedecay/tests/transport_acceptance_suite/graph_rebuild_status_test.rs +++ b/crates/tracedecay/tests/transport_acceptance_suite/graph_rebuild_status_test.rs @@ -21,6 +21,7 @@ use std::time::Duration; use serde_json::{Value, json}; use tracedecay::daemon::ProductionProjectCompositionHarnessV1; +use tracedecay_mcp::JsonRpcResponse; use crate::common::mcp_response::tool_json; From b2e0d430e13f7c7a78edd18bb089d68cf5c9f473 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Mon, 21 Sep 2026 02:01:53 +0000 Subject: [PATCH 177/182] test(mcp): derive field_sites fixture lines from the source text The real-literals field_sites test copies the live monitor_ring.rs as its fixture and pinned two absolute line numbers in it: the string literal that must not count as a field site and the one write site. A simplify commit in this batch shortened a timestamp read three lines above the write, so the write moved from 256 to 253 and the test broke on an unrelated edit. The test now finds both lines in the fixture text it copies, so only a change to the sites themselves fails it. Co-Authored-By: Claude Fable 5.1 --- .../mcp_handler_test/graph_analysis_test.rs | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/graph_analysis_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/graph_analysis_test.rs index 6c804e5a29..c93a12bd37 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/graph_analysis_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/graph_analysis_test.rs @@ -3998,14 +3998,20 @@ async fn field_sites_ignores_field_text_in_real_rust_literals() { let dir = test_temp_dir(); let project_root = dir.path().join("project"); fs::create_dir_all(project_root.join("src")).unwrap(); - fs::write( - project_root.join("src/lib.rs"), - include_str!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/../tracedecay-session-memory/src/monitor_ring.rs" - )), - ) - .unwrap(); + let source = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../tracedecay-session-memory/src/monitor_ring.rs" + )); + let line_of = |needle: &str| { + source + .lines() + .position(|line| line.contains(needle)) + .map(|index| index as u64 + 1) + .unwrap_or_else(|| panic!("fixture lost the line containing {needle:?}")) + }; + let literal_line = line_of("\"monitor.mmap\""); + let write_line = line_of("self.mmap = unsafe"); + fs::write(project_root.join("src/lib.rs"), source).unwrap(); let host = init_test_project(&project_root).await; let result = handle_tool_call( @@ -4025,10 +4031,13 @@ async fn field_sites_ignores_field_text_in_real_rust_literals() { assert!( output["read_sites"] .as_array() - .is_some_and(|sites| sites.iter().all(|site| site["line"] != 38)), + .is_some_and(|sites| sites.iter().all(|site| site["line"] != literal_line)), "string literal was reported as a field site: {output}" ); - assert_eq!(output["write_sites"][0]["line"], 256, "payload: {output}"); + assert_eq!( + output["write_sites"][0]["line"], write_line, + "payload: {output}" + ); } fn field_site(line: u64, enclosing: &str, snippet: &str) -> Value { From e49f1ae4d67ab02d10805e28297171818a8311b3 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Mon, 21 Sep 2026 02:44:07 +0000 Subject: [PATCH 178/182] fix(code-index): bind a seat's source proof under the scheduler lock An unchanged reconcile pass re-proves a seat that was installed without a currency witness. It did so after the blocking reconcile closure had returned, so the scheduler mutex was already released and the pass guard already dropped when the witness landed. A reader that held the scheduler to keep the seat unproven, the exact state a restart-restored seat is in before its first passing probe, could then watch the proof appear under it: the verified read abstained and the readiness census answered ready a moment later. CI hit that window in busy_scheduler_still_refuses_a_seated_generation_without_a_currency_witness. The proof is now bound inside the closure, on the Noop outcome, while the pass still holds the scheduler and its in-progress guard. The late arm only announces the change. The bind helper's own checks are unchanged: it still refuses a seat whose snapshot is not the one the pass verified and only records a proof the freshness fence can supply. Co-Authored-By: Claude Fable 5.1 --- .../code_index_scheduler/registry/mount.rs | 48 +++++++++++-------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/mount.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/mount.rs index 5f00b144fa..e00e30d869 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/mount.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/mount.rs @@ -899,6 +899,9 @@ impl CodeIndexSchedulerRegistryV1 { "code-index background reconcile pass started" ); let shutting_down = Arc::clone(&worker_shutting_down); + let bind_serving_generation = Arc::clone(&worker_serving_generation); + let bind_serving_source_witness = Arc::clone(&worker_serving_source_witness); + let bind_source_freshness = worker_source_freshness.clone(); let source_result = hotpath::future!( tokio::task::spawn_blocking(move || { let mut scheduler = @@ -952,7 +955,7 @@ impl CodeIndexSchedulerRegistryV1 { }, )); } - if let Some(metadata) = retained_text_metadata { + let outcome = if let Some(metadata) = retained_text_metadata { match scheduler.reconcile_retained_text_generation_with( &metadata, !graph_activation_enabled, @@ -968,7 +971,28 @@ impl CodeIndexSchedulerRegistryV1 { scheduler.activate_or_reconcile() } else { scheduler.reconcile_now() + }?; + // A seat whose publishing pass could not prove its + // source (`code_index_post_projection_source_unverified`) + // installs without a currency witness. The swap arm + // re-proves such a seat as `Offered`, but a retained + // native graph that already serves skips the graph + // prepare and with it the swap, so no later pass ever + // reached that arm. This unchanged pass verified + // exactly the snapshot the seat was sealed from, so + // bind that proof here, while this pass still holds + // the scheduler: a reader that holds the scheduler to + // keep a seat unproven must not see the proof land + // after the pass has already let go. + if let CodeIndexReconcileOutcomeV1::Noop(evidence) = &outcome { + Self::bind_unproven_seat_to_verified_source( + &bind_serving_generation, + &bind_serving_source_witness, + &bind_source_freshness, + &evidence.snapshot_content_identity, + ); } + Ok(outcome) }), // Sealing moved inside this blocking reconcile pipeline. // Keep the outer future labeled so default reports retain @@ -2115,25 +2139,9 @@ impl CodeIndexSchedulerRegistryV1 { && worker_source_freshness .ready_without_stat(&worker_project_root, &worker_shutting_down) { - // A seat whose publishing pass could not prove its - // source (`code_index_post_projection_source_unverified`) - // installs without a currency witness. The swap arm - // re-proves such a seat as `Offered`, but a retained - // native graph that already serves skips the graph - // prepare and with it the swap, so no later pass ever - // reached that arm: every ready probe kept requesting a - // reconcile and the route stayed `stale / verifying` - // indefinitely. This unchanged pass verified exactly - // the snapshot the seat was sealed from, so bind that - // proof here. - if let CodeIndexReconcileOutcomeV1::Noop(evidence) = outcome { - Self::bind_unproven_seat_to_verified_source( - &worker_serving_generation, - &worker_serving_source_witness, - &worker_source_freshness, - &evidence.snapshot_content_identity, - ); - } + // The pass bound the seat's source proof under the + // scheduler lock; announce the change now that the + // proof is public. worker_serving_generation_changed.send_replace(()); // The clone-backfill continuation was stamped before // this pass dropped `reconcile_in_progress`. From e1717ab3963b01a1ba9006421b4a2c4514b0ab6a Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Mon, 21 Sep 2026 03:10:36 +0000 Subject: [PATCH 179/182] chore: keep one simplify commit type and document it Three folded PRs each added `simplify` to the commitlint type list, and the merges kept all three entries. One remains, and AGENTS.md's type list now names it so the hook, the linter, and the conventions agree. It is hidden from generated release notes the same way `refactor` is. Co-Authored-By: Claude Fable 5.1 --- AGENTS.md | 3 ++- commitlint.config.cjs | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8f37df7930..91df6e9a5d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -92,7 +92,8 @@ unauthorized external action after completing independent, authorized work. - Commits: `(): ` (scope optional; full header ≤ 72 chars) with one of `build`, `chore`, `ci`, `docs`, `feat`, `fix`, `perf`, `refactor`, `revert`, - `style`, `test`. Every non-merge commit message must pass commitlint + `simplify`, `style`, `test`. `simplify` is a behavior-preserving deletion + or dedup; like `refactor` it is hidden from generated release notes. Every non-merge commit message must pass commitlint (`npm run lint:commit`, configured in `commitlint.config.cjs`; the `.githooks/commit-msg` hook runs it locally via `scripts/install-git-hooks.sh`). diff --git a/commitlint.config.cjs b/commitlint.config.cjs index 422039c4f4..3f19d02c48 100644 --- a/commitlint.config.cjs +++ b/commitlint.config.cjs @@ -10,9 +10,7 @@ const allowedTypes = [ "revert", "simplify", "style", - "simplify", "test", - "simplify", ]; module.exports = { From cc28eeb53b2a6fd1d075ec301039b38b032eca08 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Mon, 21 Sep 2026 03:11:58 +0000 Subject: [PATCH 180/182] fix(release): keep the transitive feature check on the release path A folded commit passed --manifest-only to the production feature checker from the release profile resolver, so a release build only read the feature table and skipped the cargo tree walk that proves no dependency enables test-transport transitively. Stable and beta releases are dispatched on a tag without waiting for the CI run that still performs that walk, and a cancelled master run leaves the tag unproven. The resolver runs the full check again; it costs under a second on the workspace. The flag had no other caller and is gone. The resolver's fixture test builds a real single-crate workspace with an offline lockfile so the same walk runs there. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/ci.yml | 4 ++-- scripts/check-production-feature-profile.py | 9 --------- scripts/resolve-release-source-profile.py | 8 +------- scripts/test-resolve-release-source-profile.py | 16 +++++++++++++++- 4 files changed, 18 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc86000dcb..544d3b37fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1058,8 +1058,8 @@ jobs: # forks on the local feature while hotpath::io! expands on the backend. cargo check --workspace --all-targets --features hotpath/hotpath --locked - # One graph walk for the whole push. Release jobs only read the manifest; - # repeating cargo tree on every target does not change the binary. + # One graph walk per push. The release resolver repeats it on the tag it + # builds, since a release is dispatched without waiting on this run. - name: Check the production feature graph run: python3 scripts/check-production-feature-profile.py diff --git a/scripts/check-production-feature-profile.py b/scripts/check-production-feature-profile.py index f1eb82e12c..ea12e82500 100755 --- a/scripts/check-production-feature-profile.py +++ b/scripts/check-production-feature-profile.py @@ -48,11 +48,6 @@ def main() -> int: type=Path, default=Path(__file__).resolve().parent.parent, ) - parser.add_argument( - "--manifest-only", - action="store_true", - help="check the feature table and skip cargo tree", - ) arguments = parser.parse_args() repo = arguments.repo.resolve() @@ -71,10 +66,6 @@ def main() -> int: raise SystemExit("production feature set lost a required member") if "test-transport" in features["production"]: raise SystemExit("production feature directly enables test-transport") - if arguments.manifest_only: - print("production feature manifest is eligible for release") - return 0 - # `default` is exactly `["production"]`, so a second default-feature walk # repeats this graph. `cargo metadata` unifies dev-dependency features # across the workspace and makes test-only transports look reachable; diff --git a/scripts/resolve-release-source-profile.py b/scripts/resolve-release-source-profile.py index bd9bd39d78..fd9c35b1d3 100755 --- a/scripts/resolve-release-source-profile.py +++ b/scripts/resolve-release-source-profile.py @@ -63,13 +63,7 @@ def main() -> int: if "production" in features: checker = Path(__file__).with_name("check-production-feature-profile.py") subprocess.run( - [ - sys.executable, - str(checker), - "--repo", - str(source), - "--manifest-only", - ], + [sys.executable, str(checker), "--repo", str(source)], check=True, ) profile = "production" diff --git a/scripts/test-resolve-release-source-profile.py b/scripts/test-resolve-release-source-profile.py index b40faf0aca..45736b8c6c 100755 --- a/scripts/test-resolve-release-source-profile.py +++ b/scripts/test-resolve-release-source-profile.py @@ -27,8 +27,22 @@ def run_fixture(manifest: str) -> FixtureResult: # The resolver reads the product package manifest, not the workspace # root: `crates/tracedecay/Cargo.toml` is where the feature table lives. product = source.joinpath("crates", "tracedecay") - product.mkdir(parents=True) + product.joinpath("src").mkdir(parents=True) product.joinpath("Cargo.toml").write_text(manifest, encoding="utf-8") + product.joinpath("src", "lib.rs").write_text("", encoding="utf-8") + # The resolver walks the resolved dependency graph with `cargo tree + # --locked`, so the fixture is a real workspace with a lockfile. It + # has no dependencies, so the lockfile resolves offline. + source.joinpath("Cargo.toml").write_text( + '[workspace]\nmembers = ["crates/tracedecay"]\nresolver = "2"\n', + encoding="utf-8", + ) + subprocess.run( + ["cargo", "generate-lockfile", "--offline"], + cwd=source, + check=True, + capture_output=True, + ) output = source / "github-output.txt" completed = subprocess.run( [ From 5901984c30dba6d50eb7b273829b9f987135a751 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Mon, 21 Sep 2026 03:53:34 +0000 Subject: [PATCH 181/182] test(mcp): warm until the seat's source proof has settled The shared warm helper waited for status=current, a search on the sealed generation, and complete lane coverage. A seat can still owe its source proof to a continuation pass at that point, and a read taken before the pass binds it reports verifying. #1891 fenced that window in one search test; the context test then hit the same window on CI. The helper now also takes the first search that answers fresh, so every test that warms first is fenced in one place. Co-Authored-By: Claude Fable 5.1 --- crates/tracedecay/tests/mcp_suite/support.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/tracedecay/tests/mcp_suite/support.rs b/crates/tracedecay/tests/mcp_suite/support.rs index 26c0014d42..e59a8227f6 100644 --- a/crates/tracedecay/tests/mcp_suite/support.rs +++ b/crates/tracedecay/tests/mcp_suite/support.rs @@ -297,11 +297,16 @@ pub(crate) async fn wait_for_code_index_generation(server: &McpServer, query: &s last_search = serde_json::from_str(extract_real_server_text(&result)).expect("search payload JSON"); let incomplete = common::incomplete_code_index_query_lanes(&last_search); + // `status = current` and complete lanes prove the generation, but the + // seat can still owe its source proof to a continuation pass, and a + // read taken before that pass binds it reports `verifying`. A settled + // seat answers `fresh`; take the first search that reports it. if freshness["status"] == "current" && last_search["reason"].as_str() != Some("authority_unavailable") && last_search["code_generation"].as_str() == status_generation && status_generation.is_some() && incomplete.is_empty() + && last_search["freshness"] == json!({ "state": "fresh" }) { return; } @@ -309,7 +314,7 @@ pub(crate) async fn wait_for_code_index_generation(server: &McpServer, query: &s } let incomplete = common::incomplete_code_index_query_lanes(&last_search); panic!( - "code-index search did not complete lane coverage within the polling budget: incomplete lanes={incomplete:?}; status={last_status}; search={last_search}" + "code-index search did not settle within the polling budget: incomplete lanes={incomplete:?}; status={last_status}; search={last_search}" ); } From 9b430d2336b2e2f930fd2fd534c9446cd9902991 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Mon, 21 Sep 2026 04:26:57 +0000 Subject: [PATCH 182/182] test(sessions): write the descendant pid file atomically The shutdown-guard test polled for the pid file's existence and then parsed it, but the shell's redirection creates the file before the pid is written, so a fast poll read it empty and failed on ParseIntError. The shell now writes a sibling and renames it into place, so the file only appears with its pid inside. Co-Authored-By: Claude Fable 5.1 --- .../src/runtime/hosts/codex_app_server.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/tracedecay-sessions/src/runtime/hosts/codex_app_server.rs b/crates/tracedecay-sessions/src/runtime/hosts/codex_app_server.rs index 247316c405..099e1558f8 100644 --- a/crates/tracedecay-sessions/src/runtime/hosts/codex_app_server.rs +++ b/crates/tracedecay-sessions/src/runtime/hosts/codex_app_server.rs @@ -1147,9 +1147,16 @@ mod tests { } let temp = tempfile::tempdir().unwrap(); let descendant_pid_path = temp.path().join("descendant.pid"); + // `>` creates the pid file before the shell writes into it, and a poll + // that only checks the path can read it empty. Write to a sibling and + // rename so the file appears with its pid already in it. let mut command = Command::new("sh"); command - .args(["-c", "sleep 30 & echo $! > \"$1\"; wait", "sh"]) + .args([ + "-c", + "sleep 30 & echo $! > \"$1.tmp\" && mv \"$1.tmp\" \"$1\"; wait", + "sh", + ]) .arg(&descendant_pid_path); let child = spawn_codex_app_server(&mut command, "sh").expect("spawn child"); let mut child = ChildGuard {