diff --git a/crates/tracedecay-api/src/http/application_operation_owner.rs b/crates/tracedecay-api/src/http/application_operation_owner.rs index c9cd27c105..44bacbed3a 100644 --- a/crates/tracedecay-api/src/http/application_operation_owner.rs +++ b/crates/tracedecay-api/src/http/application_operation_owner.rs @@ -156,7 +156,8 @@ pub const fn http_application_owner_kind( | ApplicationSurfaceOperation::SkillList | ApplicationSurfaceOperation::SkillView | ApplicationSurfaceOperation::HermesSkillBridge - | ApplicationSurfaceOperation::Analytics => return None, + | ApplicationSurfaceOperation::Analytics + | ApplicationSurfaceOperation::Search => return None, ApplicationSurfaceOperation::FactStoreCurate | ApplicationSurfaceOperation::FactStoreAdd | ApplicationSurfaceOperation::FactStoreSearch diff --git a/crates/tracedecay-contracts/src/graph_tool.rs b/crates/tracedecay-contracts/src/graph_tool.rs index 55c321cf11..720d615e8c 100644 --- a/crates/tracedecay-contracts/src/graph_tool.rs +++ b/crates/tracedecay-contracts/src/graph_tool.rs @@ -17,9 +17,9 @@ use crate::retrieval::{ GrepSearchResultV1, HealthResultV1, HermesSkillBridgeResultV1, HotspotsResultV1, ImpactResultV1, InheritanceDepthResultV1, LargestResultV1, NodeResultV1, PortOrderResultV1, PortStatusResultV1, PrContextResultV1, RankResultV1, RecursionResultV1, RedundancyResultV1, - RenamePreviewPrimitiveOutcomeV1, RetrieveResultV1, SignatureResultV1, SimilarResultV1, - SkillListResultV1, SkillViewResultV1, TestMapResultV1, TestRiskResultV1, TodosResultV1, - UnmountedFilesResultV1, UnsafePatternsResultV1, + RenamePreviewPrimitiveOutcomeV1, RetrieveResultV1, SearchResultV1, SignatureResultV1, + SimilarResultV1, SkillListResultV1, SkillViewResultV1, TestMapResultV1, TestRiskResultV1, + TodosResultV1, UnmountedFilesResultV1, UnsafePatternsResultV1, }; /// One graph read's typed result, tagged by its operation. @@ -82,6 +82,7 @@ pub enum GraphToolResultV1 { SkillView(Box), HermesSkillBridge(Box), Analytics(Box), + Search(Box), } impl GraphToolResultV1 { @@ -150,6 +151,7 @@ impl GraphToolResultV1 { Operation::SkillView => Self::SkillView(serde_json::from_value(value)?), Operation::HermesSkillBridge => Self::HermesSkillBridge(serde_json::from_value(value)?), Operation::Analytics => Self::Analytics(serde_json::from_value(value)?), + Operation::Search => Self::Search(serde_json::from_value(value)?), operation => { return Err(serde::de::Error::custom(format!( "{} is not a graph-tool operation", @@ -218,6 +220,7 @@ impl GraphToolResultV1 { Self::SkillView(result) => serde_json::to_value(result), Self::HermesSkillBridge(result) => serde_json::to_value(result), Self::Analytics(result) => serde_json::to_value(result), + Self::Search(result) => serde_json::to_value(result), } } } diff --git a/crates/tracedecay-contracts/src/retrieval/catalog.rs b/crates/tracedecay-contracts/src/retrieval/catalog.rs index 863b166a52..02f6cd6177 100644 --- a/crates/tracedecay-contracts/src/retrieval/catalog.rs +++ b/crates/tracedecay-contracts/src/retrieval/catalog.rs @@ -72,6 +72,7 @@ use crate::retrieval::requests::{ StorageStatusPrimitiveResult, }; use crate::retrieval::response_handle_surface::{RetrieveResultV1, RetrieveSurfaceRequestV1}; +use crate::retrieval::search_surface::{SearchResultV1, SearchSurfaceRequestV1}; use crate::retrieval::symbol_graph::{ ImplementationRecord, SymbolGraphPage, SymbolPrimitiveRecord, SymbolRelationRecord, TypeHierarchyRecord, @@ -253,6 +254,7 @@ const PRIMITIVE_READ_SPECS: &[PrimitiveReadSpec] = &[ graph_report_spec("skill_view"), graph_report_spec("hermes_skill_bridge"), graph_report_spec("analytics"), + graph_report_spec("search"), git_context_spec("affected"), git_context_spec("diff_context"), git_context_spec("changelog"), @@ -349,7 +351,8 @@ fn primitive_read_surfaces(spec: &PrimitiveReadSpec) -> &'static [BindingSurface | "skill_list" | "skill_view" | "hermes_skill_bridge" - | "analytics" => &CLI_MCP_PRIMITIVE_SURFACES, + | "analytics" + | "search" => &CLI_MCP_PRIMITIVE_SURFACES, "health_read" | "storage_status" | "diagnostics_read" => &DASHBOARD_PRIMITIVE_SURFACES, _ => &PRE_DASHBOARD_PRIMITIVE_SURFACES, } @@ -568,6 +571,9 @@ fn primitive_read_description(operation: &str) -> &'static str { "analytics" => { "Roll up tool, hint, fact-funnel and automation-run analytics for the resolved project." } + "search" => { + "Rank symbols by name, identifier fragment, signature, path, or phrase through exact and lexical routes." + } _ => "Read bounded data from the admitted project's current retained state.", } } @@ -1033,6 +1039,7 @@ fn primitive_executable_schemas( HermesSkillBridgeResultV1 ); add!("analytics", AnalyticsSurfaceRequestV1, AnalyticsResultV1); + add!("search", SearchSurfaceRequestV1, SearchResultV1); Ok(schemas) } diff --git a/crates/tracedecay-contracts/src/retrieval/mod.rs b/crates/tracedecay-contracts/src/retrieval/mod.rs index f629be0bd6..6bfe932573 100644 --- a/crates/tracedecay-contracts/src/retrieval/mod.rs +++ b/crates/tracedecay-contracts/src/retrieval/mod.rs @@ -15,6 +15,7 @@ mod primitive_surface; mod project_file_surface; mod requests; mod response_handle_surface; +mod search_surface; mod service; mod source_read; mod symbol_graph; @@ -223,6 +224,16 @@ pub use response_handle_surface::{ RetrieveHandleExpiredV1, RetrieveHandleMissingV1, RetrieveResultV1, RetrieveSurfaceRequestV1, RetrievedPageV1, }; +pub use search_surface::{ + SEARCH_MAX_LEXICAL_ALIASES, SEARCH_MAX_LEXICAL_ANCHOR_BYTES, SEARCH_MAX_LEXICAL_ANCHORS, + SEARCH_MAX_LEXICAL_PHRASES, SEARCH_MAX_LEXICAL_PROXIMITIES, SEARCH_MAX_LEXICAL_PROXIMITY_GAP, + SEARCH_MAX_LEXICAL_PROXIMITY_TERMS, SearchCompleteV1, SearchCoverageV1, + SearchExternalImportCandidatesV1, SearchExternalImportHintV1, SearchExternalImportV1, + SearchLaneStateV1, SearchLaneStatusV1, SearchLexicalAliasV1, SearchLexicalAlternativeReasonV1, + SearchLexicalFieldFilterV1, SearchLexicalFieldV1, SearchLexicalProximityV1, + SearchLexicalRouteV1, SearchResultDisplayV1, SearchResultRowV1, SearchResultV1, + SearchRouteMatchV1, SearchSpellingVariantV1, SearchSurfaceRequestV1, SearchUnavailableV1, +}; pub use source_read::{ MAX_SOURCE_READ_PATH_BYTES, SourceReadModeV1, SourceReadPortContext, SourceReadPortFuture, SourceReadPortOutcome, SourceReadPrimitivePort, SourceReadPrimitiveRequest, SourceReadResultV1, diff --git a/crates/tracedecay-contracts/src/retrieval/search_surface.rs b/crates/tracedecay-contracts/src/retrieval/search_surface.rs new file mode 100644 index 0000000000..895d6d04f7 --- /dev/null +++ b/crates/tracedecay-contracts/src/retrieval/search_surface.rs @@ -0,0 +1,297 @@ +//! Canonical CLI/MCP wire contract for `tracedecay_search`: exact and lexical +//! code search over the served code generation. +//! +//! Presentation-only transport keys such as `format` are removed before the +//! request body is decoded. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use tracedecay_domain::FusedCandidate; + +use crate::ApplicationProblemDetailV1; +use crate::retrieval::{ + ContextLexicalAnchorV1, PrimitiveLaneCompleteV1, PrimitiveRecallV1, PrimitiveSearchFreshnessV1, + PrimitiveUnavailableEvidenceV1, PrimitiveUnavailableStatusV1, +}; + +/// Caller-facing lexical routing bounds; they mirror the retrieval kernel's +/// `MAX_LEXICAL_*_V1` limits so the schema states the limits it enforces. +pub const SEARCH_MAX_LEXICAL_ANCHORS: usize = 8; +pub const SEARCH_MAX_LEXICAL_ANCHOR_BYTES: usize = 128; +pub const SEARCH_MAX_LEXICAL_ALIASES: usize = 8; +pub const SEARCH_MAX_LEXICAL_PHRASES: usize = 4; +pub const SEARCH_MAX_LEXICAL_PROXIMITIES: usize = 4; +pub const SEARCH_MAX_LEXICAL_PROXIMITY_TERMS: usize = 8; +pub const SEARCH_MAX_LEXICAL_PROXIMITY_GAP: u32 = 8; +const SEARCH_LEXICAL_FIELDS: usize = 9; + +#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct SearchSurfaceRequestV1 { + /// Search query string to match against symbol names + pub query: String, + /// Maximum number of results to return (default: 10) + pub limit: Option, + /// Authenticated opaque continuation returned as next_cursor. Repeat the same query and lexical options with it. + pub cursor: Option, + /// Exact identifiers or technical terms (e.g. 'reserve_stock', 'Foo::bar', 'E0308') that the answer must be about. Each is ranked through the lexical lane as its own route: a hit carrying an anchor outranks every hit that carries none, exact hits included, every anchor with matches keeps at least its best sites through the lane cap, and `lexical_anchors` in the response reports each anchor's outcome (`matched` rows, `admitted` sites this response returns, `dropped` admitted sites it could not carry with the reason, `unmatched`, or `not_served`). Ranked retrieval, not exhaustive grep (use tracedecay_grep for that). Each result names the routes that ranked it. At most 8 anchors, each one whitespace-free term of at most 128 bytes, no repeats. + #[schemars(length(max = SEARCH_MAX_LEXICAL_ANCHORS))] + pub lexical_anchors: Option>, + /// Add a lexical route restricted to symbol-name matches for the identifier-shaped words of the query (default: false). Query words such as class/struct/function/find/explain are ignored; 'Foo::bar' and 'Foo.bar' contribute 'bar'. + pub prefer_symbol: Option, + /// Named query-time vocabulary aliases. The strict query always ranks first. Alias-only hits follow it with the strict query, alternative, and configured-vocabulary reason disclosed. + #[schemars(length(max = SEARCH_MAX_LEXICAL_ALIASES))] + pub lexical_aliases: Option>, + /// Exact lexical phrases to rank through n-gram candidate pruning. + #[schemars(length(max = SEARCH_MAX_LEXICAL_PHRASES))] + pub lexical_phrases: Option>, + /// Ordered lexical terms that must occur within the bounded intervening-token gap. + #[schemars(length(max = SEARCH_MAX_LEXICAL_PROXIMITIES))] + pub lexical_proximities: Option>, + /// Include or exclude lexical symbol_name, qualified_name, path, signature, documentation, body_text, preamble_text, exact_term, or subtoken fields. + #[schemars(length(max = SEARCH_LEXICAL_FIELDS))] + pub lexical_field_filters: Option>, + /// Opt in to bounded indexing of ignored dependency entry files when an import hint matches (default: false). + pub lazy_index_ignored_dependencies: Option, +} + +/// One named query-time vocabulary alias. +#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct SearchLexicalAliasV1 { + #[schemars(length(max = SEARCH_MAX_LEXICAL_ANCHOR_BYTES))] + pub strict_query: String, + #[schemars(length(max = SEARCH_MAX_LEXICAL_ANCHOR_BYTES))] + pub alternative: String, +} + +/// Ordered terms that must occur in one field within `maximum_gap` +/// intervening tokens of each other. +#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct SearchLexicalProximityV1 { + #[schemars(length(min = 2, max = SEARCH_MAX_LEXICAL_PROXIMITY_TERMS))] + pub terms: Vec, + #[schemars(range(max = SEARCH_MAX_LEXICAL_PROXIMITY_GAP))] + pub maximum_gap: u32, +} + +/// A lexical posting field a search may include or exclude. +#[derive(Clone, Copy, Debug, Deserialize, JsonSchema, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum SearchLexicalFieldV1 { + SymbolName, + QualifiedName, + Path, + Signature, + Documentation, + BodyText, + PreambleText, + ExactTerm, + Subtoken, +} + +#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct SearchLexicalFieldFilterV1 { + pub field: SearchLexicalFieldV1, + pub include: bool, +} + +/// What a search served: a ranked page, or the typed reason no generation +/// could answer. +#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)] +#[serde(untagged)] +pub enum SearchResultV1 { + Complete(SearchCompleteV1), + Unavailable(SearchUnavailableV1), +} + +#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct SearchCompleteV1 { + /// Freshness of the served code generation, derived from the typed lane + /// coverage and the daemon scheduler's worktree state. + pub freshness: PrimitiveSearchFreshnessV1, + pub code_generation: String, + pub query_fallback_digest: String, + /// Authenticated continuation for the next page; null on the last page. + pub next_cursor: Option, + pub coverage: SearchCoverageV1, + pub results: Vec, + /// Every lexical route fused into this page; present only when a route + /// beyond the strict query ran or ranked a result. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub lexical_routes: Option>, + /// Outcome of every caller `lexical_anchors` entry, beside the routes. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub lexical_anchors: Option>, + /// The session's scope prefix. Ranked candidates carry no file path, so + /// the scope is reported and not applied. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub scope_prefix: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub scope_prefix_applied: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub verified_graph_evidence: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub external_import_hint: Option, +} + +#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct SearchUnavailableV1 { + pub freshness: PrimitiveSearchFreshnessV1, + /// Always empty: no generation answered. + pub results: Vec, + pub code_generation: Option, + /// Always null: no fallback subpayload was produced. + pub query_fallback_digest: Option, + pub status: PrimitiveUnavailableStatusV1, + pub reason: String, + pub coverage: SearchCoverageV1, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub verified_graph_evidence: Option, + /// Present when indexing is parked: its cause, remedy, and wake retry. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub detail: Option, +} + +/// Per-lane recall of one search, so a full-recall answer is told apart from +/// one produced while a lane was down. +#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct SearchCoverageV1 { + pub exact: SearchLaneStatusV1, + pub lexical: SearchLaneStatusV1, + pub graph: SearchLaneStatusV1, + pub recall: PrimitiveRecallV1, +} + +#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Eq, Serialize)] +#[serde(untagged)] +pub enum SearchLaneStatusV1 { + Complete(PrimitiveLaneCompleteV1), + State(SearchLaneStateV1), +} + +/// A lane that did not serve the current complete generation. A partial +/// lane names its generation and reason when it has them, null otherwise. +#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Eq, Serialize)] +#[serde(tag = "status", rename_all = "snake_case", deny_unknown_fields)] +pub enum SearchLaneStateV1 { + Stale { + generation: String, + }, + Partial { + generation: Option, + reason: Option, + }, + Unavailable { + reason: String, + }, +} + +/// One ranked candidate with its generation-bound display metadata. +#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct SearchResultRowV1 { + pub candidate: FusedCandidate, + pub final_ordinal: u32, + /// The graph symbol to read with `tracedecay_source_body`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub node_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub display: Option, + /// The lexical routes that ranked this candidate. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub lexical_routes: Option>, +} + +#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct SearchResultDisplayV1 { + pub name: String, + pub qualified_name: String, + pub kind: String, + pub path: String, +} + +/// One route that ranked a candidate, named by its route label. +#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct SearchRouteMatchV1 { + pub route: String, + pub score_micros: u64, + pub matched_terms: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub spelling_variants: Vec, +} + +#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct SearchSpellingVariantV1 { + pub query: String, + pub alternative: String, +} + +/// Why the lexical lane tried a configured alternative. +#[derive(Clone, Copy, Debug, Deserialize, JsonSchema, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum SearchLexicalAlternativeReasonV1 { + ConfiguredVocabularyAlias, +} + +/// One lexical route fused into the page, with its display label. +#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Eq, Serialize)] +#[serde(tag = "route", rename_all = "snake_case", deny_unknown_fields)] +pub enum SearchLexicalRouteV1 { + /// The caller's strict lexical query. + Query { label: String }, + /// One caller-supplied exact identifier or term. + Anchor { anchor: String, label: String }, + /// Identifier-shaped tokens of the query, restricted to symbol names. + PreferredSymbol { tokens: Vec, label: String }, + /// Identifier and path components recovered from the strict query. + IdentifierSplit { + strict_query: String, + terms: Vec, + label: String, + }, + /// A configured query-time vocabulary alternative. + Alias { + strict_query: String, + alternative: String, + reason: SearchLexicalAlternativeReasonV1, + label: String, + }, +} + +/// Advisory evidence that a sparse search matched external-module imports, +/// or why that evidence could not be read. +#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Eq, Serialize)] +#[serde(untagged)] +pub enum SearchExternalImportHintV1 { + Candidates(SearchExternalImportCandidatesV1), + Unavailable(PrimitiveUnavailableEvidenceV1), +} + +#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct SearchExternalImportCandidatesV1 { + pub message: String, + pub evidence: String, + pub resolution_status: String, + pub candidates: Vec, + pub suggested_action: String, +} + +#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct SearchExternalImportV1 { + pub module: String, + /// The imported name; null for a whole-module import. + pub symbol: Option, + pub import_file: String, + pub line: u32, +} diff --git a/crates/tracedecay-contracts/src/sdk_catalog.rs b/crates/tracedecay-contracts/src/sdk_catalog.rs index 93bd2b829b..fac24ce959 100644 --- a/crates/tracedecay-contracts/src/sdk_catalog.rs +++ b/crates/tracedecay-contracts/src/sdk_catalog.rs @@ -164,7 +164,8 @@ pub fn application_http_route_path(operation: ApplicationSurfaceOperation) -> St | ApplicationSurfaceOperation::SkillList | ApplicationSurfaceOperation::SkillView | ApplicationSurfaceOperation::HermesSkillBridge - | ApplicationSurfaceOperation::Analytics) => { + | ApplicationSurfaceOperation::Analytics + | ApplicationSurfaceOperation::Search) => { format!("/primitives/{}", operation.as_str()) } operation @ (ApplicationSurfaceOperation::ConfigurationList diff --git a/crates/tracedecay-daemon-protocol/src/application_surface.rs b/crates/tracedecay-daemon-protocol/src/application_surface.rs index af4427d272..664db7168c 100644 --- a/crates/tracedecay-daemon-protocol/src/application_surface.rs +++ b/crates/tracedecay-daemon-protocol/src/application_surface.rs @@ -940,7 +940,8 @@ pub fn parse_application_surface_request( | ApplicationSurfaceOperation::SkillList | ApplicationSurfaceOperation::SkillView | ApplicationSurfaceOperation::HermesSkillBridge - | ApplicationSurfaceOperation::Analytics => match value { + | ApplicationSurfaceOperation::Analytics + | ApplicationSurfaceOperation::Search => match value { Value::Object(arguments) => Ok(ApplicationSurfaceRequest::GraphTool(arguments)), _ => Err(ApplicationSurfaceAdapterError::invalid_request(format!( "invalid arguments: {} expects a JSON object", diff --git a/crates/tracedecay-daemon-protocol/src/application_surface/invocation.rs b/crates/tracedecay-daemon-protocol/src/application_surface/invocation.rs index 696e8109af..f36e597ebc 100644 --- a/crates/tracedecay-daemon-protocol/src/application_surface/invocation.rs +++ b/crates/tracedecay-daemon-protocol/src/application_surface/invocation.rs @@ -905,6 +905,7 @@ pub fn application_surface_feedback_operation( | ApplicationSurfaceOperation::SkillView | ApplicationSurfaceOperation::HermesSkillBridge | ApplicationSurfaceOperation::Analytics + | ApplicationSurfaceOperation::Search | ApplicationSurfaceOperation::HealthRead | ApplicationSurfaceOperation::HealthDelta | ApplicationSurfaceOperation::StorageStatus diff --git a/crates/tracedecay-daemon-protocol/src/contract/mod.rs b/crates/tracedecay-daemon-protocol/src/contract/mod.rs index e5cb968f5e..0cdb87bd77 100644 --- a/crates/tracedecay-daemon-protocol/src/contract/mod.rs +++ b/crates/tracedecay-daemon-protocol/src/contract/mod.rs @@ -1039,7 +1039,8 @@ impl DaemonInvocationRequest { | ApplicationSurfaceOperation::SkillList | ApplicationSurfaceOperation::SkillView | ApplicationSurfaceOperation::HermesSkillBridge - | ApplicationSurfaceOperation::Analytics => { + | ApplicationSurfaceOperation::Analytics + | ApplicationSurfaceOperation::Search => { unreachable!("graph-tool operations use their typed constructor") } ApplicationSurfaceOperation::FactStoreCurate diff --git a/crates/tracedecay-mcp-catalog/src/definitions.rs b/crates/tracedecay-mcp-catalog/src/definitions.rs index a1563f5767..137fea9082 100644 --- a/crates/tracedecay-mcp-catalog/src/definitions.rs +++ b/crates/tracedecay-mcp-catalog/src/definitions.rs @@ -47,7 +47,6 @@ use ast_grep::ast_grep_available; use edit::*; use git::*; use graph::*; -pub use graph::{SEARCH_MAX_LEXICAL_ANCHOR_BYTES, SEARCH_MAX_LEXICAL_ANCHORS}; use lcm::*; use multi_root::*; use skills::*; @@ -322,7 +321,7 @@ fn build_maximal_tool_definitions() -> Result, McpCatalogErr canonical_application_request_schema(application_registry, operation) }; let mut definitions = vec![ - def_search(), + def_search(request_schema("search")?), def_grep(request_schema("grep")?), def_ast_grep_search(request_schema("ast_grep_search")?), def_retrieve(request_schema("retrieve")?), diff --git a/crates/tracedecay-mcp-catalog/src/definitions/application.rs b/crates/tracedecay-mcp-catalog/src/definitions/application.rs index ee42a7fda2..f01aa1e159 100644 --- a/crates/tracedecay-mcp-catalog/src/definitions/application.rs +++ b/crates/tracedecay-mcp-catalog/src/definitions/application.rs @@ -82,6 +82,7 @@ const HANDWRITTEN_DEFINITION_OPERATIONS: &[ApplicationSurfaceOperation] = &[ ApplicationSurfaceOperation::SkillView, ApplicationSurfaceOperation::HermesSkillBridge, ApplicationSurfaceOperation::Analytics, + ApplicationSurfaceOperation::Search, ApplicationSurfaceOperation::StrReplace, ApplicationSurfaceOperation::MultiStrReplace, ApplicationSurfaceOperation::InsertAt, diff --git a/crates/tracedecay-mcp-catalog/src/definitions/graph.rs b/crates/tracedecay-mcp-catalog/src/definitions/graph.rs index d496071c50..112fc79d84 100644 --- a/crates/tracedecay-mcp-catalog/src/definitions/graph.rs +++ b/crates/tracedecay-mcp-catalog/src/definitions/graph.rs @@ -1,122 +1,18 @@ //! Graph query and navigation tool definitions. -use serde_json::{Value, json}; +use serde_json::Value; use super::{context_description, def, def_always_load}; use crate::ToolDefinition; // ── alwaysLoad tools (loaded into the model prompt immediately) ───────── -/// Caller-facing bounds for `lexical_anchors`; they mirror the retrieval -/// kernel's `MAX_LEXICAL_ANCHORS_V1` / `MAX_LEXICAL_ANCHOR_BYTES_V1` so the -/// schema states the same limit the handler enforces. -pub const SEARCH_MAX_LEXICAL_ANCHORS: usize = 8; -pub const SEARCH_MAX_LEXICAL_ANCHOR_BYTES: usize = 128; -const SEARCH_MAX_LEXICAL_ALIASES: usize = 8; -const SEARCH_MAX_LEXICAL_PHRASES: usize = 4; -const SEARCH_MAX_LEXICAL_PROXIMITIES: usize = 4; -const SEARCH_MAX_LEXICAL_PROXIMITY_TERMS: usize = 8; -const SEARCH_MAX_LEXICAL_PROXIMITY_GAP: u32 = 8; - -pub(super) fn def_search() -> ToolDefinition { +pub(super) fn def_search(input_schema: Value) -> ToolDefinition { def_always_load( "tracedecay_search", "Search Symbols", "Exact and lexical code search over the active project's code graph: find symbols (functions, structs, traits, etc.) by name, identifier fragment, signature, path, or phrase through ranked exact and lexical routes. Every response opens with a `freshness: fresh | possibly_stale` line, so no status preflight is needed. Pass known identifiers as `lexical_anchors` (each an extra ranked route) and set `prefer_symbol` to add a symbol-name route for identifier-shaped query words.", - json!({ - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "Search query string to match against symbol names" - }, - "limit": { - "type": "number", - "description": "Maximum number of results to return (default: 10)" - }, - "cursor": { - "type": "string", - "description": "Authenticated opaque continuation returned as next_cursor. Repeat the same query and lexical options with it." - }, - "lexical_anchors": { - "type": "array", - "items": { "type": "string" }, - "maxItems": SEARCH_MAX_LEXICAL_ANCHORS, - "description": format!( - "Exact identifiers or technical terms (e.g. 'reserve_stock', 'Foo::bar', 'E0308') that the answer must be about. Each is ranked through the lexical lane as its own route: a hit carrying an anchor outranks every hit that carries none, exact hits included, every anchor with matches keeps at least its best sites through the lane cap, and `lexical_anchors` in the response reports each anchor's outcome (`matched` rows, `admitted` sites this response returns, `dropped` admitted sites it could not carry with the reason, `unmatched`, or `not_served`). Ranked retrieval, not exhaustive grep (use tracedecay_grep for that). Each result names the routes that ranked it. At most {SEARCH_MAX_LEXICAL_ANCHORS} anchors, each one whitespace-free term of at most {SEARCH_MAX_LEXICAL_ANCHOR_BYTES} bytes, no repeats." - ) - }, - "prefer_symbol": { - "type": "boolean", - "description": "Add a lexical route restricted to symbol-name matches for the identifier-shaped words of the query (default: false). Query words such as class/struct/function/find/explain are ignored; 'Foo::bar' and 'Foo.bar' contribute 'bar'." - }, - "lexical_aliases": { - "type": "array", - "maxItems": SEARCH_MAX_LEXICAL_ALIASES, - "description": "Named query-time vocabulary aliases. The strict query always ranks first. Alias-only hits follow it with the strict query, alternative, and configured-vocabulary reason disclosed.", - "items": { - "type": "object", - "additionalProperties": false, - "required": ["strict_query", "alternative"], - "properties": { - "strict_query": { "type": "string", "maxLength": SEARCH_MAX_LEXICAL_ANCHOR_BYTES }, - "alternative": { "type": "string", "maxLength": SEARCH_MAX_LEXICAL_ANCHOR_BYTES } - } - } - }, - "lexical_phrases": { - "type": "array", - "maxItems": SEARCH_MAX_LEXICAL_PHRASES, - "description": "Exact lexical phrases to rank through n-gram candidate pruning.", - "items": { "type": "string" } - }, - "lexical_proximities": { - "type": "array", - "maxItems": SEARCH_MAX_LEXICAL_PROXIMITIES, - "description": "Ordered lexical terms that must occur within the bounded intervening-token gap.", - "items": { - "type": "object", - "additionalProperties": false, - "required": ["terms", "maximum_gap"], - "properties": { - "terms": { - "type": "array", - "minItems": 2, - "maxItems": SEARCH_MAX_LEXICAL_PROXIMITY_TERMS, - "items": { "type": "string" } - }, - "maximum_gap": { - "type": "integer", - "minimum": 0, - "maximum": SEARCH_MAX_LEXICAL_PROXIMITY_GAP - } - } - } - }, - "lexical_field_filters": { - "type": "array", - "maxItems": 9, - "description": "Include or exclude lexical symbol_name, qualified_name, path, signature, documentation, body_text, preamble_text, exact_term, or subtoken fields.", - "items": { - "type": "object", - "additionalProperties": false, - "required": ["field", "include"], - "properties": { - "field": { - "type": "string", - "enum": ["symbol_name", "qualified_name", "path", "signature", "documentation", "body_text", "preamble_text", "exact_term", "subtoken"] - }, - "include": { "type": "boolean" } - } - } - }, - "lazy_index_ignored_dependencies": { - "type": "boolean", - "description": "Opt in to bounded indexing of ignored dependency entry files when an import hint matches (default: false)." - } - }, - "required": ["query"] - }), + input_schema, ) } @@ -300,47 +196,75 @@ pub(super) fn def_find_exact_symbol(input_schema: Value) -> ToolDefinition { #[cfg(test)] mod search_schema_tests { - use super::{ + use tracedecay_contracts::retrieval::{ SEARCH_MAX_LEXICAL_ALIASES, SEARCH_MAX_LEXICAL_ANCHORS, SEARCH_MAX_LEXICAL_PROXIMITY_GAP, - def_search, }; + fn search_schema() -> serde_json::Value { + crate::get_maximal_tool_definitions() + .expect("tool definitions") + .into_iter() + .find(|definition| definition.name == "tracedecay_search") + .expect("tracedecay_search definition") + .input_schema + } + + /// The schema a `$ref` into the tool schema's `$defs` names, or `node`. + fn definition<'a>( + schema: &'a serde_json::Value, + node: &'a serde_json::Value, + ) -> &'a serde_json::Value { + match node["$ref"] + .as_str() + .and_then(|reference| reference.strip_prefix("#/$defs/")) + { + Some(name) => &schema["$defs"][name], + None => node, + } + } + #[test] fn search_schema_has_no_semantic_mode_and_keeps_the_cursor() { - let definition = def_search(); - assert!(definition.input_schema["properties"]["semantic_mode"].is_null()); + let schema = search_schema(); + assert!(schema["properties"]["semantic_mode"].is_null()); assert_eq!( - definition.input_schema["properties"]["cursor"]["type"], - "string" + schema["properties"]["cursor"]["type"], + serde_json::json!(["string", "null"]) ); } #[test] fn search_schema_declares_lexical_routing_parameters() { - let definition = def_search(); - let anchors = &definition.input_schema["properties"]["lexical_anchors"]; - assert_eq!(anchors["type"], "array"); + let schema = search_schema(); + let anchors = &schema["properties"]["lexical_anchors"]; + assert_eq!(anchors["type"], serde_json::json!(["array", "null"])); assert_eq!(anchors["items"]["type"], "string"); assert_eq!( anchors["maxItems"], serde_json::json!(SEARCH_MAX_LEXICAL_ANCHORS) ); assert_eq!( - definition.input_schema["properties"]["prefer_symbol"]["type"], - "boolean" + schema["properties"]["prefer_symbol"]["type"], + serde_json::json!(["boolean", "null"]) ); assert_eq!( - definition.input_schema["properties"]["lexical_aliases"]["maxItems"], + schema["properties"]["lexical_aliases"]["maxItems"], serde_json::json!(SEARCH_MAX_LEXICAL_ALIASES) ); + let proximity = definition( + &schema, + &schema["properties"]["lexical_proximities"]["items"], + ); assert_eq!( - definition.input_schema["properties"]["lexical_proximities"]["items"]["properties"]["maximum_gap"] - ["maximum"], + proximity["properties"]["maximum_gap"]["maximum"], serde_json::json!(SEARCH_MAX_LEXICAL_PROXIMITY_GAP) ); + let filter = definition( + &schema, + &schema["properties"]["lexical_field_filters"]["items"], + ); assert!( - definition.input_schema["properties"]["lexical_field_filters"]["items"]["properties"] - ["field"]["enum"] + definition(&schema, &filter["properties"]["field"])["enum"] .as_array() .is_some_and(|fields| fields.contains(&serde_json::json!("documentation"))) ); diff --git a/crates/tracedecay-mcp-catalog/src/lib.rs b/crates/tracedecay-mcp-catalog/src/lib.rs index e06e780e8f..d30e43d868 100644 --- a/crates/tracedecay-mcp-catalog/src/lib.rs +++ b/crates/tracedecay-mcp-catalog/src/lib.rs @@ -36,10 +36,9 @@ mod project_access; pub use catalog_error::McpCatalogError; pub use definitions::ast_grep::{ast_grep_available, ast_grep_diagnostics_json}; pub use definitions::{ - SEARCH_MAX_LEXICAL_ANCHOR_BYTES, SEARCH_MAX_LEXICAL_ANCHORS, ToolRegistryMode, - apply_context_warming_budget, context_description, context_warming_description, - explore_call_budget, format_capable_tool_names, get_maximal_tool_definitions, - get_maximal_tool_definitions_with_budget, get_tool_definitions, + ToolRegistryMode, apply_context_warming_budget, context_description, + context_warming_description, explore_call_budget, format_capable_tool_names, + get_maximal_tool_definitions, get_maximal_tool_definitions_with_budget, get_tool_definitions, get_tool_definitions_with_budget, get_tool_definitions_with_warming_budget, internal_daemon_tool_definition, mcp_input_schema, project_catalog_discovery_scope, retain_host_available_tool_definitions, tool_defaults_to_markdown, diff --git a/crates/tracedecay-mcp/src/handlers/dependency_hints.rs b/crates/tracedecay-mcp/src/handlers/dependency_hints.rs index 5332cb4889..5a9b86f7ed 100644 --- a/crates/tracedecay-mcp/src/handlers/dependency_hints.rs +++ b/crates/tracedecay-mcp/src/handlers/dependency_hints.rs @@ -1,11 +1,12 @@ -use serde_json::{Value, json}; +use serde_json::Value; use tracedecay_application::code_index::{ CodeIndexIgnoredDependencyAdmissionErrorV1, CodeIndexIgnoredDependencyAdmissionPortV1, CodeIndexIgnoredDependencyAdmissionRequestV1, }; use tracedecay_code_index::chunks::CodeIndexImportEvidenceV1; use tracedecay_contracts::retrieval::{ - PrimitiveUnavailableEvidenceV1, PrimitiveUnavailableStatusV1, + PrimitiveUnavailableEvidenceV1, PrimitiveUnavailableStatusV1, SearchExternalImportCandidatesV1, + SearchExternalImportV1, }; use tracedecay_domain::errors::{Result, TraceDecayError}; @@ -19,12 +20,6 @@ pub fn should_check_external_import_hint(result_count: usize, limit: usize) -> b result_count == 0 || result_count < limit.clamp(1, 20) } -pub fn lazy_indexing_requested(args: &Value) -> bool { - args.get("lazy_index_ignored_dependencies") - .and_then(Value::as_bool) - .unwrap_or(false) -} - /// Advisory external-import evidence for a sparse search. /// /// The bound context carries the graph's admitted scope plus the caller's @@ -37,25 +32,28 @@ pub fn external_import_hint( query: &str, limit: usize, scope_prefix: Option<&str>, -) -> Result> { +) -> Result> { let candidates = hotpath::measure_block!("mcp.search.import_hint.scan", { ignored_dependency_candidates(ctx, graph, query, limit, scope_prefix)? }); if candidates.is_empty() { return Ok(None); } - Ok(Some(json!({ - "message": "Search results were sparse, and parser-backed imports contain matching external-module specifiers. Module resolution and ignored-source status are not verified by this advisory read.", - "evidence": "parser_external_module_specifier", - "resolution_status": "unverified", - "candidates": candidates.into_iter().map(|candidate| json!({ - "module": candidate.module_specifier, - "symbol": candidate.imported_name, - "import_file": candidate.logical_path, - "line": user_line(candidate.start_line), - })).collect::>(), - "suggested_action": "verify_external_import_before_lazy_indexing", - }))) + Ok(Some(SearchExternalImportCandidatesV1 { + message: "Search results were sparse, and parser-backed imports contain matching external-module specifiers. Module resolution and ignored-source status are not verified by this advisory read.".to_owned(), + evidence: "parser_external_module_specifier".to_owned(), + resolution_status: "unverified".to_owned(), + candidates: candidates + .into_iter() + .map(|candidate| SearchExternalImportV1 { + module: candidate.module_specifier, + symbol: candidate.imported_name, + import_file: candidate.logical_path, + line: user_line(candidate.start_line), + }) + .collect(), + suggested_action: "verify_external_import_before_lazy_indexing".to_owned(), + })) } pub fn unavailable_evidence(error: &TraceDecayError) -> PrimitiveUnavailableEvidenceV1 { @@ -79,10 +77,6 @@ pub fn unavailable_evidence(error: &TraceDecayError) -> PrimitiveUnavailableEvid } } -pub fn unavailable_hint(error: &TraceDecayError) -> Value { - json!(unavailable_evidence(error)) -} - #[hotpath::measure(label = "mcp.search.import_admit.total")] pub async fn admit_verified_ignored_dependency( ctx: &McpToolContext<'_>, diff --git a/crates/tracedecay-mcp/src/handlers/graph/clones.rs b/crates/tracedecay-mcp/src/handlers/graph/clones.rs new file mode 100644 index 0000000000..a16c306e62 --- /dev/null +++ b/crates/tracedecay-mcp/src/handlers/graph/clones.rs @@ -0,0 +1,617 @@ +//! `tracedecay_similar` and `tracedecay_redundancy`: the clone-family reads. + +use serde_json::Value; +use tracedecay_contracts::graph_tool::{GraphToolCompletionV1, GraphToolResultV1}; +use tracedecay_contracts::retrieval::{ + RedundancyScopeV1, RedundancySurfaceRequestV1, SimilarCoverageV1, SimilarFamilyV1, + SimilarMatchClassV1, SimilarOccurrenceV1, SimilarResultV1, SimilarSurfaceRequestV1, + SimilarTargetV1, +}; +use tracedecay_domain::errors::{Result, TraceDecayError}; + +use crate::McpToolContext; +use crate::handlers::support::decode_primitive_request; + +use super::graph_tool_completion; + +#[hotpath::measure(label = "mcp.graph.similar.total")] +pub async fn compute_similar( + ctx: &McpToolContext<'_>, + args: Value, +) -> Result { + let request: SimilarSurfaceRequestV1 = decode_primitive_request(&args, "tracedecay_similar")?; + let project_id = request.project_id; + let repository_id = request.repository_id; + let target = match request.target { + SimilarTargetV1::SymbolOccurrence { + symbol_occurrence_id, + } => tracedecay_query::code_search::CodeIndexSimilarTargetV1::SymbolOccurrence( + symbol_occurrence_id, + ), + SimilarTargetV1::SourceRange { path, span } => { + tracedecay_query::code_search::CodeIndexSimilarTargetV1::SourceRange { path, span } + } + }; + let match_classes = request + .match_classes + .iter() + .map(|class| match class { + SimilarMatchClassV1::ConservativeExact => { + tracedecay_code_index::clones::CloneNormalizationClassV1::Conservative + } + SimilarMatchClassV1::RenameNormalizedExact => { + tracedecay_code_index::clones::CloneNormalizationClassV1::Rename + } + }) + .collect(); + let cursor = request + .cursor + .as_deref() + .map(tracedecay_query::retrieval::lexical::CloneArtifactCursorV1::decode) + .transpose() + .map_err(|error| TraceDecayError::Config { + message: format!("invalid tracedecay_similar cursor: {error}"), + })?; + let executor = ctx.code_index_similar_executor().ok_or_else(|| { + clone_lane_unavailable_error( + "similarity", + tracedecay_query::code_search::CodeIndexSearchUnavailableReasonV1::CapabilityUnavailable, + ) + })?; + let similar = match executor(tracedecay_query::code_search::CodeIndexSimilarRequestV1 { + project_root: ctx.project_root().to_path_buf(), + target, + match_classes, + result_limit: request.result_limit as usize, + work_limit: request.work_limit as usize, + cursor, + authority: ctx.code_index_search_authority().cloned(), + deadline: ctx.deadline().cloned(), + cancellation: ctx.cancellation().cloned(), + }) + .await + { + tracedecay_query::code_search::CodeIndexSimilarOutcomeV1::Complete(similar) => *similar, + tracedecay_query::code_search::CodeIndexSimilarOutcomeV1::NotFound => { + return Err(TraceDecayError::ProjectRoute { + reason_code: "similar-source-not-found".to_owned(), + retryable: false, + detail: "the selected source has no body in the verified clone index".to_owned(), + }); + } + tracedecay_query::code_search::CodeIndexSimilarOutcomeV1::Unavailable(reason) => { + return Err(clone_lane_unavailable_error("similarity", reason)); + } + }; + if similar.source.occurrence.project_id != project_id + || similar.source.occurrence.repository_id != repository_id + { + return Err(TraceDecayError::ProjectRoute { + reason_code: "similar-source-not-found".to_owned(), + retryable: false, + detail: "the selected source is outside the authorized repository scope".to_owned(), + }); + } + let source = similar_occurrence(&similar.source.occurrence); + let mut touched_files = vec![source.path.clone()]; + let mut complete = true; + let families = similar + .exact_groups + .into_iter() + .map(|group| -> Result { + complete &= group.complete; + let match_class = match group.key.class { + tracedecay_code_index::clones::CloneNormalizationClassV1::Conservative => { + SimilarMatchClassV1::ConservativeExact + } + tracedecay_code_index::clones::CloneNormalizationClassV1::Rename => { + SimilarMatchClassV1::RenameNormalizedExact + } + }; + let members = group + .members + .into_iter() + .filter(|member| { + member.occurrence.project_id == project_id + && member.occurrence.repository_id == repository_id + }) + .map(|member| { + let occurrence = similar_occurrence(&member.occurrence); + touched_files.push(occurrence.path.clone()); + occurrence + }) + .collect::>(); + let next_cursor = group + .next_cursor + .as_ref() + .map(tracedecay_query::retrieval::lexical::CloneArtifactCursorV1::encode) + .transpose() + .map_err(|error| TraceDecayError::Config { + message: format!("failed to encode tracedecay_similar cursor: {error}"), + })?; + Ok(SimilarFamilyV1 { + match_class, + normalization_revision: group.key.normalization_revision, + family_digest: group.key.digest, + representative_payload_digest: similar.source.payload.payload_digest.clone(), + member_count: members.len(), + members, + complete: group.complete, + next_cursor, + }) + }) + .collect::>>()?; + touched_files.sort(); + touched_files.dedup(); + let coverage = match similar.source.occurrence.eligibility { + tracedecay_code_index::clones::CloneBodyEligibilityV1::Eligible if complete => { + SimilarCoverageV1::Complete + } + tracedecay_code_index::clones::CloneBodyEligibilityV1::Eligible => { + SimilarCoverageV1::Partial + } + tracedecay_code_index::clones::CloneBodyEligibilityV1::ExcludedTooSmall { + minimum_tokens, + } => SimilarCoverageV1::ExcludedTooSmall { minimum_tokens }, + tracedecay_code_index::clones::CloneBodyEligibilityV1::ExcludedTooLarge { + maximum_tokens, + maximum_bytes, + } => SimilarCoverageV1::ExcludedTooLarge { + maximum_tokens, + maximum_bytes, + }, + tracedecay_code_index::clones::CloneBodyEligibilityV1::ExcludedIncompleteTokenization => { + SimilarCoverageV1::ExcludedIncompleteTokenization + } + }; + let result = SimilarResultV1 { + source_generation: source.source_generation.clone(), + source, + families, + coverage, + }; + Ok(graph_tool_completion( + GraphToolResultV1::Similar(result), + touched_files, + )) +} + +/// The one clone-family unavailable wire shape. `tracedecay_similar` and +/// `tracedecay_redundancy` read the same executor vocabulary, so they report +/// the same `reason_code` and the same retry verdict; only the human lane name +/// in `detail` differs. +/// +/// Deliberately does **not** re-emit retired opaque tokens +/// (`verified-code-similarity-unavailable` / +/// `verified-code-redundancy-unavailable`). Those never shipped on master; the +/// shared `reason.as_str()` vocabulary is the sole consumer-visible code. +fn clone_lane_unavailable_error( + lane: &str, + reason: tracedecay_query::code_search::CodeIndexSearchUnavailableReasonV1, +) -> TraceDecayError { + TraceDecayError::ProjectRoute { + reason_code: reason.as_str().to_owned(), + retryable: reason.is_retryable(), + detail: format!( + "the maintained clone {lane} lane is unavailable: {}", + reason.as_str() + ), + } +} + +#[hotpath::measure(label = "mcp.graph.redundancy.total")] +pub async fn compute_redundancy( + ctx: &McpToolContext<'_>, + args: Value, +) -> Result { + let request: RedundancySurfaceRequestV1 = + decode_primitive_request(&args, "tracedecay_redundancy")?; + if request.project_id != ctx.admitted_scope().project_id + || request.repository_id != ctx.admitted_scope().repository_id + { + return Err(TraceDecayError::ProjectRoute { + reason_code: "redundancy-repository-not-authorized".to_owned(), + retryable: false, + detail: "the selected repository is outside the authorized repository scope".to_owned(), + }); + } + let match_classes = request + .match_classes + .iter() + .map(|class| match class { + SimilarMatchClassV1::ConservativeExact => { + tracedecay_code_index::clones::CloneNormalizationClassV1::Conservative + } + SimilarMatchClassV1::RenameNormalizedExact => { + tracedecay_code_index::clones::CloneNormalizationClassV1::Rename + } + }) + .collect(); + let scope = match request.scope { + RedundancyScopeV1::Repository => { + tracedecay_query::code_search::CodeIndexRedundancyScopeV1::Repository + } + RedundancyScopeV1::Path { path } => { + tracedecay_query::code_search::CodeIndexRedundancyScopeV1::Path(path) + } + RedundancyScopeV1::PullRequest { + provider, + pull_request_id, + head_commit_id, + mut changed_paths, + } => { + changed_paths.sort(); + changed_paths.dedup(); + let pull_request_id = tracedecay_domain::feedback::GitHubPullRequestIdV1::new( + pull_request_id, + ) + .map_err(|error| TraceDecayError::Config { + message: format!("invalid arguments for tracedecay_redundancy: {error}"), + })?; + tracedecay_query::code_search::CodeIndexRedundancyScopeV1::PullRequest { + provider, + pull_request_id, + head_commit_id, + changed_paths, + } + } + }; + let executor = ctx.code_index_redundancy_executor().ok_or_else(|| { + clone_lane_unavailable_error( + "family", + tracedecay_query::code_search::CodeIndexSearchUnavailableReasonV1::CapabilityUnavailable, + ) + })?; + let outcome = executor(tracedecay_query::code_search::CodeIndexRedundancyQueryV1 { + project_root: ctx.project_root().to_path_buf(), + project_id: request.project_id, + repository_id: request.repository_id, + match_classes, + scope, + include_generated_paths: request.include_generated_paths, + family_limit: request.family_limit as usize, + member_limit: request.member_limit as usize, + work_limit: request.work_limit as usize, + cursor: request.cursor, + authority: ctx.code_index_search_authority().cloned(), + deadline: ctx.deadline().cloned(), + cancellation: ctx.cancellation().cloned(), + }) + .await + .map_err(|reason| clone_lane_unavailable_error("family", reason))?; + let mut touched_files = outcome + .families + .iter() + .flat_map(|group| group.family.members.iter()) + .map(|member| member.path.clone()) + .collect::>(); + touched_files.sort(); + touched_files.dedup(); + Ok(graph_tool_completion( + GraphToolResultV1::Redundancy(outcome), + touched_files, + )) +} + +fn similar_occurrence( + occurrence: &tracedecay_code_index::clones::CloneBodyOccurrenceV1, +) -> SimilarOccurrenceV1 { + SimilarOccurrenceV1 { + project_id: occurrence.project_id.clone(), + repository_id: occurrence.repository_id.clone(), + worktree_id: occurrence.worktree_id.clone(), + source_generation: occurrence.source_generation.clone(), + snapshot_digest: occurrence.snapshot_digest.clone(), + symbol_occurrence_id: occurrence.symbol_occurrence_id.clone(), + path: occurrence.path.clone(), + body_span: occurrence.body_span, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn clone_lanes_report_one_unavailable_wire_protocol() { + use tracedecay_query::code_search::CodeIndexSearchUnavailableReasonV1 as Reason; + + for (reason, code, retryable) in [ + ( + Reason::CapabilityUnavailable, + "code_index_unavailable", + false, + ), + (Reason::AuthorityUnavailable, "authority_unavailable", false), + ( + Reason::LinkedWorktreeDisabled, + "linked_worktree_disabled", + false, + ), + (Reason::Cancelled, "cancelled", true), + (Reason::TimedOut, "timed_out", true), + ( + Reason::CapacityUnavailable, + "search_capacity_unavailable", + true, + ), + ( + Reason::GenerationUnavailable, + "generation_unavailable", + true, + ), + (Reason::GenerationUnverified, "generation_unverified", true), + (Reason::InvalidRequest, "invalid_request", false), + ( + Reason::CorruptionResetRequired, + "index_corruption_reset_required", + false, + ), + (Reason::Internal, "search_failed", false), + ] { + for lane in ["similarity", "family"] { + let error = clone_lane_unavailable_error(lane, reason); + assert_eq!( + error + .project_route_context() + .expect("clone lane failures are typed project-route errors"), + ( + code, + retryable, + format!("the maintained clone {lane} lane is unavailable: {code}").as_str(), + ) + ); + } + } + } + + #[tokio::test] + async fn similar_unavailable_wire_preserves_reason_and_retryability() { + let temp = tempfile::tempdir().expect("temp root"); + let admitted = crate::tool_context::tests::scope("similar-unavailable"); + let project = crate::tool_context::tests::project_bundle(temp.path(), &admitted, None); + let authority = tracedecay_query::code_search::CodeIndexSearchAuthorityV1 { + principal: tracedecay_domain::PrincipalId::new("principal.similar-unavailable") + .expect("principal"), + authorization_revision: tracedecay_domain::AuthorizationRevision::new( + "revision.similar-unavailable", + ) + .expect("revision"), + }; + + for (reason, reason_code, retryable) in [ + ( + tracedecay_query::code_search::CodeIndexSearchUnavailableReasonV1::GenerationUnavailable, + "generation_unavailable", + true, + ), + ( + tracedecay_query::code_search::CodeIndexSearchUnavailableReasonV1::CorruptionResetRequired, + "index_corruption_reset_required", + false, + ), + ] { + let executor: tracedecay_query::code_search::CodeIndexSimilarExecutor = + std::sync::Arc::new(move |_| { + Box::pin(async move { + tracedecay_query::code_search::CodeIndexSimilarOutcomeV1::Unavailable(reason) + }) + }); + let code_index = + crate::AdmittedCodeIndex::new(&authority, None, Some(&executor), None, None) + .expect("similar executor admits"); + let ctx = crate::McpToolContext::bind(crate::McpToolBinding { + project: &project, + request: crate::McpRequestAuthoritiesV1 { + code_index: Some(code_index), + ..crate::McpRequestAuthoritiesV1::default() + }, + }) + .expect("admitted similar binding"); + let result = compute_similar( + &ctx, + json!({ + "project_id": admitted.project_id, + "repository_id": admitted.repository_id, + "target": { + "kind": "symbol_occurrence", + "symbol_occurrence_id": "symbol.similar-unavailable", + }, + "match_classes": ["conservative_exact"], + "result_limit": 10, + "work_limit": 20, + }), + ) + .await; + let Err(error) = result else { + panic!("unavailable similar executor must remain a transport failure"); + }; + let response = + crate::tool_error_response(json!(1), "tracedecay_similar", &error); + let wire: Value = serde_json::from_str(&crate::serialize_response_line(&response)) + .expect("JSON-RPC response"); + + assert_eq!(wire["error"]["data"]["reason_code"], reason_code); + assert_eq!(wire["error"]["data"]["retryable"], retryable); + } + } + + #[tokio::test] + async fn redundancy_unavailable_wire_preserves_reason_and_retryability() { + let temp = tempfile::tempdir().expect("temp root"); + let admitted = crate::tool_context::tests::scope("redundancy-unavailable"); + let project = crate::tool_context::tests::project_bundle(temp.path(), &admitted, None); + let authority = tracedecay_query::code_search::CodeIndexSearchAuthorityV1 { + principal: tracedecay_domain::PrincipalId::new("principal.redundancy-unavailable") + .expect("principal"), + authorization_revision: tracedecay_domain::AuthorizationRevision::new( + "revision.redundancy-unavailable", + ) + .expect("revision"), + }; + + for (reason, reason_code, retryable) in [ + ( + tracedecay_query::code_search::CodeIndexSearchUnavailableReasonV1::GenerationUnavailable, + "generation_unavailable", + true, + ), + ( + tracedecay_query::code_search::CodeIndexSearchUnavailableReasonV1::CorruptionResetRequired, + "index_corruption_reset_required", + false, + ), + ] { + let executor: tracedecay_query::code_search::CodeIndexRedundancyExecutor = + std::sync::Arc::new(move |_| { + Box::pin(async move { Err(reason) }) + }); + let code_index = + crate::AdmittedCodeIndex::new(&authority, None, None, Some(&executor), None) + .expect("redundancy executor admits"); + let ctx = crate::McpToolContext::bind(crate::McpToolBinding { + project: &project, + request: crate::McpRequestAuthoritiesV1 { + code_index: Some(code_index), + ..crate::McpRequestAuthoritiesV1::default() + }, + }) + .expect("admitted redundancy binding"); + let result = compute_redundancy( + &ctx, + json!({ + "project_id": admitted.project_id, + "repository_id": admitted.repository_id, + "match_classes": ["conservative_exact"], + "scope": {"kind": "repository"}, + "include_generated_paths": false, + "family_limit": 10, + "member_limit": 10, + "work_limit": 20, + }), + ) + .await; + let Err(error) = result else { + panic!("unavailable redundancy executor must remain a transport failure"); + }; + let response = + crate::tool_error_response(json!(1), "tracedecay_redundancy", &error); + let wire: Value = serde_json::from_str(&crate::serialize_response_line(&response)) + .expect("JSON-RPC response"); + + assert_eq!(wire["error"]["data"]["reason_code"], reason_code); + assert_eq!(wire["error"]["data"]["retryable"], retryable); + assert_ne!( + wire["error"]["data"]["reason_code"], + "verified-code-redundancy-unavailable" + ); + } + } + + #[tokio::test] + async fn missing_clone_lane_executors_emit_shared_capability_unavailable() { + let temp = tempfile::tempdir().expect("temp root"); + let admitted = crate::tool_context::tests::scope("clone-missing-executor"); + let project = crate::tool_context::tests::project_bundle(temp.path(), &admitted, None); + let authority = tracedecay_query::code_search::CodeIndexSearchAuthorityV1 { + principal: tracedecay_domain::PrincipalId::new("principal.clone-missing-executor") + .expect("principal"), + authorization_revision: tracedecay_domain::AuthorizationRevision::new( + "revision.clone-missing-executor", + ) + .expect("revision"), + }; + let shared_code = tracedecay_query::code_search::CodeIndexSearchUnavailableReasonV1::CapabilityUnavailable + .as_str(); + + // Admit the sibling lane so the request is authorized, then omit the + // lane under test, the Codex P2 gap (opaque missing-executor tokens). + let similar_stub: tracedecay_query::code_search::CodeIndexSimilarExecutor = + std::sync::Arc::new(|_| { + Box::pin(async { + tracedecay_query::code_search::CodeIndexSimilarOutcomeV1::NotFound + }) + }); + let redundancy_only = + crate::AdmittedCodeIndex::new(&authority, None, Some(&similar_stub), None, None) + .expect("similar executor admits without redundancy"); + let redundancy_ctx = crate::McpToolContext::bind(crate::McpToolBinding { + project: &project, + request: crate::McpRequestAuthoritiesV1 { + code_index: Some(redundancy_only), + ..crate::McpRequestAuthoritiesV1::default() + }, + }) + .expect("admitted similar-only binding"); + let redundancy_err = compute_redundancy( + &redundancy_ctx, + json!({ + "project_id": admitted.project_id, + "repository_id": admitted.repository_id, + "match_classes": ["conservative_exact"], + "scope": {"kind": "repository"}, + "include_generated_paths": false, + "family_limit": 10, + "member_limit": 10, + "work_limit": 20, + }), + ) + .await + .expect_err("missing redundancy executor must be typed unavailable"); + let redundancy_wire: Value = serde_json::from_str(&crate::serialize_response_line( + &crate::tool_error_response(json!(1), "tracedecay_redundancy", &redundancy_err), + )) + .expect("JSON-RPC response"); + assert_eq!(redundancy_wire["error"]["data"]["reason_code"], shared_code); + assert_eq!(redundancy_wire["error"]["data"]["retryable"], false); + assert_ne!( + redundancy_wire["error"]["data"]["reason_code"], + "verified-code-redundancy-unavailable" + ); + + let redundancy_stub: tracedecay_query::code_search::CodeIndexRedundancyExecutor = + std::sync::Arc::new(|_| { + Box::pin(async { + Err(tracedecay_query::code_search::CodeIndexSearchUnavailableReasonV1::Internal) + }) + }); + let similar_only = + crate::AdmittedCodeIndex::new(&authority, None, None, Some(&redundancy_stub), None) + .expect("redundancy executor admits without similar"); + let similar_ctx = crate::McpToolContext::bind(crate::McpToolBinding { + project: &project, + request: crate::McpRequestAuthoritiesV1 { + code_index: Some(similar_only), + ..crate::McpRequestAuthoritiesV1::default() + }, + }) + .expect("admitted redundancy-only binding"); + let similar_err = compute_similar( + &similar_ctx, + json!({ + "project_id": admitted.project_id, + "repository_id": admitted.repository_id, + "target": { + "kind": "symbol_occurrence", + "symbol_occurrence_id": "symbol.clone-missing-executor", + }, + "match_classes": ["conservative_exact"], + "result_limit": 10, + "work_limit": 20, + }), + ) + .await + .expect_err("missing similar executor must be typed unavailable"); + let similar_wire: Value = serde_json::from_str(&crate::serialize_response_line( + &crate::tool_error_response(json!(1), "tracedecay_similar", &similar_err), + )) + .expect("JSON-RPC response"); + assert_eq!(similar_wire["error"]["data"]["reason_code"], shared_code); + assert_eq!(similar_wire["error"]["data"]["retryable"], false); + assert_ne!( + similar_wire["error"]["data"]["reason_code"], + "verified-code-similarity-unavailable" + ); + } +} diff --git a/crates/tracedecay-mcp/src/handlers/graph/context.rs b/crates/tracedecay-mcp/src/handlers/graph/context.rs new file mode 100644 index 0000000000..820b4838be --- /dev/null +++ b/crates/tracedecay-mcp/src/handlers/graph/context.rs @@ -0,0 +1,644 @@ +//! `tracedecay_context`: ranked search matches, their graph neighborhood, +//! code blocks, and scoped memory, assembled for one task. + +use std::collections::HashMap; +use std::future::Future; + +use serde_json::Value; +use tracedecay_code_index::graph_projection::CodeGraphSymbolSummaryV1; +use tracedecay_contracts::InvocationAnalyticsV1; +use tracedecay_contracts::graph_tool::{GraphToolCompletionV1, GraphToolResultV1}; +use tracedecay_contracts::retrieval::{ + ContextCodeBlockV1, ContextLexicalAnchorV1, ContextModeV1, ContextRelatedOmissionV1, + ContextResultV1, ContextRetrievalPlanV1, ContextSearchMatchV1, ContextStageV1, + ContextSurfaceRequestV1, LexicalAnchorDropReasonV1, +}; +use tracedecay_domain::errors::{Result, TraceDecayError}; +use tracedecay_domain::{ExactClass, RelationEdgeKindV1}; + +use crate::McpToolContext; +#[cfg(test)] +use crate::context_headings::CONTEXT_SEEN_NODE_IDS_LABEL; +use crate::handlers::dependency_hints; +use crate::handlers::support::{decode_primitive_request, unique_file_paths}; + +use super::context_markdown::verified_plan_context; +use super::context_support::{ + ContextMemoryOutcome, context_memory_analytics, context_memory_options, context_memory_outcome, + context_memory_read_control, context_memory_stage, +}; +use super::lexical_routing; +use super::primitive_surface::{ + search_coverage as primitive_search_coverage, symbol_location as primitive_symbol_location, +}; +use super::search::execute_code_index_search; +use super::search_evidence::{bind_verified_graph_to_search, race_primary_search_with_graph}; +use super::search_freshness::{ + ServedGenerationV1, search_freshness, worktree_freshness_from_payload, +}; +use super::{ + graph_symbol_end_line, graph_symbol_paths, required_graph_file_path, required_graph_metadata, + user_line, +}; + +#[cfg(test)] +use super::context_support::{context_markdown_lane_preview, context_memory_section}; + +/// Edge rows each direction of the related-symbol walk reads across all +/// selected symbols. The walk takes each neighbor from its edge row and +/// ranks it from the catalog without reading a symbol entity, so it covers +/// a hub's neighborhood before the `max_nodes` cut; past this many rows the +/// omission reports its total as a lower bound. +const CONTEXT_RELATED_WALK_ROWS: usize = 4_096; + +/// Related symbols rank by the best edge kind joining them to a selected +/// symbol, lower first: behavior (calls), then the type hierarchy +/// (implements, extends), then signatures (`type_of`, returns, receives), +/// then looser references (uses, annotates), then containment. +fn context_related_kind_rank(kind: RelationEdgeKindV1) -> u8 { + match kind { + RelationEdgeKindV1::Calls => 0, + RelationEdgeKindV1::Implements => 1, + RelationEdgeKindV1::Extends => 2, + RelationEdgeKindV1::TypeOf => 3, + RelationEdgeKindV1::Returns => 4, + RelationEdgeKindV1::Receives => 5, + RelationEdgeKindV1::Uses => 6, + RelationEdgeKindV1::Annotates => 7, + RelationEdgeKindV1::Contains => 8, + } +} + +#[derive(Default)] +struct ContextGraphProjection { + selected: Vec, + related: Vec, + related_omission: Option, + code_blocks: Vec, + touched_files: Vec, +} + +fn context_search_matches( + complete: &tracedecay_query::code_search::CodeIndexSearchCompletedV1, + scope_prefix: Option<&str>, +) -> Vec { + complete + .ordered_candidates + .iter() + .filter_map(|ranked| { + let display = complete + .display_by_anchor + .get(&ranked.candidate.anchor_id)?; + if scope_prefix.is_some_and(|prefix| !display.path.starts_with(prefix)) { + return None; + } + let exact_class = match ranked.candidate.exact_class { + ExactClass::ExactMessage => "exact_message", + ExactClass::ExactLiteralPhrase => "exact_literal_phrase", + ExactClass::Approximate => "approximate", + }; + Some(ContextSearchMatchV1 { + anchor_id: ranked.candidate.anchor_id.as_str().to_owned(), + name: display.name.clone(), + qualified_name: display.qualified_name.clone(), + kind: display.kind.clone(), + file: display.path.clone(), + exact_class: exact_class.to_owned(), + rank: ranked.final_ordinal.saturating_add(1), + utility_micros: ranked.candidate.utility_micros, + }) + }) + .collect() +} + +/// The kernel's per-anchor receipts in the context wire shape, counted +/// against the search matches this context carries: a served site outside +/// `scope_prefix` is dropped as out of scope. +fn context_lexical_anchors( + complete: &tracedecay_query::code_search::CodeIndexSearchCompletedV1, + scope_prefix: Option<&str>, +) -> Vec { + let mut receipt = complete.lexical_routes.clone(); + if let Some(prefix) = scope_prefix { + receipt.reconcile_served(|site| { + complete + .display_by_anchor + .get(site) + .filter(|display| !display.path.starts_with(prefix)) + .map(|_| LexicalAnchorDropReasonV1::OutOfScope) + }); + } + receipt + .anchors + .iter() + .map(lexical_routing::anchor_outcome) + .collect() +} + +fn context_graph_projection( + ctx: &McpToolContext<'_>, + graph: &tracedecay_graph_query::VerifiedGraphQuery, + complete: &tracedecay_query::code_search::CodeIndexSearchCompletedV1, + scope_prefix: Option<&str>, + max_nodes: usize, + include_code: bool, + max_code_blocks: usize, +) -> Result { + let mut selected = Vec::new(); + for ranked in &complete.ordered_candidates { + let Some(display) = complete.display_by_anchor.get(&ranked.candidate.anchor_id) else { + continue; + }; + if scope_prefix.is_some_and(|prefix| !display.path.starts_with(prefix)) { + continue; + } + let candidates = + graph.resolve_qualified_name(&display.qualified_name, Some(&display.kind), 16)?; + for candidate in candidates { + if required_graph_file_path(&candidate)? == display.path.as_str() + && !selected.iter().any(|existing: &CodeGraphSymbolSummaryV1| { + existing.occurrence == candidate.occurrence + }) + { + selected.push(candidate); + break; + } + } + } + let seeds = selected + .iter() + .map(|symbol| symbol.occurrence.clone()) + .collect::>(); + let (related, related_omission) = if seeds.is_empty() { + (Vec::new(), None) + } else { + let ranked = graph.ranked_neighbors( + &seeds, + context_related_kind_rank, + CONTEXT_RELATED_WALK_ROWS, + max_nodes, + )?; + let omitted = ranked.total.saturating_sub(ranked.neighbors.len()); + let omission = (omitted > 0 || ranked.walk_truncated).then_some(ContextRelatedOmissionV1 { + total: ranked.total, + omitted, + total_is_lower_bound: ranked.walk_truncated, + }); + (ranked.neighbors, omission) + }; + + let mut all_symbols = selected.clone(); + all_symbols.extend(related.iter().cloned()); + let touched_files = graph_symbol_paths(&all_symbols)?; + let mut code_blocks = Vec::new(); + if include_code { + // Context snippets are filesystem windows, not the mmap'd sealed + // lexical artifact (that path serves n-gram search). Cache by path + // so five symbols in one file do not re-read the whole source. + let mut source_by_path = HashMap::::new(); + for symbol in selected.iter().take(max_code_blocks) { + let metadata = required_graph_metadata(symbol)?; + let file_path = required_graph_file_path(symbol)?; + if !source_by_path.contains_key(file_path) { + source_by_path.insert( + file_path.to_owned(), + tracedecay_runtime_core::sync::read_source_file( + &ctx.project_root().join(file_path), + )?, + ); + } + let Some(source) = source_by_path.get(file_path) else { + return Err(TraceDecayError::Config { + message: format!("context source window missing for '{file_path}'"), + }); + }; + code_blocks.push(ContextCodeBlockV1 { + node_id: symbol.occurrence.as_str().to_owned(), + file: file_path.to_owned(), + start_line: user_line(metadata.start_line), + end_line: user_line(graph_symbol_end_line(metadata)?), + code: extract_lines( + source, + metadata.start_line, + graph_symbol_end_line(metadata)?, + ), + }); + } + } + Ok(ContextGraphProjection { + selected, + related, + related_omission, + code_blocks, + touched_files, + }) +} + +/// Extract the source spanning tree-sitter rows `start_line..=end_line` +/// (0-based, inclusive) from `source`. Node line fields are stored as the +/// raw tree-sitter row index, so the caller passes them through unchanged. +/// Returns the empty string if the range is out of bounds. +fn extract_lines(source: &str, start_line: u32, end_line: u32) -> String { + let start = start_line as usize; + let end_exclusive = (end_line as usize).saturating_add(1); + if start >= end_exclusive { + return String::new(); + } + let mut selected = source.lines().skip(start).take(end_exclusive - start); + let Some(first) = selected.next() else { + return String::new(); + }; + let mut body = String::with_capacity(first.len()); + body.push_str(first); + for line in selected { + body.push('\n'); + body.push_str(line); + } + body +} + +#[hotpath::measure(label = "mcp.graph.context.total")] +pub async fn compute_context( + ctx: &McpToolContext<'_>, + graph: F, + args: Value, + scope_prefix: Option<&str>, +) -> Result +where + F: Future>, +{ + let search_executor = ctx.code_index_search_executor(); + let search_authority = ctx.code_index_search_authority(); + let deadline = ctx.deadline().cloned(); + let cancellation = ctx.cancellation().cloned(); + let request: ContextSurfaceRequestV1 = decode_primitive_request(&args, "tracedecay_context")?; + let task = request.task.as_str(); + let mode = request.mode.unwrap_or(ContextModeV1::Explore); + let max_nodes = request + .max_nodes + .map_or(20, |value| value.clamp(1, 200) as usize); + let include_code = request.include_code.unwrap_or(false); + let max_code_blocks = request + .max_code_blocks + .map_or(5, |value| value.clamp(1, 20) as usize); + let requested_anchors = request.lexical_anchors.clone().unwrap_or_default(); + let lexical_routing = lexical_routing::routing_from_parts( + requested_anchors.clone(), + request.prefer_symbol.unwrap_or(false), + )?; + let memory_options = context_memory_options(&args); + let memory_read_control = + context_memory_read_control(&memory_options, deadline.as_ref(), cancellation.as_ref())?; + // Graph enrichment is optional unless the caller asks for source bodies. + // That request waits for graph admission under the same deadline and + // cancellation as search; ordinary lexical/exact retrieval stays independent. + let search = execute_code_index_search( + search_executor, + tracedecay_query::code_search::CodeIndexSearchRequestV1 { + project_root: ctx.project_root().to_path_buf(), + query: task.to_owned(), + source_revision: None, + source_tree: None, + source_reference: None, + limit: max_nodes, + cursor: None, + lexical_routing, + authority: search_authority.cloned(), + deadline, + cancellation, + }, + ); + let memory = context_memory_outcome(ctx, task, &memory_options, memory_read_control.as_ref()); + let search_and_graph = race_primary_search_with_graph(search, graph, false, None, include_code); + let ((outcome, graph), memory_outcome) = tokio::join!(search_and_graph, memory); + // Read after the search settles: the verdict must describe the scheduler + // state at serve time, not a snapshot taken before the lanes ran. + let freshness_payload = ctx.freshness().await; + let worktree_freshness = worktree_freshness_from_payload(freshness_payload.as_ref()); + let (complete, code_generation, coverage, freshness, search_matches, lexical_anchors) = + match outcome { + tracedecay_query::code_search::CodeIndexSearchOutcomeV1::Complete(complete) => { + let search_matches = context_search_matches(&complete, scope_prefix); + let lexical_anchors = context_lexical_anchors(&complete, scope_prefix); + let code_generation = Some(complete.code_generation.clone()); + let coverage = primitive_search_coverage(&complete.coverage); + let freshness = search_freshness( + ServedGenerationV1::Served(&complete.code_generation), + &complete.coverage, + &worktree_freshness, + ); + ( + Some(complete), + code_generation, + coverage, + freshness, + search_matches, + lexical_anchors, + ) + } + tracedecay_query::code_search::CodeIndexSearchOutcomeV1::Unavailable(unavailable) => ( + None, + unavailable.code_generation, + primitive_search_coverage(&unavailable.coverage), + search_freshness( + ServedGenerationV1::Unavailable { + reason: unavailable.reason.as_str(), + }, + &unavailable.coverage, + &worktree_freshness, + ), + Vec::new(), + requested_anchors + .iter() + .map(|anchor| ContextLexicalAnchorV1::NotServed { + anchor: anchor.clone(), + }) + .collect(), + ), + }; + let graph = match complete.as_ref() { + Some(complete) => bind_verified_graph_to_search(graph, &complete.code_generation), + None => graph, + }; + let (graph, projection, verified_graph_evidence, graph_stage) = match (graph, complete.as_ref()) + { + (Ok(graph), Some(complete)) => match hotpath::measure_block!( + "mcp.graph.context.graph", + context_graph_projection( + ctx, + &graph, + complete, + scope_prefix, + max_nodes, + include_code, + max_code_blocks, + ) + ) { + Ok(projection) => { + let stage = ContextStageV1::ran(max_nodes, projection.selected.len(), false); + (Some(graph), projection, None, stage) + } + Err(error) => ( + None, + ContextGraphProjection::default(), + Some(dependency_hints::unavailable_evidence(&error)), + ContextStageV1::Unavailable, + ), + }, + (Ok(graph), None) => ( + Some(graph), + ContextGraphProjection::default(), + None, + ContextStageV1::Skipped, + ), + (Err(error), _) => ( + None, + ContextGraphProjection::default(), + Some(dependency_hints::unavailable_evidence(&error)), + ContextStageV1::Unavailable, + ), + }; + let retrieval = ContextRetrievalPlanV1 { + search: match complete.as_ref() { + Some(complete) => ContextStageV1::ran( + max_nodes, + search_matches.len(), + complete.next_cursor.is_some(), + ), + None => ContextStageV1::Unavailable, + }, + graph: graph_stage, + related: if projection.selected.is_empty() { + ContextStageV1::Skipped + } else { + ContextStageV1::ran( + max_nodes, + projection.related.len(), + projection.related_omission.is_some(), + ) + }, + code: if !include_code { + ContextStageV1::NotRequested + } else if projection.selected.is_empty() { + ContextStageV1::Skipped + } else { + ContextStageV1::ran( + max_code_blocks, + projection.code_blocks.len(), + projection.selected.len() > max_code_blocks, + ) + }, + memory: ContextStageV1::NotRequested, + }; + let ContextMemoryOutcome { + hits: memory_matches, + graph_coverage: memory_graph_coverage, + error: memory_matches_error, + } = memory_outcome; + let symbols = projection + .selected + .iter() + .map(primitive_symbol_location) + .collect::>>()?; + let related_symbols = projection + .related + .iter() + .map(primitive_symbol_location) + .collect::>>()?; + let plan = match (mode, graph.as_ref()) { + (ContextModeV1::Plan, Some(graph)) => { + Some(verified_plan_context(graph, &projection.selected)?) + } + _ => None, + }; + let touched_files = unique_file_paths( + projection.touched_files.iter().map(String::as_str).chain( + search_matches + .iter() + .map(|search_match| search_match.file.as_str()), + ), + ); + let analytics = InvocationAnalyticsV1 { + context_memory: Some(context_memory_analytics( + &memory_options, + &memory_matches, + memory_matches_error.as_deref(), + )), + pr_context: None, + }; + let retrieval = ContextRetrievalPlanV1 { + memory: context_memory_stage( + &memory_options, + &memory_matches, + memory_matches_error.as_deref(), + ), + ..retrieval + }; + let result = ContextResultV1 { + task: request.task, + mode, + freshness, + code_generation, + search_matches, + lexical_anchors, + symbols, + related_symbols, + related_omission: projection.related_omission, + code: projection.code_blocks, + coverage, + memory_matches, + memory_graph_coverage, + memory_matches_error, + verified_graph_evidence, + plan, + retrieval, + }; + Ok(GraphToolCompletionV1 { + result: GraphToolResultV1::Context(Box::new(result)), + touched_files, + code_graph: None, + analytics: Some(analytics), + cost: None, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + use tracedecay_contracts::memory::FactSearchHitV1; + + fn context_memory_hit(content: &str) -> FactSearchHitV1 { + serde_json::from_value(json!({ + "fact": { + "owner": {"kind": "profile"}, + "fact_id": "fact.0000000000000000000000000000000000000000000000000000000000000000.1111111111111111111111111111111111111111111111111111111111111111", + "content": content, + "category": "project", + "tags": [], + "entities": [], + "trust_score_millionths": 900_000, + "source": {"kind": "application", "operation_id": "operation.context-memory"}, + "source_label": "context-test", + "active_assertion_id": "assertion.context-memory", + "last_event_id": "event.context-memory", + "projected_as_of": 1, + "telemetry": { + "retrieval_count": 0, + "access_count": 0, + "helpful_count": 0, + "unhelpful_count": 0, + "created_at": 1, + "updated_at": 1, + "last_retrieved_at": null, + "last_recalled_at": null, + "last_feedback_at": null + }, + "metadata": {} + }, + "scores": { + "score_millionths": 500_000, + "fts_score_millionths": 250_000, + "jaccard_score_millionths": 250_000, + "holographic_score_millionths": 0, + "trust_score_millionths": 900_000 + }, + "why": null + })) + .expect("canonical context memory hit") + } + + #[test] + fn context_markdown_lane_preview_keeps_all_lanes_visible() { + let full = format!( + "## Code Context\n**Query:** q\n\n### Memory Matches\n{}\n### Entry Points\n{}\n### Related Symbols\n{}\n### Code\n{}\n### Index Coverage Hint\n{}\n### Extension Points\n{}\n### Test Coverage\n{}\nseen_node_ids: [{}]\n", + "memory fact with unicode caf\u{e9}\n".repeat(300), + "- **entry** src/lib.rs:1\n".repeat(300), + "- related\n".repeat(500), + "```rust\nfn demo() {}\n```\n".repeat(500), + "hint\n".repeat(500), + "- trait\n".repeat(400), + "- tests/context_test.rs\n".repeat(400), + "\"node-id\",".repeat(400) + ); + + let preview = context_markdown_lane_preview(&full); + + for heading in [ + "## Code Context", + "### Memory Matches", + "### Entry Points", + "### Related Symbols", + "### Code", + "### Index Coverage Hint", + "### Extension Points", + "### Test Coverage", + "seen_node_ids:", + ] { + assert!(preview.contains(heading), "missing {heading}: {preview}"); + } + assert!(preview.len() < full.len()); + assert!(preview.contains("lane truncated")); + assert!(preview.is_char_boundary(preview.len())); + } + + #[test] + fn context_lane_preview_keeps_seen_node_ids_parseable() { + let ids: Vec = (0..100).map(|i| format!("function:{i:032x}")).collect(); + let markdown = format!( + "{} {}\n", + CONTEXT_SEEN_NODE_IDS_LABEL, + serde_json::to_string(&ids) + .unwrap_or_else(|err| panic!("failed to serialize seen node ids: {err}")) + ); + + let preview = context_markdown_lane_preview(&markdown); + let json = match preview.strip_prefix(CONTEXT_SEEN_NODE_IDS_LABEL) { + Some(json) => json.trim(), + None => panic!("preview should keep seen_node_ids label: {preview}"), + }; + let parsed: Vec = serde_json::from_str(json) + .unwrap_or_else(|err| panic!("failed to parse seen node ids: {err}: {json}")); + + assert_eq!(parsed, ids); + assert!(!preview.contains("lane truncated")); + } + + #[test] + fn context_memory_section_keeps_full_content_for_retrieval_handle() { + let content = format!("{}tail-marker", "long memory body ".repeat(100)); + let hit = context_memory_hit(&content); + + let Some(section) = context_memory_section(&[hit], None) else { + panic!("memory hit should render"); + }; + + assert!(section.contains(&content)); + assert!(section.contains("tail-marker")); + assert!(!section.contains("...")); + assert!(section.contains("tracedecay_fact_feedback")); + } + + #[test] + fn context_lane_preview_closes_open_code_fence_before_truncation_note() { + let markdown = format!("### Code\n```rust\n{}\n", "fn demo() {}\n".repeat(1_000)); + + let preview = context_markdown_lane_preview(&markdown); + + assert!(preview.contains("```\n\n... lane truncated")); + } + + #[test] + fn context_lane_preview_ignores_heading_markers_inside_code_fences() { + let markdown = format!( + "### Code\n```markdown\n{}\n```\n### Test Coverage\n- real lane\n", + "### not a lane\n".repeat(1_000) + ); + + let preview = context_markdown_lane_preview(&markdown); + + assert!(preview.contains("### Code")); + assert!(preview.contains("### Test Coverage")); + assert_eq!(preview.matches("lane truncated").count(), 1); + } +} diff --git a/crates/tracedecay-mcp/src/handlers/graph/dispatch.rs b/crates/tracedecay-mcp/src/handlers/graph/dispatch.rs deleted file mode 100644 index 7c3bb89f34..0000000000 --- a/crates/tracedecay-mcp/src/handlers/graph/dispatch.rs +++ /dev/null @@ -1,37 +0,0 @@ -//! Name table for the code-graph navigation and lookup family. - -use serde_json::Value; -use tracedecay_application::code_index::CodeIndexIgnoredDependencyAdmissionPortV1; -use tracedecay_domain::errors::Result; - -use super::handle_search; -use crate::ToolResult; -use crate::handlers::support::unknown_tool_error; -use crate::handlers::verified_read::{VerifiedGraphOpen, verified_read_operation as read}; -use crate::tool_context::McpToolContext; - -/// Dispatches one graph-family tool (`tracedecay_search`) onto its handler, -/// opening the verified graph through `open` under the operation the catalog -/// registers for it. -pub async fn dispatch_tool( - ctx: &McpToolContext<'_>, - open: &VerifiedGraphOpen<'_>, - tool_name: &str, - args: Value, - scope_prefix: Option<&str>, - ignored_dependency_admission: Option<&dyn CodeIndexIgnoredDependencyAdmissionPortV1>, -) -> Result { - match tool_name { - "tracedecay_search" => { - handle_search( - ctx, - open(read("code_symbol_search")?), - args, - scope_prefix, - ignored_dependency_admission, - ) - .await - } - _ => Err(unknown_tool_error(tool_name)), - } -} diff --git a/crates/tracedecay-mcp/src/handlers/graph/exact_symbol.rs b/crates/tracedecay-mcp/src/handlers/graph/exact_symbol.rs new file mode 100644 index 0000000000..a35f1a6467 --- /dev/null +++ b/crates/tracedecay-mcp/src/handlers/graph/exact_symbol.rs @@ -0,0 +1,85 @@ +//! `tracedecay_find_exact_symbol`: bare-name lookup against the served graph. + +use serde_json::Value; +use tracedecay_contracts::graph_tool::{GraphToolCompletionV1, GraphToolResultV1}; +use tracedecay_contracts::retrieval::{ + FindExactSymbolMatchV1, FindExactSymbolResultV1, FindExactSymbolSurfaceRequestV1, +}; +use tracedecay_domain::errors::Result; + +use crate::McpToolContext; +use crate::handlers::dependency_hints; +use crate::handlers::support::decode_primitive_request; + +use super::{ + graph_symbol_paths, graph_symbols_in_scope, graph_tool_completion, required_graph_file_path, + required_graph_metadata, user_line, +}; + +/// Bare-name lookup against `idx_nodes_name`, no BM25 scoring, no fuzzy +/// match, no qualified-name suffix walk. Returns every node whose `name` +/// column equals the query exactly. Useful when you already know the symbol +/// and want the apples-to-apples cost of an index hit instead of +/// `tracedecay_search`'s ranked query. +#[hotpath::measure(label = "mcp.graph.find_exact_symbol.total")] +pub async fn compute_find_exact_symbol( + ctx: &McpToolContext<'_>, + graph: &tracedecay_graph_query::VerifiedGraphQuery, + args: Value, + scope_prefix: Option<&str>, + ignored_dependency_admission: Option< + &dyn tracedecay_application::code_index::CodeIndexIgnoredDependencyAdmissionPortV1, + >, +) -> Result { + let request: FindExactSymbolSurfaceRequestV1 = + decode_primitive_request(&args, "tracedecay_find_exact_symbol")?; + let name = request.name.as_str(); + let limit = request.limit.map_or(20, |v| v.min(200) as usize); + + let mut nodes = hotpath::measure_block!("mcp.graph.find_exact_symbol.graph", { + let nodes = graph.resolve_simple_name(name, None, limit.saturating_mul(4))?; + graph_symbols_in_scope(nodes, scope_prefix)? + }); + if nodes.is_empty() && request.lazy_index_ignored_dependencies.unwrap_or(false) { + hotpath::future!( + dependency_hints::admit_verified_ignored_dependency( + ctx, + ignored_dependency_admission, + graph, + name, + scope_prefix, + ), + label = "mcp.graph.find_exact_symbol.admit" + ) + .await?; + } + if nodes.len() > limit { + nodes.truncate(limit); + } + + let touched_files = graph_symbol_paths(&nodes)?; + let matches = nodes + .iter() + .map(|node| { + let metadata = required_graph_metadata(node)?; + let file_path = required_graph_file_path(node)?; + Ok(FindExactSymbolMatchV1 { + id: node.occurrence.as_str().to_owned(), + name: metadata.simple_name.clone(), + qualified_name: metadata.qualified_name.clone(), + kind: metadata.kind.clone(), + file: file_path.to_owned(), + line: user_line(metadata.start_line), + signature: metadata.signature.clone(), + }) + }) + .collect::>>()?; + Ok(graph_tool_completion( + GraphToolResultV1::FindExactSymbol(FindExactSymbolResultV1 { + name: request.name.clone(), + count: matches.len() as u64, + matches, + }), + touched_files, + )) +} diff --git a/crates/tracedecay-mcp/src/handlers/graph/lexical_routing.rs b/crates/tracedecay-mcp/src/handlers/graph/lexical_routing.rs index 604e86479f..2ab2e3b57d 100644 --- a/crates/tracedecay-mcp/src/handlers/graph/lexical_routing.rs +++ b/crates/tracedecay-mcp/src/handlers/graph/lexical_routing.rs @@ -1,72 +1,71 @@ -use crate::tools::render::Md; -use serde::de::DeserializeOwned; -use serde_json::{Value, json}; -use tracedecay_contracts::retrieval::{LexicalAnchorDropReasonV1, LexicalAnchorDropV1}; +use serde_json::Value; +use tracedecay_contracts::retrieval::{ + ContextLexicalAnchorV1, LexicalAnchorDropReasonV1, LexicalAnchorDropV1, + SearchLexicalAlternativeReasonV1, SearchLexicalFieldV1, SearchLexicalRouteV1, + SearchResultRowV1, SearchRouteMatchV1, SearchSpellingVariantV1, SearchSurfaceRequestV1, +}; use tracedecay_domain::errors::{Result, TraceDecayError}; use tracedecay_query::retrieval::lexical::{ - LexicalAliasV1, LexicalAnchorOutcomeV1, LexicalAnchorReceiptV1, LexicalFieldFilterV1, - LexicalProximityV1, LexicalRouteKindV1, LexicalRouteReceiptV1, LexicalRoutingV1, + LexicalAliasV1, LexicalAlternativeReasonV1, LexicalAnchorOutcomeV1, LexicalAnchorReceiptV1, + LexicalFieldFilterV1, LexicalFieldV1, LexicalProximityV1, LexicalRouteKindV1, + LexicalRouteReceiptV1, LexicalRoutingV1, }; -pub(super) fn routing_from_args(args: &Value) -> Result { - let anchors = match args.get("lexical_anchors") { - None | Some(Value::Null) => Vec::new(), - Some(Value::Array(items)) => items - .iter() - .enumerate() - .map(|(index, item)| { - item.as_str() - .map(str::to_owned) - .ok_or_else(|| TraceDecayError::Config { - message: format!("lexical_anchors[{index}] must be a string"), - }) - }) - .collect::>>()?, - Some(_) => { - return Err(TraceDecayError::Config { - message: "lexical_anchors must be an array of strings".to_owned(), - }); - } - }; - let prefer_symbol = match args.get("prefer_symbol") { - None | Some(Value::Null) => false, - Some(Value::Bool(flag)) => *flag, - Some(_) => { - return Err(TraceDecayError::Config { - message: "prefer_symbol must be a boolean".to_owned(), - }); - } - }; - let aliases: Vec = decode_array(args, "lexical_aliases")?; - let phrases: Vec = decode_array(args, "lexical_phrases")?; - let proximities: Vec = decode_array(args, "lexical_proximities")?; - let field_filters: Vec = decode_array(args, "lexical_field_filters")?; - let mut routing = routing_from_parts(anchors, prefer_symbol)? - .with_aliases(aliases) - .map_err(|error| TraceDecayError::Config { - message: error.to_string(), - })?; - routing.phrases = phrases; - routing.proximities = proximities; - routing.field_filters = field_filters; +use crate::tools::render::Md; + +/// The kernel routing for one typed search request. The kernel bounds and +/// validates the anchors and aliases; a violation is a typed request error. +pub(super) fn routing_from_request(request: &SearchSurfaceRequestV1) -> Result { + let aliases = request + .lexical_aliases + .iter() + .flatten() + .map(|alias| LexicalAliasV1 { + strict_query: alias.strict_query.clone(), + alternative: alias.alternative.clone(), + }) + .collect(); + let mut routing = routing_from_parts( + request.lexical_anchors.clone().unwrap_or_default(), + request.prefer_symbol.unwrap_or(false), + )? + .with_aliases(aliases) + .map_err(|error| TraceDecayError::Config { + message: error.to_string(), + })?; + routing.phrases = request.lexical_phrases.clone().unwrap_or_default(); + routing.proximities = request + .lexical_proximities + .iter() + .flatten() + .map(|proximity| LexicalProximityV1 { + terms: proximity.terms.clone(), + maximum_gap: proximity.maximum_gap, + }) + .collect(); + routing.field_filters = request + .lexical_field_filters + .iter() + .flatten() + .map(|filter| LexicalFieldFilterV1 { + field: lexical_field(filter.field), + include: filter.include, + }) + .collect(); Ok(routing) } -fn decode_array(args: &Value, field: &str) -> Result> { - match args.get(field) { - None | Some(Value::Null) => Ok(Vec::new()), - Some(Value::Array(values)) => values - .iter() - .cloned() - .map(|value| { - serde_json::from_value(value).map_err(|error| TraceDecayError::Config { - message: format!("{field} is invalid: {error}"), - }) - }) - .collect(), - Some(_) => Err(TraceDecayError::Config { - message: format!("{field} must be an array"), - }), +fn lexical_field(field: SearchLexicalFieldV1) -> LexicalFieldV1 { + match field { + SearchLexicalFieldV1::SymbolName => LexicalFieldV1::SymbolName, + SearchLexicalFieldV1::QualifiedName => LexicalFieldV1::QualifiedName, + SearchLexicalFieldV1::Path => LexicalFieldV1::Path, + SearchLexicalFieldV1::Signature => LexicalFieldV1::Signature, + SearchLexicalFieldV1::Documentation => LexicalFieldV1::Documentation, + SearchLexicalFieldV1::BodyText => LexicalFieldV1::BodyText, + SearchLexicalFieldV1::PreambleText => LexicalFieldV1::PreambleText, + SearchLexicalFieldV1::ExactTerm => LexicalFieldV1::ExactTerm, + SearchLexicalFieldV1::Subtoken => LexicalFieldV1::Subtoken, } } @@ -93,58 +92,98 @@ pub(super) fn route_label(route: &LexicalRouteKindV1) -> String { } } -pub(super) fn attach_route_evidence( - output: &mut Value, - results: &mut [Value], +/// The page-level route and anchor evidence, attached only when a route +/// beyond the strict query ran or ranked a result, and each ranked row's +/// matching routes. +pub(super) fn route_evidence( + results: &mut [SearchResultRowV1], receipt: &LexicalRouteReceiptV1, -) -> Result<()> { +) -> ( + Option>, + Option>, +) { if !receipt.has_disclosure() { - return Ok(()); - } - let mut routes = Vec::with_capacity(receipt.routes.len()); - for route in &receipt.routes { - let mut value = serde_json::to_value(route)?; - value["label"] = json!(route_label(route)); - routes.push(value); - } - output["lexical_routes"] = Value::Array(routes); - if !receipt.anchors.is_empty() { - output["lexical_anchors"] = serde_json::to_value(&receipt.anchors)?; + return (None, None); } - for result in results.iter_mut() { - let Some(anchor) = result - .get("candidate") - .and_then(|candidate| candidate.get("anchor_id")) - .and_then(Value::as_str) - else { + for row in results.iter_mut() { + let Some(matches) = receipt.matches_by_anchor.get(&row.candidate.anchor_id) else { continue; }; - let Some(matches) = receipt - .matches_by_anchor - .iter() - .find(|(candidate_anchor, _)| candidate_anchor.as_str() == anchor) - .map(|(_, matches)| matches) - else { - continue; - }; - result["lexical_routes"] = json!( + row.lexical_routes = Some( matches .iter() - .map(|route_match| { - let mut value = json!({ - "route": route_label(&route_match.route), - "score_micros": route_match.score_micros, - "matched_terms": route_match.matched_terms, - }); - if !route_match.spelling_variants.is_empty() { - value["spelling_variants"] = json!(route_match.spelling_variants); - } - value + .map(|route_match| SearchRouteMatchV1 { + route: route_label(&route_match.route), + score_micros: route_match.score_micros, + matched_terms: route_match.matched_terms.clone(), + spelling_variants: route_match + .spelling_variants + .iter() + .map(|variant| SearchSpellingVariantV1 { + query: variant.query.clone(), + alternative: variant.alternative.clone(), + }) + .collect(), }) - .collect::>() + .collect(), ); } - Ok(()) + let routes = receipt.routes.iter().map(search_route).collect(); + let anchors = + (!receipt.anchors.is_empty()).then(|| receipt.anchors.iter().map(anchor_outcome).collect()); + (Some(routes), anchors) +} + +fn search_route(route: &LexicalRouteKindV1) -> SearchLexicalRouteV1 { + let label = route_label(route); + match route { + LexicalRouteKindV1::Query => SearchLexicalRouteV1::Query { label }, + LexicalRouteKindV1::Anchor { anchor } => SearchLexicalRouteV1::Anchor { + anchor: anchor.as_str().to_owned(), + label, + }, + LexicalRouteKindV1::PreferredSymbol { tokens } => SearchLexicalRouteV1::PreferredSymbol { + tokens: tokens.clone(), + label, + }, + LexicalRouteKindV1::IdentifierSplit { + strict_query, + terms, + } => SearchLexicalRouteV1::IdentifierSplit { + strict_query: strict_query.clone(), + terms: terms.clone(), + label, + }, + LexicalRouteKindV1::Alias { + strict_query, + alternative, + reason: LexicalAlternativeReasonV1::ConfiguredVocabularyAlias, + } => SearchLexicalRouteV1::Alias { + strict_query: strict_query.clone(), + alternative: alternative.clone(), + reason: SearchLexicalAlternativeReasonV1::ConfiguredVocabularyAlias, + label, + }, + } +} + +/// One caller anchor's kernel receipt in its wire shape. +pub(super) fn anchor_outcome(receipt: &LexicalAnchorReceiptV1) -> ContextLexicalAnchorV1 { + let anchor = receipt.anchor.as_str().to_owned(); + match &receipt.outcome { + LexicalAnchorOutcomeV1::Matched { + matched, + admitted, + dropped, + } => ContextLexicalAnchorV1::Matched { + anchor, + matched: *matched, + admitted: *admitted, + dropped: dropped.clone(), + }, + LexicalAnchorOutcomeV1::Unmatched => ContextLexicalAnchorV1::Unmatched { anchor }, + LexicalAnchorOutcomeV1::NotServed => ContextLexicalAnchorV1::NotServed { anchor }, + } } pub(super) fn result_route_suffix(result: &Value) -> String { @@ -272,13 +311,20 @@ pub(super) fn append_routes_md(md: &mut Md, value: &Value) { mod tests { use std::collections::BTreeMap; + use serde_json::json; use tracedecay_query::retrieval::lexical::{LexicalRouteMatchV1, MAX_LEXICAL_ANCHORS_V1}; use super::*; + fn routing(arguments: &Value) -> Result { + let request: SearchSurfaceRequestV1 = + crate::handlers::support::decode_primitive_request(arguments, "tracedecay_search")?; + routing_from_request(&request) + } + #[test] - fn routing_args_decode_and_reject_typed_violations() { - let routing = routing_from_args(&json!({ + fn routing_requests_decode_and_reject_typed_violations() { + let routing_plan = routing(&json!({ "query": "memoization", "lexical_anchors": ["reserve_stock", "Foo::bar"], "prefer_symbol": true, @@ -297,50 +343,50 @@ mod tests { }], })) .expect("valid routing"); - assert_eq!(routing.anchors.len(), 2); - assert!(routing.prefer_symbol); - assert_eq!(routing.aliases[0].strict_query, "memoization"); - assert_eq!(routing.aliases[0].alternative, "cache"); - assert_eq!(routing.phrases, ["durable cache"]); - assert_eq!(routing.proximities[0].terms, ["durable", "owner"]); + assert_eq!(routing_plan.anchors.len(), 2); + assert!(routing_plan.prefer_symbol); + assert_eq!(routing_plan.aliases[0].strict_query, "memoization"); + assert_eq!(routing_plan.aliases[0].alternative, "cache"); + assert_eq!(routing_plan.phrases, ["durable cache"]); + assert_eq!(routing_plan.proximities[0].terms, ["durable", "owner"]); assert_eq!( - routing.field_filters, - [tracedecay_query::retrieval::lexical::LexicalFieldFilterV1 { - field: tracedecay_query::retrieval::lexical::LexicalFieldV1::Documentation, + routing_plan.field_filters, + [LexicalFieldFilterV1 { + field: LexicalFieldV1::Documentation, include: true, }] ); - let plain = routing_from_args(&json!({"query": "inventory"})).expect("query only"); + let plain = routing(&json!({"query": "inventory"})).expect("query only"); assert_eq!(plain, LexicalRoutingV1::default()); let too_many: Vec = (0..=MAX_LEXICAL_ANCHORS_V1) .map(|index| format!("anchor_{index}")) .collect(); - let error = routing_from_args(&json!({"lexical_anchors": too_many})) + let error = routing(&json!({"query": "q", "lexical_anchors": too_many})) .expect_err("anchor count is bounded"); assert!( error.to_string().contains("at most 8 anchors"), "typed bound in the message: {error}" ); - let error = routing_from_args(&json!({"lexical_anchors": ["ok", ""]})) + let error = routing(&json!({"query": "q", "lexical_anchors": ["ok", ""]})) .expect_err("empty anchors are rejected"); assert!(error.to_string().contains("anchor 1 is empty"), "{error}"); - let error = routing_from_args(&json!({"lexical_anchors": ["two words"]})) + let error = routing(&json!({"query": "q", "lexical_anchors": ["two words"]})) .expect_err("multi-term anchors are rejected"); assert!(error.to_string().contains("one identifier"), "{error}"); - let error = routing_from_args(&json!({"lexical_anchors": "reserve_stock"})) - .expect_err("a bare string is not an anchor list"); - assert!(error.to_string().contains("array of strings"), "{error}"); - let error = routing_from_args(&json!({"lexical_anchors": [1]})) - .expect_err("anchors must be strings"); - assert!( - error.to_string().contains("[0] must be a string"), - "{error}" + assert_eq!( + routing(&json!({"query": "q", "lexical_anchors": "reserve_stock"})) + .expect_err("a bare string is not an anchor list") + .to_string(), + "config error: invalid arguments for tracedecay_search: invalid type: string \"reserve_stock\", expected a sequence" + ); + assert_eq!( + routing(&json!({"query": "q", "prefer_symbol": "yes"})) + .expect_err("prefer_symbol must be a boolean") + .to_string(), + "config error: invalid arguments for tracedecay_search: invalid type: string \"yes\", expected a boolean" ); - let error = routing_from_args(&json!({"prefer_symbol": "yes"})) - .expect_err("prefer_symbol must be a boolean"); - assert!(error.to_string().contains("must be a boolean"), "{error}"); let aliases = (0..=tracedecay_query::retrieval::lexical::MAX_LEXICAL_ALIASES_V1) .map(|index| { @@ -351,28 +397,48 @@ mod tests { }) .collect::>(); assert!( - routing_from_args(&json!({"lexical_aliases": aliases})) + routing(&json!({"query": "q", "lexical_aliases": aliases})) .expect_err("aliases are bounded") .to_string() .contains("at most 8") ); } + fn row(anchor: &str) -> SearchResultRowV1 { + SearchResultRowV1 { + candidate: serde_json::from_value(json!({ + "anchor_id": anchor, + "logical_evidence_id": "evidence.reserve", + "occurrences": [], + "exact_class": "approximate", + "utility_micros": 0, + "contributions": [], + "freshness": [], + "decisions": [], + })) + .expect("fused candidate"), + final_ordinal: 0, + node_id: None, + display: None, + lexical_routes: None, + } + } + #[test] fn route_evidence_is_attached_only_when_additional_routes_ran() { - let mut output = json!({"results": []}); - let mut results = vec![json!({"candidate": {"anchor_id": "code-symbol:reserve"}})]; + let mut results = vec![row("code-symbol:reserve")]; let query_only = LexicalRouteReceiptV1 { routes: vec![LexicalRouteKindV1::Query], matches_by_anchor: BTreeMap::new(), anchors: Vec::new(), dropped_sites: BTreeMap::new(), }; - attach_route_evidence(&mut output, &mut results, &query_only).expect("attach"); - assert!(output.get("lexical_routes").is_none()); - assert!(results[0].get("lexical_routes").is_none()); - assert_eq!(result_route_suffix(&results[0]), ""); - + assert_eq!(route_evidence(&mut results, &query_only), (None, None)); + assert_eq!(results[0].lexical_routes, None); + assert_eq!( + result_route_suffix(&serde_json::to_value(&results[0]).unwrap()), + "" + ); let anchor = LexicalRoutingV1::new(vec!["reserve_stock".to_owned()], true) .expect("routing") .anchors @@ -440,7 +506,9 @@ mod tests { ], dropped_sites: BTreeMap::new(), }; - attach_route_evidence(&mut output, &mut results, &receipt).expect("attach"); + let (routes, anchors) = route_evidence(&mut results, &receipt); + let output = json!({"lexical_routes": routes, "lexical_anchors": anchors}); + let results = [serde_json::to_value(&results[0]).unwrap()]; assert_eq!( output["lexical_anchors"], json!([ diff --git a/crates/tracedecay-mcp/src/handlers/graph/mod.rs b/crates/tracedecay-mcp/src/handlers/graph/mod.rs index 4364a0aa65..49c5cda9a2 100644 --- a/crates/tracedecay-mcp/src/handlers/graph/mod.rs +++ b/crates/tracedecay-mcp/src/handlers/graph/mod.rs @@ -1,25 +1,29 @@ //! Portable graph-navigation handlers over one source-bound verified query. +mod clones; +mod context; mod context_markdown; mod context_support; -mod dispatch; +mod exact_symbol; mod lexical_routing; mod navigation; mod primitive_surface; +mod rename_preview; mod search; mod search_evidence; mod search_freshness; mod verified; +pub use clones::{compute_redundancy, compute_similar}; +pub use context::compute_context; pub(crate) use context_markdown::render_context; -pub use dispatch::dispatch_tool; +pub use exact_symbol::compute_find_exact_symbol; pub use navigation::{ compute_by_qualified_name, compute_derives, compute_impact, compute_node, compute_signature, }; -pub use search::{ - compute_context, compute_find_exact_symbol, compute_redundancy, compute_rename_preview, - compute_similar, handle_search, -}; +pub use rename_preview::compute_rename_preview; +pub use search::compute_search; +pub(crate) use search::render_search; pub use verified::{ GRAPH_RELATION_READ_LIMIT, VerifiedNeighbor, cost_to_expand_verified, graph_occurrence_id, graph_symbol_corrupt, graph_symbol_end_line, graph_symbol_paths, graph_symbols_in_scope, diff --git a/crates/tracedecay-mcp/src/handlers/graph/rename_preview.rs b/crates/tracedecay-mcp/src/handlers/graph/rename_preview.rs new file mode 100644 index 0000000000..74873a05b9 --- /dev/null +++ b/crates/tracedecay-mcp/src/handlers/graph/rename_preview.rs @@ -0,0 +1,287 @@ +//! `tracedecay_rename_preview`: what a rename of one symbol would touch. + +use std::collections::HashMap; +use std::path::Path; + +use serde_json::Value; +use tracedecay_contracts::graph_tool::{GraphToolCompletionV1, GraphToolResultV1}; +use tracedecay_contracts::retrieval::{ + RenamePreviewNodeV1, RenamePreviewPrimitiveOutcomeV1, RenamePreviewPrimitiveRequestV1, + RenamePreviewPrimitiveResultV1, RenamePreviewReferenceV1, RenamePreviewTextOnlyMatchV1, +}; +use tracedecay_domain::errors::{Result, TraceDecayError}; + +use crate::McpToolContext; +use crate::handlers::support::{decode_primitive_request, unique_file_paths}; + +use super::{ + graph_occurrence_id, graph_tool_completion, line_for_byte_offset, node_not_found_result, + required_graph_file_path, required_graph_metadata, single_graph_adjacency_batch, user_line, +}; + +/// Reads a file's lines (0-based) for snippet extraction, memoizing by path so +/// a file with many references is read once. `None` when the file cannot be +/// read (e.g. deleted since indexing). +fn cached_file_lines<'a>( + project_root: &Path, + cache: &'a mut HashMap>>, + file_path: &str, +) -> Option<&'a [String]> { + if !cache.contains_key(file_path) { + let abs = project_root.join(file_path); + let lines = std::fs::read_to_string(&abs) + .ok() + .map(|source| source.lines().map(str::to_string).collect::>()); + cache.insert(file_path.to_string(), lines); + } + cache + .get(file_path) + .and_then(Option::as_ref) + .map(Vec::as_slice) +} + +/// Trims and length-caps a source line for use as a preview snippet. +fn snippet_text(line: &str) -> String { + tracedecay_runtime_core::text::utf8_prefix_at_or_before(line.trim(), 160).to_string() +} + +/// Picks a current-text snippet near `approx_line` (0-based; edge line bases are +/// approximate, so neighbors are tried) that actually contains `name`, falling +/// back to the line itself. `None` when no line is available. +fn reference_line_snippet( + lines: &[String], + approx_line: Option, + name: &str, +) -> Option { + let approx = approx_line? as usize; + let candidates = [approx, approx.saturating_sub(1), approx + 1]; + let idx = candidates + .into_iter() + .find(|&i| lines.get(i).is_some_and(|line| line.contains(name))) + .unwrap_or(approx); + lines.get(idx).map(|line| snippet_text(line)) +} + +/// True for bytes that can appear inside an identifier. Non-ASCII bytes count so +/// multi-byte unicode identifiers are not falsely split at a boundary. +fn is_ident_byte(b: u8) -> bool { + b == b'_' || b.is_ascii_alphanumeric() || b >= 0x80 +} + +/// Counts occurrences of `name` in `haystack` bounded as a whole identifier +/// (neither neighbouring byte is an identifier byte). Used to estimate the +/// literal textual matches a rename would touch, independent of the graph. +fn count_identifier_occurrences(haystack: &str, name: &str) -> usize { + if name.is_empty() { + return 0; + } + let bytes = haystack.as_bytes(); + let name_len = name.len(); + let mut count = 0; + let mut start = 0; + while let Some(pos) = haystack[start..].find(name) { + let abs = start + pos; + let before_ok = abs == 0 || !is_ident_byte(bytes[abs - 1]); + let after_idx = abs + name_len; + let after_ok = after_idx >= bytes.len() || !is_ident_byte(bytes[after_idx]); + if before_ok && after_ok { + count += 1; + } + start = abs + name_len; + } + count +} + +/// Graph-derived inputs for one rename-preview reference site, extracted +/// before the blocking file walk so the worker needs no graph access. +struct RenameReferenceSiteInput { + from_node_id: String, + from_name: String, + from_kind: String, + edge_kind: String, + file: String, + evidence_start_byte: u64, +} + +/// READ-ONLY: reports what a rename of the given symbol WOULD touch, the +/// declaration site and every graph reference site (incoming edges; outgoing +/// edges reference other symbols and so are excluded), each with a +/// current-text snippet, plus a per-file count of literal name occurrences +/// that are NOT backed by a graph edge ("text-only matches, review +/// manually"). Nothing is rewritten. +#[hotpath::measure(label = "mcp.graph.rename_preview.total")] +pub async fn compute_rename_preview( + ctx: &McpToolContext<'_>, + graph: &tracedecay_graph_query::VerifiedGraphQuery, + args: Value, +) -> Result { + let request: RenamePreviewPrimitiveRequestV1 = + decode_primitive_request(&args, "tracedecay_rename_preview")?; + + let occurrence = graph_occurrence_id(&request.node_id)?; + // Graph occurrences per file (declaration + reference sites), subtracted + // from the literal textual count to isolate the text-only matches. + let mut graph_counts: HashMap = HashMap::new(); + let mut touched: Vec = Vec::new(); + // Graph phase: extract owned declaration fields and per-reference-site + // inputs so the blocking file walk below needs no graph value at all. + let (mut declaration, declaration_line, symbol_name, reference_inputs) = + hotpath::measure_block!("mcp.graph.rename_preview.graph", { + let Some(node) = graph.symbol_summary(&occurrence)? else { + return Ok(graph_tool_completion( + GraphToolResultV1::RenamePreview(RenamePreviewPrimitiveOutcomeV1::NotFound( + node_not_found_result(&request.node_id), + )), + Vec::new(), + )); + }; + let node_metadata = required_graph_metadata(&node)?; + let node_file = required_graph_file_path(&node)?; + let symbol_name = node_metadata.simple_name.clone(); + + touched.push(node_file.to_owned()); + *graph_counts.entry(node_file.to_owned()).or_default() += 1; + let declaration = RenamePreviewNodeV1 { + id: node.occurrence.as_str().to_owned(), + name: node_metadata.simple_name.clone(), + qualified_name: node_metadata.qualified_name.clone(), + kind: node_metadata.kind.clone(), + file: node_file.to_owned(), + line: user_line(node_metadata.start_line), + snippet: None, + }; + + // Reference sites: incoming edges are the callers/users that name this + // symbol. NOTE: call-edge coverage improves as the resolver improves; + // the text-only counts below catch what the graph currently misses. + let incoming = single_graph_adjacency_batch(graph.callers( + std::slice::from_ref(&node.occurrence), + &[], + 2_000_000, + )?)?; + let mut reference_inputs = + Vec::::with_capacity(incoming.len()); + for edge in incoming { + let source_node = edge.neighbor; + let source_metadata = required_graph_metadata(&source_node)?; + let source_file = required_graph_file_path(&source_node)?; + touched.push(source_file.to_owned()); + *graph_counts.entry(source_file.to_owned()).or_default() += 1; + reference_inputs.push(RenameReferenceSiteInput { + from_node_id: source_node.occurrence.as_str().to_owned(), + from_name: source_metadata.simple_name.clone(), + from_kind: source_metadata.kind.clone(), + edge_kind: edge.edge.kind.as_str().to_owned(), + file: source_file.to_owned(), + evidence_start_byte: edge.edge.evidence_span.start_byte, + }); + } + ( + declaration, + node_metadata.start_line, + symbol_name, + reference_inputs, + ) + }); + + let touched_files = unique_file_paths(touched.iter().map(std::string::String::as_str)); + + // File-walk phase: every referenced source file is read from disk, so it + // runs on a blocking worker like the sibling analysis scans instead of + // holding the async dispatch thread through the reads. + let project_root = ctx.project_root().to_path_buf(); + let declaration_file = declaration.file.clone(); + let walk_symbol_name = symbol_name.clone(); + let walk_graph_counts = graph_counts; + let walk_touched_files = touched_files.clone(); + let (decl_snippet, references, text_only_matches) = hotpath::future!( + tokio::task::spawn_blocking( + move || -> Result<( + Option, + Vec, + Vec, + )> { + let mut lines_cache: HashMap>> = HashMap::new(); + let decl_snippet = + cached_file_lines(&project_root, &mut lines_cache, &declaration_file).and_then( + |lines| { + lines + .get(declaration_line as usize) + .map(|line| snippet_text(line)) + }, + ); + + let mut references = + Vec::::with_capacity(reference_inputs.len()); + for input in reference_inputs { + let source = tracedecay_runtime_core::sync::read_source_file(&project_root.join(&input.file))?; + let line = line_for_byte_offset(&source, input.evidence_start_byte)?; + let snippet = cached_file_lines(&project_root, &mut lines_cache, &input.file) + .and_then(|lines| reference_line_snippet(lines, Some(line), &walk_symbol_name)); + references.push(RenamePreviewReferenceV1 { + from_node_id: input.from_node_id, + from_name: input.from_name, + from_kind: input.from_kind, + edge_kind: input.edge_kind, + file: input.file, + line: user_line(line), + snippet, + }); + } + + // Text-only matches per touched file: literal identifier occurrences + // of the name minus the graph occurrences already accounted for. + // These are the comments/strings/dynamic-dispatch/unresolved sites a + // graph-only rename would miss, the scan is bounded to files that + // already appear in the preview, so occurrences in wholly unrelated + // files are not counted. + let mut text_only_matches = Vec::::new(); + for file in &walk_touched_files { + let total = + cached_file_lines(&project_root, &mut lines_cache, file).map_or(0, |lines| { + lines + .iter() + .map(|line| count_identifier_occurrences(line, &walk_symbol_name)) + .sum::() + }); + let graph = walk_graph_counts.get(file).copied().unwrap_or(0); + let text_only = total.saturating_sub(graph); + if text_only > 0 { + text_only_matches.push(RenamePreviewTextOnlyMatchV1 { + file: file.clone(), + text_only_count: text_only, + note: "text-only matches, review manually".to_owned(), + }); + } + } + Ok((decl_snippet, references, text_only_matches)) + } + ), + label = "mcp.graph.rename_preview.walk" + ) + .await + .map_err(|join_error| TraceDecayError::Config { + message: format!("rename preview file scan task failed: {join_error}"), + })??; + declaration.snippet = decl_snippet; + + let result = RenamePreviewPrimitiveResultV1 { + read_only: true, + note: "Preview only. Nothing is edited. 'references' are graph reference sites \ + (the declaration is reported separately in 'node'); 'text_only_matches' are \ + literal name occurrences NOT backed by a graph edge (comments, strings, \ + dynamic dispatch, unresolved refs) and must be reviewed by hand. Graph \ + call-edge coverage improves as the resolver does." + .to_owned(), + symbol: symbol_name, + new_name: request.new_name, + node: declaration, + reference_count: references.len(), + references, + text_only_matches, + }; + Ok(graph_tool_completion( + GraphToolResultV1::RenamePreview(RenamePreviewPrimitiveOutcomeV1::Preview(result)), + touched_files, + )) +} diff --git a/crates/tracedecay-mcp/src/handlers/graph/search.rs b/crates/tracedecay-mcp/src/handlers/graph/search.rs index e022570794..502fad942a 100644 --- a/crates/tracedecay-mcp/src/handlers/graph/search.rs +++ b/crates/tracedecay-mcp/src/handlers/graph/search.rs @@ -1,48 +1,31 @@ -//! Graph leftover handlers that read the admitted project/request authorities: -//! `search`, `context`, `similar`, `find_exact_symbol`, `rename_preview`. +//! `tracedecay_search`: exact and lexical code search over the served code +//! generation. +//! +//! The graph-tool owner decodes the typed request and computes the typed +//! result; every surface renders that result through [`render_search`]. -use std::collections::HashMap; use std::future::Future; use std::path::Path; -use serde_json::{Value, json}; -use tracedecay_code_index::graph_projection::CodeGraphSymbolSummaryV1; +use serde_json::Value; +use tracedecay_contracts::ApplicationProblemDetailV1; use tracedecay_contracts::graph_tool::{GraphToolCompletionV1, GraphToolResultV1}; use tracedecay_contracts::retrieval::{ - ContextCodeBlockV1, ContextLexicalAnchorV1, ContextModeV1, ContextRelatedOmissionV1, - ContextResultV1, ContextRetrievalPlanV1, ContextSearchMatchV1, ContextStageV1, - ContextSurfaceRequestV1, FindExactSymbolMatchV1, FindExactSymbolResultV1, - FindExactSymbolSurfaceRequestV1, LexicalAnchorDropReasonV1, RedundancyScopeV1, - RedundancySurfaceRequestV1, RenamePreviewNodeV1, RenamePreviewPrimitiveOutcomeV1, - RenamePreviewPrimitiveRequestV1, RenamePreviewPrimitiveResultV1, RenamePreviewReferenceV1, - RenamePreviewTextOnlyMatchV1, SimilarCoverageV1, SimilarFamilyV1, SimilarMatchClassV1, - SimilarOccurrenceV1, SimilarResultV1, SimilarSurfaceRequestV1, SimilarTargetV1, + PrimitiveLaneCompleteV1, PrimitiveRecallV1, PrimitiveSearchFreshnessV1, + PrimitiveUnavailableStatusV1, SearchCompleteV1, SearchCoverageV1, SearchLaneStateV1, + SearchLaneStatusV1, SearchResultDisplayV1, SearchResultRowV1, SearchResultV1, + SearchSurfaceRequestV1, SearchUnavailableV1, }; -use tracedecay_contracts::{ApplicationProblemDetailV1, InvocationAnalyticsV1}; -use tracedecay_domain::errors::{Result, TraceDecayError}; -use tracedecay_domain::{ExactClass, RelationEdgeKindV1}; -use tracedecay_query::retrieval::lexical::LexicalAnchorOutcomeV1; -#[cfg(test)] -use tracedecay_query::retrieval::lexical::LexicalRoutingV1; +use tracedecay_domain::errors::Result; +use tracedecay_query::code_search::{CodeIndexLaneStatusV1, CodeIndexSearchCoverageV1}; -#[cfg(test)] -use crate::context_headings::CONTEXT_SEEN_NODE_IDS_LABEL; use crate::handlers::dependency_hints; use crate::handlers::support::{ - decode_primitive_request, rendered_tool_result as support_rendered, retrieval_cursor, - unique_file_paths, + decode_primitive_request, decode_retrieval_cursor, rendered_tool_result, unique_file_paths, }; use crate::tools::render::{self, Md}; use crate::{McpToolContext, ToolResult}; -use super::context_markdown::verified_plan_context; -use super::context_support::{ - ContextMemoryOutcome, context_memory_analytics, context_memory_options, context_memory_outcome, - context_memory_read_control, context_memory_stage, -}; -use super::primitive_surface::{ - search_coverage as primitive_search_coverage, symbol_location as primitive_symbol_location, -}; use super::search_evidence::{ SearchGraphEvidence, bind_verified_graph_to_search, race_primary_search_with_graph, }; @@ -50,17 +33,10 @@ use super::search_freshness::{ ServedGenerationV1, freshness_lines, search_freshness, worktree_freshness_from_payload, }; use super::verified::CODE_SYMBOL_EVIDENCE_PREFIX; -use super::{ - graph_occurrence_id, graph_symbol_end_line, graph_symbol_paths, graph_symbols_in_scope, - graph_tool_completion, line_for_byte_offset, node_not_found_result, required_graph_file_path, - required_graph_metadata, single_graph_adjacency_batch, user_line, -}; +use super::{graph_occurrence_id, graph_tool_completion}; use super::{lexical_routing, search_evidence}; -#[cfg(test)] -use super::context_support::{context_markdown_lane_preview, context_memory_section}; - -async fn execute_code_index_search( +pub(super) async fn execute_code_index_search( executor: Option<&tracedecay_query::code_search::CodeIndexSearchExecutor>, request: tracedecay_query::code_search::CodeIndexSearchRequestV1, ) -> tracedecay_query::code_search::CodeIndexSearchOutcomeV1 { @@ -97,64 +73,47 @@ fn preserve_complete_search_after_lazy_admission(result: Result<()>) -> Result<( } } -/// Renders the per-lane recall marker so a caller can tell a full-recall -/// answer from one produced while a lane was down. Emitted on every search -/// response, including the successful ones, because "no matches" and "the -/// matching lane was not running" are otherwise indistinguishable. -fn coverage_value(coverage: &tracedecay_query::code_search::CodeIndexSearchCoverageV1) -> Value { - fn lane(status: &tracedecay_query::code_search::CodeIndexLaneStatusV1) -> Value { +/// The per-lane recall marker every search response carries, so "no +/// matches" is told apart from "the matching lane was not running". +fn search_coverage(coverage: &CodeIndexSearchCoverageV1) -> SearchCoverageV1 { + fn lane(status: &CodeIndexLaneStatusV1) -> SearchLaneStatusV1 { match status { - tracedecay_query::code_search::CodeIndexLaneStatusV1::Complete => json!("complete"), - tracedecay_query::code_search::CodeIndexLaneStatusV1::Stale { generation } => json!({ - "status": "stale", - "generation": generation, - }), - tracedecay_query::code_search::CodeIndexLaneStatusV1::Partial { - generation, - reason, - } => { - json!({ - "status": "partial", - "generation": generation, - "reason": reason, + CodeIndexLaneStatusV1::Complete => { + SearchLaneStatusV1::Complete(PrimitiveLaneCompleteV1::Complete) + } + CodeIndexLaneStatusV1::Stale { generation } => { + SearchLaneStatusV1::State(SearchLaneStateV1::Stale { + generation: generation.clone(), + }) + } + CodeIndexLaneStatusV1::Partial { generation, reason } => { + SearchLaneStatusV1::State(SearchLaneStateV1::Partial { + generation: generation.clone(), + reason: reason.map(str::to_owned), + }) + } + CodeIndexLaneStatusV1::Unavailable { reason } => { + SearchLaneStatusV1::State(SearchLaneStateV1::Unavailable { + reason: (*reason).to_owned(), }) } - tracedecay_query::code_search::CodeIndexLaneStatusV1::Unavailable { reason } => json!({ - "status": "unavailable", - "reason": reason, - }), } } - - json!({ - "exact": lane(&coverage.exact), - "lexical": lane(&coverage.lexical), - "graph": lane(&coverage.graph), - "recall": if coverage.is_degraded() { "partial" } else { "full" }, - }) -} - -fn rendered_tool_result( - ctx: &McpToolContext<'_>, - args: &Value, - value: &Value, - touched_files: Vec, - md: F, -) -> ToolResult -where - F: FnOnce() -> String, -{ - support_rendered( - Some(&ctx.store_layout().response_handle_root), - args, - value, - touched_files, - md, - ) + SearchCoverageV1 { + exact: lane(&coverage.exact), + lexical: lane(&coverage.lexical), + graph: lane(&coverage.graph), + recall: if coverage.is_degraded() { + PrimitiveRecallV1::Partial + } else { + PrimitiveRecallV1::Full + }, + } } +/// Computes one `tracedecay_search` page on the graph-tool owner's side. #[hotpath::measure(label = "mcp.graph.search.total")] -pub async fn handle_search( +pub async fn compute_search( ctx: &McpToolContext<'_>, graph: F, args: Value, @@ -162,29 +121,21 @@ pub async fn handle_search( ignored_dependency_admission: Option< &dyn tracedecay_application::code_index::CodeIndexIgnoredDependencyAdmissionPortV1, >, -) -> Result +) -> Result where F: Future>, { + let request: SearchSurfaceRequestV1 = decode_primitive_request(&args, "tracedecay_search")?; let search_executor = ctx.code_index_search_executor(); let search_authority = ctx.code_index_search_authority(); let deadline = ctx.deadline().cloned(); let cancellation = ctx.cancellation().cloned(); - let query = - args.get("query") - .and_then(|v| v.as_str()) - .ok_or_else(|| TraceDecayError::Config { - message: "missing required parameter: query".to_string(), - })?; - - let lexical_routing = lexical_routing::routing_from_args(&args)?; - let lazy_indexing_requested = dependency_hints::lazy_indexing_requested(&args); - let cursor = retrieval_cursor(&args)?; + let lexical_routing = lexical_routing::routing_from_request(&request)?; + let lazy_indexing_requested = request.lazy_index_ignored_dependencies.unwrap_or(false); + let cursor = decode_retrieval_cursor(request.cursor.as_deref())?; let include_graph_node_ids = render::wants_json(&args); - let limit = args - .get("limit") - .and_then(serde_json::Value::as_u64) - .map_or(10, |v| v.min(500) as usize); + let limit = request.limit.map_or(10, |v| v.min(500) as usize); + let query = request.query.as_str(); // A scope prefix cannot be applied as a post-filter here the way the // sibling handlers do it: the retrieval pipeline returns anchor-keyed // candidates that carry no file path. Refusing to search at all would make @@ -281,105 +232,78 @@ where ); hotpath::measure_block!("mcp.graph.search.graph", { for ranked in &complete.ordered_candidates { - let mut result = json!(ranked); let anchor = ranked.candidate.anchor_id.as_str(); - if anchor.starts_with(CODE_SYMBOL_EVIDENCE_PREFIX) { - result["node_id"] = json!(graph_occurrence_id(anchor)?); - } - if let Some(display) = - complete.display_by_anchor.get(&ranked.candidate.anchor_id) + let mut node_id = if anchor.starts_with(CODE_SYMBOL_EVIDENCE_PREFIX) { + Some(graph_occurrence_id(anchor)?.as_str().to_owned()) + } else { + None + }; + let display = complete.display_by_anchor.get(&ranked.candidate.anchor_id); + if include_graph_node_ids + && node_id.is_none() + && let Some(display) = display { - result["display"] = json!({ - "name": display.name, - "qualified_name": display.qualified_name, - "kind": display.kind, - "path": display.path, - }); - if include_graph_node_ids && result.get("node_id").is_none() { - graph_evidence.enrich_node_id(&mut result, display); - } + node_id = graph_evidence.node_id_for(display); } - results.push(result); + results.push(SearchResultRowV1 { + candidate: ranked.candidate.clone(), + final_ordinal: ranked.final_ordinal, + node_id, + display: display.map(|display| SearchResultDisplayV1 { + name: display.name.clone(), + qualified_name: display.qualified_name.clone(), + kind: display.kind.clone(), + path: display.path.clone(), + }), + lexical_routes: None, + }); } }); let result_count = results.len(); - let freshness = search_freshness( - ServedGenerationV1::Served(&complete.code_generation), - &complete.coverage, - &worktree_freshness, - ); - let mut output = hotpath::measure_block!( - "mcp.graph.search.serialize", - json!({ - "freshness": freshness, - "code_generation": complete.code_generation, - "query_fallback_digest": &complete.query_fallback.digest, - "next_cursor": complete.next_cursor + let (lexical_routes, lexical_anchors) = + lexical_routing::route_evidence(&mut results, &complete.lexical_routes); + let external_import_hint = if scope_prefix.is_some() + || dependency_hints::should_check_external_import_hint(result_count, limit) + { + graph_evidence.external_import_hint(ctx, query, limit, scope_prefix) + } else { + None + }; + let result = SearchCompleteV1 { + freshness: search_freshness( + ServedGenerationV1::Served(&complete.code_generation), + &complete.coverage, + &worktree_freshness, + ), + query_fallback_digest: complete.query_fallback.digest.as_str().to_owned(), + next_cursor: complete + .next_cursor .as_ref() .map(serde_json::to_string) .transpose()?, - "coverage": coverage_value(&complete.coverage), - }) - ); - lexical_routing::attach_route_evidence( - &mut output, - &mut results, - &complete.lexical_routes, - )?; - output["results"] = Value::Array(results); - if let Some(scope) = scope_prefix { - output["scope_prefix"] = json!(scope); - output["scope_prefix_applied"] = json!(false); - } - if let Some(unavailable) = graph_evidence.unavailable() { - output["verified_graph_evidence"] = unavailable.clone(); - } - if (scope_prefix.is_some() - || dependency_hints::should_check_external_import_hint(result_count, limit)) - && let Some(hint) = - graph_evidence.external_import_hint(ctx, query, limit, scope_prefix) - { - output["external_import_hint"] = hint; - } - let output = output; - Ok(rendered_tool_result( - ctx, - &args, - &output, + coverage: search_coverage(&complete.coverage), + code_generation: complete.code_generation, + results, + lexical_routes, + lexical_anchors, + scope_prefix: scope_prefix.map(str::to_owned), + scope_prefix_applied: scope_prefix.map(|_| false), + verified_graph_evidence: graph_evidence.unavailable().cloned(), + external_import_hint, + }; + Ok(graph_tool_completion( + GraphToolResultV1::Search(Box::new(SearchResultV1::Complete(result))), touched_files, - || { - format!( - "{}{}", - freshness_lines(&freshness), - render_search_md(&output) - ) - }, )) } tracedecay_query::code_search::CodeIndexSearchOutcomeV1::Unavailable(unavailable) => { let reason = unavailable.reason.as_str(); - let graph_evidence = SearchGraphEvidence::new(graph.as_ref()); let freshness = search_freshness( ServedGenerationV1::Unavailable { reason }, &unavailable.coverage, &worktree_freshness, ); - let mut output = hotpath::measure_block!( - "mcp.graph.search.serialize", - json!({ - "freshness": freshness, - "results": [], - "code_generation": unavailable.code_generation, - "query_fallback_digest": Value::Null, - "status": "unavailable", - "reason": reason, - "coverage": coverage_value(&unavailable.coverage), - }) - ); - if let Some(unavailable_graph) = graph_evidence.unavailable() { - output["verified_graph_evidence"] = unavailable_graph.clone(); - } - let parked = freshness + let detail = freshness .indexing .as_ref() .and_then(|indexing| indexing.parked.as_ref()) @@ -388,25 +312,54 @@ where remedy: parked.remediation.clone(), retries_on_wake: parked.retries_on_wake, }); - let failure = match &parked { - Some(detail) => detail.message(), - None => format!("code-index search unavailable: {reason}"), + let result = SearchUnavailableV1 { + freshness, + results: Vec::new(), + code_generation: unavailable.code_generation, + query_fallback_digest: None, + status: PrimitiveUnavailableStatusV1::Unavailable, + reason: reason.to_owned(), + coverage: search_coverage(&unavailable.coverage), + verified_graph_evidence: SearchGraphEvidence::new(graph.as_ref()) + .unavailable() + .cloned(), + detail, }; - if let Some(detail) = parked { - output["detail"] = serde_json::to_value(detail)?; - } - Ok(rendered_tool_result(ctx, &args, &output, Vec::new(), || { - format!( - "{}{}", - freshness_lines(&freshness), - render_search_md(&output) - ) - }) - .with_failure_message(failure)) + Ok(graph_tool_completion( + GraphToolResultV1::Search(Box::new(SearchResultV1::Unavailable(result))), + Vec::new(), + )) } } } +/// Renders a search result as its tool result: the `freshness:` verdict line +/// ahead of the markdown body, or the typed JSON. An unavailable search names +/// its failure, parked indexing by its cause and remedy. +pub(crate) fn render_search( + response_handle_root: Option<&Path>, + args: &Value, + result: &SearchResultV1, +) -> Result { + let value = serde_json::to_value(result)?; + let freshness: &PrimitiveSearchFreshnessV1 = match result { + SearchResultV1::Complete(complete) => &complete.freshness, + SearchResultV1::Unavailable(unavailable) => &unavailable.freshness, + }; + let rendered = rendered_tool_result(response_handle_root, args, &value, Vec::new(), || { + format!("{}{}", freshness_lines(freshness), render_search_md(&value)) + }); + Ok(match result { + SearchResultV1::Complete(_) => rendered, + SearchResultV1::Unavailable(unavailable) => { + rendered.with_failure_message(match &unavailable.detail { + Some(detail) => detail.message(), + None => format!("code-index search unavailable: {}", unavailable.reason), + }) + } + }) +} + /// Warns, in the human-facing body, that a result list is short because a lane /// was missing. A degraded page is otherwise indistinguishable from a thorough /// one, which is exactly how a partial answer gets trusted as a complete one. @@ -536,1166 +489,13 @@ fn render_search_md(value: &Value) -> String { md.render() } -/// Edge rows each direction of the related-symbol walk reads across all -/// selected symbols. The walk takes each neighbor from its edge row and -/// ranks it from the catalog without reading a symbol entity, so it covers -/// a hub's neighborhood before the `max_nodes` cut; past this many rows the -/// omission reports its total as a lower bound. -const CONTEXT_RELATED_WALK_ROWS: usize = 4_096; - -/// Related symbols rank by the best edge kind joining them to a selected -/// symbol, lower first: behavior (calls), then the type hierarchy -/// (implements, extends), then signatures (`type_of`, returns, receives), -/// then looser references (uses, annotates), then containment. -fn context_related_kind_rank(kind: RelationEdgeKindV1) -> u8 { - match kind { - RelationEdgeKindV1::Calls => 0, - RelationEdgeKindV1::Implements => 1, - RelationEdgeKindV1::Extends => 2, - RelationEdgeKindV1::TypeOf => 3, - RelationEdgeKindV1::Returns => 4, - RelationEdgeKindV1::Receives => 5, - RelationEdgeKindV1::Uses => 6, - RelationEdgeKindV1::Annotates => 7, - RelationEdgeKindV1::Contains => 8, - } -} - -#[derive(Default)] -struct ContextGraphProjection { - selected: Vec, - related: Vec, - related_omission: Option, - code_blocks: Vec, - touched_files: Vec, -} - -fn context_search_matches( - complete: &tracedecay_query::code_search::CodeIndexSearchCompletedV1, - scope_prefix: Option<&str>, -) -> Vec { - complete - .ordered_candidates - .iter() - .filter_map(|ranked| { - let display = complete - .display_by_anchor - .get(&ranked.candidate.anchor_id)?; - if scope_prefix.is_some_and(|prefix| !display.path.starts_with(prefix)) { - return None; - } - let exact_class = match ranked.candidate.exact_class { - ExactClass::ExactMessage => "exact_message", - ExactClass::ExactLiteralPhrase => "exact_literal_phrase", - ExactClass::Approximate => "approximate", - }; - Some(ContextSearchMatchV1 { - anchor_id: ranked.candidate.anchor_id.as_str().to_owned(), - name: display.name.clone(), - qualified_name: display.qualified_name.clone(), - kind: display.kind.clone(), - file: display.path.clone(), - exact_class: exact_class.to_owned(), - rank: ranked.final_ordinal.saturating_add(1), - utility_micros: ranked.candidate.utility_micros, - }) - }) - .collect() -} - -/// The kernel's per-anchor receipts in the context wire shape, counted -/// against the search matches this context carries: a served site outside -/// `scope_prefix` is dropped as out of scope. -fn context_lexical_anchors( - complete: &tracedecay_query::code_search::CodeIndexSearchCompletedV1, - scope_prefix: Option<&str>, -) -> Vec { - let mut receipt = complete.lexical_routes.clone(); - if let Some(prefix) = scope_prefix { - receipt.reconcile_served(|site| { - complete - .display_by_anchor - .get(site) - .filter(|display| !display.path.starts_with(prefix)) - .map(|_| LexicalAnchorDropReasonV1::OutOfScope) - }); - } - receipt - .anchors - .into_iter() - .map(|receipt| { - let anchor = receipt.anchor.as_str().to_owned(); - match receipt.outcome { - LexicalAnchorOutcomeV1::Matched { - matched, - admitted, - dropped, - } => ContextLexicalAnchorV1::Matched { - anchor, - matched, - admitted, - dropped, - }, - LexicalAnchorOutcomeV1::Unmatched => ContextLexicalAnchorV1::Unmatched { anchor }, - LexicalAnchorOutcomeV1::NotServed => ContextLexicalAnchorV1::NotServed { anchor }, - } - }) - .collect() -} - -fn context_graph_projection( - ctx: &McpToolContext<'_>, - graph: &tracedecay_graph_query::VerifiedGraphQuery, - complete: &tracedecay_query::code_search::CodeIndexSearchCompletedV1, - scope_prefix: Option<&str>, - max_nodes: usize, - include_code: bool, - max_code_blocks: usize, -) -> Result { - let mut selected = Vec::new(); - for ranked in &complete.ordered_candidates { - let Some(display) = complete.display_by_anchor.get(&ranked.candidate.anchor_id) else { - continue; - }; - if scope_prefix.is_some_and(|prefix| !display.path.starts_with(prefix)) { - continue; - } - let candidates = - graph.resolve_qualified_name(&display.qualified_name, Some(&display.kind), 16)?; - for candidate in candidates { - if required_graph_file_path(&candidate)? == display.path.as_str() - && !selected.iter().any(|existing: &CodeGraphSymbolSummaryV1| { - existing.occurrence == candidate.occurrence - }) - { - selected.push(candidate); - break; - } - } - } - let seeds = selected - .iter() - .map(|symbol| symbol.occurrence.clone()) - .collect::>(); - let (related, related_omission) = if seeds.is_empty() { - (Vec::new(), None) - } else { - let ranked = graph.ranked_neighbors( - &seeds, - context_related_kind_rank, - CONTEXT_RELATED_WALK_ROWS, - max_nodes, - )?; - let omitted = ranked.total.saturating_sub(ranked.neighbors.len()); - let omission = (omitted > 0 || ranked.walk_truncated).then_some(ContextRelatedOmissionV1 { - total: ranked.total, - omitted, - total_is_lower_bound: ranked.walk_truncated, - }); - (ranked.neighbors, omission) - }; - - let mut all_symbols = selected.clone(); - all_symbols.extend(related.iter().cloned()); - let touched_files = graph_symbol_paths(&all_symbols)?; - let mut code_blocks = Vec::new(); - if include_code { - // Context snippets are filesystem windows, not the mmap'd sealed - // lexical artifact (that path serves n-gram search). Cache by path - // so five symbols in one file do not re-read the whole source. - let mut source_by_path = HashMap::::new(); - for symbol in selected.iter().take(max_code_blocks) { - let metadata = required_graph_metadata(symbol)?; - let file_path = required_graph_file_path(symbol)?; - if !source_by_path.contains_key(file_path) { - source_by_path.insert( - file_path.to_owned(), - tracedecay_runtime_core::sync::read_source_file( - &ctx.project_root().join(file_path), - )?, - ); - } - let Some(source) = source_by_path.get(file_path) else { - return Err(TraceDecayError::Config { - message: format!("context source window missing for '{file_path}'"), - }); - }; - code_blocks.push(ContextCodeBlockV1 { - node_id: symbol.occurrence.as_str().to_owned(), - file: file_path.to_owned(), - start_line: user_line(metadata.start_line), - end_line: user_line(graph_symbol_end_line(metadata)?), - code: extract_lines( - source, - metadata.start_line, - graph_symbol_end_line(metadata)?, - ), - }); - } - } - Ok(ContextGraphProjection { - selected, - related, - related_omission, - code_blocks, - touched_files, - }) -} - -/// Extract the source spanning tree-sitter rows `start_line..=end_line` -/// (0-based, inclusive) from `source`. Node line fields are stored as the -/// raw tree-sitter row index, so the caller passes them through unchanged. -/// Returns the empty string if the range is out of bounds. -fn extract_lines(source: &str, start_line: u32, end_line: u32) -> String { - let start = start_line as usize; - let end_exclusive = (end_line as usize).saturating_add(1); - if start >= end_exclusive { - return String::new(); - } - let mut selected = source.lines().skip(start).take(end_exclusive - start); - let Some(first) = selected.next() else { - return String::new(); - }; - let mut body = String::with_capacity(first.len()); - body.push_str(first); - for line in selected { - body.push('\n'); - body.push_str(line); - } - body -} - -#[hotpath::measure(label = "mcp.graph.context.total")] -pub async fn compute_context( - ctx: &McpToolContext<'_>, - graph: F, - args: Value, - scope_prefix: Option<&str>, -) -> Result -where - F: Future>, -{ - let search_executor = ctx.code_index_search_executor(); - let search_authority = ctx.code_index_search_authority(); - let deadline = ctx.deadline().cloned(); - let cancellation = ctx.cancellation().cloned(); - let request: ContextSurfaceRequestV1 = decode_primitive_request(&args, "tracedecay_context")?; - let task = request.task.as_str(); - let mode = request.mode.unwrap_or(ContextModeV1::Explore); - let max_nodes = request - .max_nodes - .map_or(20, |value| value.clamp(1, 200) as usize); - let include_code = request.include_code.unwrap_or(false); - let max_code_blocks = request - .max_code_blocks - .map_or(5, |value| value.clamp(1, 20) as usize); - let requested_anchors = request.lexical_anchors.clone().unwrap_or_default(); - let lexical_routing = lexical_routing::routing_from_parts( - requested_anchors.clone(), - request.prefer_symbol.unwrap_or(false), - )?; - let memory_options = context_memory_options(&args); - let memory_read_control = - context_memory_read_control(&memory_options, deadline.as_ref(), cancellation.as_ref())?; - // Graph enrichment is optional unless the caller asks for source bodies. - // That request waits for graph admission under the same deadline and - // cancellation as search; ordinary lexical/exact retrieval stays independent. - let search = execute_code_index_search( - search_executor, - tracedecay_query::code_search::CodeIndexSearchRequestV1 { - project_root: ctx.project_root().to_path_buf(), - query: task.to_owned(), - source_revision: None, - source_tree: None, - source_reference: None, - limit: max_nodes, - cursor: None, - lexical_routing, - authority: search_authority.cloned(), - deadline, - cancellation, - }, - ); - let memory = context_memory_outcome(ctx, task, &memory_options, memory_read_control.as_ref()); - let search_and_graph = race_primary_search_with_graph(search, graph, false, None, include_code); - let ((outcome, graph), memory_outcome) = tokio::join!(search_and_graph, memory); - // Read after the search settles: the verdict must describe the scheduler - // state at serve time, not a snapshot taken before the lanes ran. - let freshness_payload = ctx.freshness().await; - let worktree_freshness = worktree_freshness_from_payload(freshness_payload.as_ref()); - let (complete, code_generation, coverage, freshness, search_matches, lexical_anchors) = - match outcome { - tracedecay_query::code_search::CodeIndexSearchOutcomeV1::Complete(complete) => { - let search_matches = context_search_matches(&complete, scope_prefix); - let lexical_anchors = context_lexical_anchors(&complete, scope_prefix); - let code_generation = Some(complete.code_generation.clone()); - let coverage = primitive_search_coverage(&complete.coverage); - let freshness = search_freshness( - ServedGenerationV1::Served(&complete.code_generation), - &complete.coverage, - &worktree_freshness, - ); - ( - Some(complete), - code_generation, - coverage, - freshness, - search_matches, - lexical_anchors, - ) - } - tracedecay_query::code_search::CodeIndexSearchOutcomeV1::Unavailable(unavailable) => ( - None, - unavailable.code_generation, - primitive_search_coverage(&unavailable.coverage), - search_freshness( - ServedGenerationV1::Unavailable { - reason: unavailable.reason.as_str(), - }, - &unavailable.coverage, - &worktree_freshness, - ), - Vec::new(), - requested_anchors - .iter() - .map(|anchor| ContextLexicalAnchorV1::NotServed { - anchor: anchor.clone(), - }) - .collect(), - ), - }; - let graph = match complete.as_ref() { - Some(complete) => bind_verified_graph_to_search(graph, &complete.code_generation), - None => graph, - }; - let (graph, projection, verified_graph_evidence, graph_stage) = match (graph, complete.as_ref()) - { - (Ok(graph), Some(complete)) => match hotpath::measure_block!( - "mcp.graph.context.graph", - context_graph_projection( - ctx, - &graph, - complete, - scope_prefix, - max_nodes, - include_code, - max_code_blocks, - ) - ) { - Ok(projection) => { - let stage = ContextStageV1::ran(max_nodes, projection.selected.len(), false); - (Some(graph), projection, None, stage) - } - Err(error) => ( - None, - ContextGraphProjection::default(), - Some(dependency_hints::unavailable_evidence(&error)), - ContextStageV1::Unavailable, - ), - }, - (Ok(graph), None) => ( - Some(graph), - ContextGraphProjection::default(), - None, - ContextStageV1::Skipped, - ), - (Err(error), _) => ( - None, - ContextGraphProjection::default(), - Some(dependency_hints::unavailable_evidence(&error)), - ContextStageV1::Unavailable, - ), - }; - let retrieval = ContextRetrievalPlanV1 { - search: match complete.as_ref() { - Some(complete) => ContextStageV1::ran( - max_nodes, - search_matches.len(), - complete.next_cursor.is_some(), - ), - None => ContextStageV1::Unavailable, - }, - graph: graph_stage, - related: if projection.selected.is_empty() { - ContextStageV1::Skipped - } else { - ContextStageV1::ran( - max_nodes, - projection.related.len(), - projection.related_omission.is_some(), - ) - }, - code: if !include_code { - ContextStageV1::NotRequested - } else if projection.selected.is_empty() { - ContextStageV1::Skipped - } else { - ContextStageV1::ran( - max_code_blocks, - projection.code_blocks.len(), - projection.selected.len() > max_code_blocks, - ) - }, - memory: ContextStageV1::NotRequested, - }; - let ContextMemoryOutcome { - hits: memory_matches, - graph_coverage: memory_graph_coverage, - error: memory_matches_error, - } = memory_outcome; - let symbols = projection - .selected - .iter() - .map(primitive_symbol_location) - .collect::>>()?; - let related_symbols = projection - .related - .iter() - .map(primitive_symbol_location) - .collect::>>()?; - let plan = match (mode, graph.as_ref()) { - (ContextModeV1::Plan, Some(graph)) => { - Some(verified_plan_context(graph, &projection.selected)?) - } - _ => None, - }; - let touched_files = unique_file_paths( - projection.touched_files.iter().map(String::as_str).chain( - search_matches - .iter() - .map(|search_match| search_match.file.as_str()), - ), - ); - let analytics = InvocationAnalyticsV1 { - context_memory: Some(context_memory_analytics( - &memory_options, - &memory_matches, - memory_matches_error.as_deref(), - )), - pr_context: None, - }; - let retrieval = ContextRetrievalPlanV1 { - memory: context_memory_stage( - &memory_options, - &memory_matches, - memory_matches_error.as_deref(), - ), - ..retrieval - }; - let result = ContextResultV1 { - task: request.task, - mode, - freshness, - code_generation, - search_matches, - lexical_anchors, - symbols, - related_symbols, - related_omission: projection.related_omission, - code: projection.code_blocks, - coverage, - memory_matches, - memory_graph_coverage, - memory_matches_error, - verified_graph_evidence, - plan, - retrieval, - }; - Ok(GraphToolCompletionV1 { - result: GraphToolResultV1::Context(Box::new(result)), - touched_files, - code_graph: None, - analytics: Some(analytics), - cost: None, - }) -} - -/// Bare-name lookup against `idx_nodes_name`, no BM25 scoring, no fuzzy -/// match, no qualified-name suffix walk. Returns every node whose `name` -/// column equals the query exactly. Useful when you already know the symbol -/// and want the apples-to-apples cost of an index hit instead of -/// `tracedecay_search`'s ranked query. -#[hotpath::measure(label = "mcp.graph.find_exact_symbol.total")] -pub async fn compute_find_exact_symbol( - ctx: &McpToolContext<'_>, - graph: &tracedecay_graph_query::VerifiedGraphQuery, - args: Value, - scope_prefix: Option<&str>, - ignored_dependency_admission: Option< - &dyn tracedecay_application::code_index::CodeIndexIgnoredDependencyAdmissionPortV1, - >, -) -> Result { - let request: FindExactSymbolSurfaceRequestV1 = - decode_primitive_request(&args, "tracedecay_find_exact_symbol")?; - let name = request.name.as_str(); - let limit = request.limit.map_or(20, |v| v.min(200) as usize); - - let mut nodes = hotpath::measure_block!("mcp.graph.find_exact_symbol.graph", { - let nodes = graph.resolve_simple_name(name, None, limit.saturating_mul(4))?; - graph_symbols_in_scope(nodes, scope_prefix)? - }); - if nodes.is_empty() && request.lazy_index_ignored_dependencies.unwrap_or(false) { - hotpath::future!( - dependency_hints::admit_verified_ignored_dependency( - ctx, - ignored_dependency_admission, - graph, - name, - scope_prefix, - ), - label = "mcp.graph.find_exact_symbol.admit" - ) - .await?; - } - if nodes.len() > limit { - nodes.truncate(limit); - } - - let touched_files = graph_symbol_paths(&nodes)?; - let matches = nodes - .iter() - .map(|node| { - let metadata = required_graph_metadata(node)?; - let file_path = required_graph_file_path(node)?; - Ok(FindExactSymbolMatchV1 { - id: node.occurrence.as_str().to_owned(), - name: metadata.simple_name.clone(), - qualified_name: metadata.qualified_name.clone(), - kind: metadata.kind.clone(), - file: file_path.to_owned(), - line: user_line(metadata.start_line), - signature: metadata.signature.clone(), - }) - }) - .collect::>>()?; - Ok(graph_tool_completion( - GraphToolResultV1::FindExactSymbol(FindExactSymbolResultV1 { - name: request.name.clone(), - count: matches.len() as u64, - matches, - }), - touched_files, - )) -} - -#[hotpath::measure(label = "mcp.graph.similar.total")] -pub async fn compute_similar( - ctx: &McpToolContext<'_>, - args: Value, -) -> Result { - let request: SimilarSurfaceRequestV1 = decode_primitive_request(&args, "tracedecay_similar")?; - let project_id = request.project_id; - let repository_id = request.repository_id; - let target = match request.target { - SimilarTargetV1::SymbolOccurrence { - symbol_occurrence_id, - } => tracedecay_query::code_search::CodeIndexSimilarTargetV1::SymbolOccurrence( - symbol_occurrence_id, - ), - SimilarTargetV1::SourceRange { path, span } => { - tracedecay_query::code_search::CodeIndexSimilarTargetV1::SourceRange { path, span } - } - }; - let match_classes = request - .match_classes - .iter() - .map(|class| match class { - SimilarMatchClassV1::ConservativeExact => { - tracedecay_code_index::clones::CloneNormalizationClassV1::Conservative - } - SimilarMatchClassV1::RenameNormalizedExact => { - tracedecay_code_index::clones::CloneNormalizationClassV1::Rename - } - }) - .collect(); - let cursor = request - .cursor - .as_deref() - .map(tracedecay_query::retrieval::lexical::CloneArtifactCursorV1::decode) - .transpose() - .map_err(|error| TraceDecayError::Config { - message: format!("invalid tracedecay_similar cursor: {error}"), - })?; - let executor = ctx.code_index_similar_executor().ok_or_else(|| { - clone_lane_unavailable_error( - "similarity", - tracedecay_query::code_search::CodeIndexSearchUnavailableReasonV1::CapabilityUnavailable, - ) - })?; - let similar = match executor(tracedecay_query::code_search::CodeIndexSimilarRequestV1 { - project_root: ctx.project_root().to_path_buf(), - target, - match_classes, - result_limit: request.result_limit as usize, - work_limit: request.work_limit as usize, - cursor, - authority: ctx.code_index_search_authority().cloned(), - deadline: ctx.deadline().cloned(), - cancellation: ctx.cancellation().cloned(), - }) - .await - { - tracedecay_query::code_search::CodeIndexSimilarOutcomeV1::Complete(similar) => *similar, - tracedecay_query::code_search::CodeIndexSimilarOutcomeV1::NotFound => { - return Err(TraceDecayError::ProjectRoute { - reason_code: "similar-source-not-found".to_owned(), - retryable: false, - detail: "the selected source has no body in the verified clone index".to_owned(), - }); - } - tracedecay_query::code_search::CodeIndexSimilarOutcomeV1::Unavailable(reason) => { - return Err(clone_lane_unavailable_error("similarity", reason)); - } - }; - if similar.source.occurrence.project_id != project_id - || similar.source.occurrence.repository_id != repository_id - { - return Err(TraceDecayError::ProjectRoute { - reason_code: "similar-source-not-found".to_owned(), - retryable: false, - detail: "the selected source is outside the authorized repository scope".to_owned(), - }); - } - let source = similar_occurrence(&similar.source.occurrence); - let mut touched_files = vec![source.path.clone()]; - let mut complete = true; - let families = similar - .exact_groups - .into_iter() - .map(|group| -> Result { - complete &= group.complete; - let match_class = match group.key.class { - tracedecay_code_index::clones::CloneNormalizationClassV1::Conservative => { - SimilarMatchClassV1::ConservativeExact - } - tracedecay_code_index::clones::CloneNormalizationClassV1::Rename => { - SimilarMatchClassV1::RenameNormalizedExact - } - }; - let members = group - .members - .into_iter() - .filter(|member| { - member.occurrence.project_id == project_id - && member.occurrence.repository_id == repository_id - }) - .map(|member| { - let occurrence = similar_occurrence(&member.occurrence); - touched_files.push(occurrence.path.clone()); - occurrence - }) - .collect::>(); - let next_cursor = group - .next_cursor - .as_ref() - .map(tracedecay_query::retrieval::lexical::CloneArtifactCursorV1::encode) - .transpose() - .map_err(|error| TraceDecayError::Config { - message: format!("failed to encode tracedecay_similar cursor: {error}"), - })?; - Ok(SimilarFamilyV1 { - match_class, - normalization_revision: group.key.normalization_revision, - family_digest: group.key.digest, - representative_payload_digest: similar.source.payload.payload_digest.clone(), - member_count: members.len(), - members, - complete: group.complete, - next_cursor, - }) - }) - .collect::>>()?; - touched_files.sort(); - touched_files.dedup(); - let coverage = match similar.source.occurrence.eligibility { - tracedecay_code_index::clones::CloneBodyEligibilityV1::Eligible if complete => { - SimilarCoverageV1::Complete - } - tracedecay_code_index::clones::CloneBodyEligibilityV1::Eligible => { - SimilarCoverageV1::Partial - } - tracedecay_code_index::clones::CloneBodyEligibilityV1::ExcludedTooSmall { - minimum_tokens, - } => SimilarCoverageV1::ExcludedTooSmall { minimum_tokens }, - tracedecay_code_index::clones::CloneBodyEligibilityV1::ExcludedTooLarge { - maximum_tokens, - maximum_bytes, - } => SimilarCoverageV1::ExcludedTooLarge { - maximum_tokens, - maximum_bytes, - }, - tracedecay_code_index::clones::CloneBodyEligibilityV1::ExcludedIncompleteTokenization => { - SimilarCoverageV1::ExcludedIncompleteTokenization - } - }; - let result = SimilarResultV1 { - source_generation: source.source_generation.clone(), - source, - families, - coverage, - }; - Ok(graph_tool_completion( - GraphToolResultV1::Similar(result), - touched_files, - )) -} - -/// The one clone-family unavailable wire shape. `tracedecay_similar` and -/// `tracedecay_redundancy` read the same executor vocabulary, so they report -/// the same `reason_code` and the same retry verdict; only the human lane name -/// in `detail` differs. -/// -/// Deliberately does **not** re-emit retired opaque tokens -/// (`verified-code-similarity-unavailable` / -/// `verified-code-redundancy-unavailable`). Those never shipped on master; the -/// shared `reason.as_str()` vocabulary is the sole consumer-visible code. -fn clone_lane_unavailable_error( - lane: &str, - reason: tracedecay_query::code_search::CodeIndexSearchUnavailableReasonV1, -) -> TraceDecayError { - TraceDecayError::ProjectRoute { - reason_code: reason.as_str().to_owned(), - retryable: reason.is_retryable(), - detail: format!( - "the maintained clone {lane} lane is unavailable: {}", - reason.as_str() - ), - } -} - -#[hotpath::measure(label = "mcp.graph.redundancy.total")] -pub async fn compute_redundancy( - ctx: &McpToolContext<'_>, - args: Value, -) -> Result { - let request: RedundancySurfaceRequestV1 = - decode_primitive_request(&args, "tracedecay_redundancy")?; - if request.project_id != ctx.admitted_scope().project_id - || request.repository_id != ctx.admitted_scope().repository_id - { - return Err(TraceDecayError::ProjectRoute { - reason_code: "redundancy-repository-not-authorized".to_owned(), - retryable: false, - detail: "the selected repository is outside the authorized repository scope".to_owned(), - }); - } - let match_classes = request - .match_classes - .iter() - .map(|class| match class { - SimilarMatchClassV1::ConservativeExact => { - tracedecay_code_index::clones::CloneNormalizationClassV1::Conservative - } - SimilarMatchClassV1::RenameNormalizedExact => { - tracedecay_code_index::clones::CloneNormalizationClassV1::Rename - } - }) - .collect(); - let scope = match request.scope { - RedundancyScopeV1::Repository => { - tracedecay_query::code_search::CodeIndexRedundancyScopeV1::Repository - } - RedundancyScopeV1::Path { path } => { - tracedecay_query::code_search::CodeIndexRedundancyScopeV1::Path(path) - } - RedundancyScopeV1::PullRequest { - provider, - pull_request_id, - head_commit_id, - mut changed_paths, - } => { - changed_paths.sort(); - changed_paths.dedup(); - let pull_request_id = tracedecay_domain::feedback::GitHubPullRequestIdV1::new( - pull_request_id, - ) - .map_err(|error| TraceDecayError::Config { - message: format!("invalid arguments for tracedecay_redundancy: {error}"), - })?; - tracedecay_query::code_search::CodeIndexRedundancyScopeV1::PullRequest { - provider, - pull_request_id, - head_commit_id, - changed_paths, - } - } - }; - let executor = ctx.code_index_redundancy_executor().ok_or_else(|| { - clone_lane_unavailable_error( - "family", - tracedecay_query::code_search::CodeIndexSearchUnavailableReasonV1::CapabilityUnavailable, - ) - })?; - let outcome = executor(tracedecay_query::code_search::CodeIndexRedundancyQueryV1 { - project_root: ctx.project_root().to_path_buf(), - project_id: request.project_id, - repository_id: request.repository_id, - match_classes, - scope, - include_generated_paths: request.include_generated_paths, - family_limit: request.family_limit as usize, - member_limit: request.member_limit as usize, - work_limit: request.work_limit as usize, - cursor: request.cursor, - authority: ctx.code_index_search_authority().cloned(), - deadline: ctx.deadline().cloned(), - cancellation: ctx.cancellation().cloned(), - }) - .await - .map_err(|reason| clone_lane_unavailable_error("family", reason))?; - let mut touched_files = outcome - .families - .iter() - .flat_map(|group| group.family.members.iter()) - .map(|member| member.path.clone()) - .collect::>(); - touched_files.sort(); - touched_files.dedup(); - Ok(graph_tool_completion( - GraphToolResultV1::Redundancy(outcome), - touched_files, - )) -} - -fn similar_occurrence( - occurrence: &tracedecay_code_index::clones::CloneBodyOccurrenceV1, -) -> SimilarOccurrenceV1 { - SimilarOccurrenceV1 { - project_id: occurrence.project_id.clone(), - repository_id: occurrence.repository_id.clone(), - worktree_id: occurrence.worktree_id.clone(), - source_generation: occurrence.source_generation.clone(), - snapshot_digest: occurrence.snapshot_digest.clone(), - symbol_occurrence_id: occurrence.symbol_occurrence_id.clone(), - path: occurrence.path.clone(), - body_span: occurrence.body_span, - } -} - -/// Reads a file's lines (0-based) for snippet extraction, memoizing by path so -/// a file with many references is read once. `None` when the file cannot be -/// read (e.g. deleted since indexing). -fn cached_file_lines<'a>( - project_root: &Path, - cache: &'a mut HashMap>>, - file_path: &str, -) -> Option<&'a [String]> { - if !cache.contains_key(file_path) { - let abs = project_root.join(file_path); - let lines = std::fs::read_to_string(&abs) - .ok() - .map(|source| source.lines().map(str::to_string).collect::>()); - cache.insert(file_path.to_string(), lines); - } - cache - .get(file_path) - .and_then(Option::as_ref) - .map(Vec::as_slice) -} - -/// Trims and length-caps a source line for use as a preview snippet. -fn snippet_text(line: &str) -> String { - tracedecay_runtime_core::text::utf8_prefix_at_or_before(line.trim(), 160).to_string() -} - -/// Picks a current-text snippet near `approx_line` (0-based; edge line bases are -/// approximate, so neighbors are tried) that actually contains `name`, falling -/// back to the line itself. `None` when no line is available. -fn reference_line_snippet( - lines: &[String], - approx_line: Option, - name: &str, -) -> Option { - let approx = approx_line? as usize; - let candidates = [approx, approx.saturating_sub(1), approx + 1]; - let idx = candidates - .into_iter() - .find(|&i| lines.get(i).is_some_and(|line| line.contains(name))) - .unwrap_or(approx); - lines.get(idx).map(|line| snippet_text(line)) -} - -/// True for bytes that can appear inside an identifier. Non-ASCII bytes count so -/// multi-byte unicode identifiers are not falsely split at a boundary. -fn is_ident_byte(b: u8) -> bool { - b == b'_' || b.is_ascii_alphanumeric() || b >= 0x80 -} - -/// Counts occurrences of `name` in `haystack` bounded as a whole identifier -/// (neither neighbouring byte is an identifier byte). Used to estimate the -/// literal textual matches a rename would touch, independent of the graph. -fn count_identifier_occurrences(haystack: &str, name: &str) -> usize { - if name.is_empty() { - return 0; - } - let bytes = haystack.as_bytes(); - let name_len = name.len(); - let mut count = 0; - let mut start = 0; - while let Some(pos) = haystack[start..].find(name) { - let abs = start + pos; - let before_ok = abs == 0 || !is_ident_byte(bytes[abs - 1]); - let after_idx = abs + name_len; - let after_ok = after_idx >= bytes.len() || !is_ident_byte(bytes[after_idx]); - if before_ok && after_ok { - count += 1; - } - start = abs + name_len; - } - count -} - -/// Graph-derived inputs for one rename-preview reference site, extracted -/// before the blocking file walk so the worker needs no graph access. -struct RenameReferenceSiteInput { - from_node_id: String, - from_name: String, - from_kind: String, - edge_kind: String, - file: String, - evidence_start_byte: u64, -} - -/// READ-ONLY: reports what a rename of the given symbol WOULD touch, the -/// declaration site and every graph reference site (incoming edges; outgoing -/// edges reference other symbols and so are excluded), each with a -/// current-text snippet, plus a per-file count of literal name occurrences -/// that are NOT backed by a graph edge ("text-only matches, review -/// manually"). Nothing is rewritten. -#[hotpath::measure(label = "mcp.graph.rename_preview.total")] -pub async fn compute_rename_preview( - ctx: &McpToolContext<'_>, - graph: &tracedecay_graph_query::VerifiedGraphQuery, - args: Value, -) -> Result { - let request: RenamePreviewPrimitiveRequestV1 = - decode_primitive_request(&args, "tracedecay_rename_preview")?; - - let occurrence = graph_occurrence_id(&request.node_id)?; - // Graph occurrences per file (declaration + reference sites), subtracted - // from the literal textual count to isolate the text-only matches. - let mut graph_counts: HashMap = HashMap::new(); - let mut touched: Vec = Vec::new(); - // Graph phase: extract owned declaration fields and per-reference-site - // inputs so the blocking file walk below needs no graph value at all. - let (mut declaration, declaration_line, symbol_name, reference_inputs) = - hotpath::measure_block!("mcp.graph.rename_preview.graph", { - let Some(node) = graph.symbol_summary(&occurrence)? else { - return Ok(graph_tool_completion( - GraphToolResultV1::RenamePreview(RenamePreviewPrimitiveOutcomeV1::NotFound( - node_not_found_result(&request.node_id), - )), - Vec::new(), - )); - }; - let node_metadata = required_graph_metadata(&node)?; - let node_file = required_graph_file_path(&node)?; - let symbol_name = node_metadata.simple_name.clone(); - - touched.push(node_file.to_owned()); - *graph_counts.entry(node_file.to_owned()).or_default() += 1; - let declaration = RenamePreviewNodeV1 { - id: node.occurrence.as_str().to_owned(), - name: node_metadata.simple_name.clone(), - qualified_name: node_metadata.qualified_name.clone(), - kind: node_metadata.kind.clone(), - file: node_file.to_owned(), - line: user_line(node_metadata.start_line), - snippet: None, - }; - - // Reference sites: incoming edges are the callers/users that name this - // symbol. NOTE: call-edge coverage improves as the resolver improves; - // the text-only counts below catch what the graph currently misses. - let incoming = single_graph_adjacency_batch(graph.callers( - std::slice::from_ref(&node.occurrence), - &[], - 2_000_000, - )?)?; - let mut reference_inputs = - Vec::::with_capacity(incoming.len()); - for edge in incoming { - let source_node = edge.neighbor; - let source_metadata = required_graph_metadata(&source_node)?; - let source_file = required_graph_file_path(&source_node)?; - touched.push(source_file.to_owned()); - *graph_counts.entry(source_file.to_owned()).or_default() += 1; - reference_inputs.push(RenameReferenceSiteInput { - from_node_id: source_node.occurrence.as_str().to_owned(), - from_name: source_metadata.simple_name.clone(), - from_kind: source_metadata.kind.clone(), - edge_kind: edge.edge.kind.as_str().to_owned(), - file: source_file.to_owned(), - evidence_start_byte: edge.edge.evidence_span.start_byte, - }); - } - ( - declaration, - node_metadata.start_line, - symbol_name, - reference_inputs, - ) - }); - - let touched_files = unique_file_paths(touched.iter().map(std::string::String::as_str)); - - // File-walk phase: every referenced source file is read from disk, so it - // runs on a blocking worker like the sibling analysis scans instead of - // holding the async dispatch thread through the reads. - let project_root = ctx.project_root().to_path_buf(); - let declaration_file = declaration.file.clone(); - let walk_symbol_name = symbol_name.clone(); - let walk_graph_counts = graph_counts; - let walk_touched_files = touched_files.clone(); - let (decl_snippet, references, text_only_matches) = hotpath::future!( - tokio::task::spawn_blocking( - move || -> Result<( - Option, - Vec, - Vec, - )> { - let mut lines_cache: HashMap>> = HashMap::new(); - let decl_snippet = - cached_file_lines(&project_root, &mut lines_cache, &declaration_file).and_then( - |lines| { - lines - .get(declaration_line as usize) - .map(|line| snippet_text(line)) - }, - ); - - let mut references = - Vec::::with_capacity(reference_inputs.len()); - for input in reference_inputs { - let source = tracedecay_runtime_core::sync::read_source_file(&project_root.join(&input.file))?; - let line = line_for_byte_offset(&source, input.evidence_start_byte)?; - let snippet = cached_file_lines(&project_root, &mut lines_cache, &input.file) - .and_then(|lines| reference_line_snippet(lines, Some(line), &walk_symbol_name)); - references.push(RenamePreviewReferenceV1 { - from_node_id: input.from_node_id, - from_name: input.from_name, - from_kind: input.from_kind, - edge_kind: input.edge_kind, - file: input.file, - line: user_line(line), - snippet, - }); - } - - // Text-only matches per touched file: literal identifier occurrences - // of the name minus the graph occurrences already accounted for. - // These are the comments/strings/dynamic-dispatch/unresolved sites a - // graph-only rename would miss, the scan is bounded to files that - // already appear in the preview, so occurrences in wholly unrelated - // files are not counted. - let mut text_only_matches = Vec::::new(); - for file in &walk_touched_files { - let total = - cached_file_lines(&project_root, &mut lines_cache, file).map_or(0, |lines| { - lines - .iter() - .map(|line| count_identifier_occurrences(line, &walk_symbol_name)) - .sum::() - }); - let graph = walk_graph_counts.get(file).copied().unwrap_or(0); - let text_only = total.saturating_sub(graph); - if text_only > 0 { - text_only_matches.push(RenamePreviewTextOnlyMatchV1 { - file: file.clone(), - text_only_count: text_only, - note: "text-only matches, review manually".to_owned(), - }); - } - } - Ok((decl_snippet, references, text_only_matches)) - } - ), - label = "mcp.graph.rename_preview.walk" - ) - .await - .map_err(|join_error| TraceDecayError::Config { - message: format!("rename preview file scan task failed: {join_error}"), - })??; - declaration.snippet = decl_snippet; - - let result = RenamePreviewPrimitiveResultV1 { - read_only: true, - note: "Preview only. Nothing is edited. 'references' are graph reference sites \ - (the declaration is reported separately in 'node'); 'text_only_matches' are \ - literal name occurrences NOT backed by a graph edge (comments, strings, \ - dynamic dispatch, unresolved refs) and must be reviewed by hand. Graph \ - call-edge coverage improves as the resolver does." - .to_owned(), - symbol: symbol_name, - new_name: request.new_name, - node: declaration, - reference_count: references.len(), - references, - text_only_matches, - }; - Ok(graph_tool_completion( - GraphToolResultV1::RenamePreview(RenamePreviewPrimitiveOutcomeV1::Preview(result)), - touched_files, - )) -} - #[cfg(test)] mod tests { - use super::*; - use tracedecay_contracts::memory::FactSearchHitV1; - - #[test] - fn clone_lanes_report_one_unavailable_wire_protocol() { - use tracedecay_query::code_search::CodeIndexSearchUnavailableReasonV1 as Reason; + use serde_json::json; + use tracedecay_domain::errors::TraceDecayError; + use tracedecay_query::retrieval::lexical::LexicalRoutingV1; - for (reason, code, retryable) in [ - ( - Reason::CapabilityUnavailable, - "code_index_unavailable", - false, - ), - (Reason::AuthorityUnavailable, "authority_unavailable", false), - ( - Reason::LinkedWorktreeDisabled, - "linked_worktree_disabled", - false, - ), - (Reason::Cancelled, "cancelled", true), - (Reason::TimedOut, "timed_out", true), - ( - Reason::CapacityUnavailable, - "search_capacity_unavailable", - true, - ), - ( - Reason::GenerationUnavailable, - "generation_unavailable", - true, - ), - (Reason::GenerationUnverified, "generation_unverified", true), - (Reason::InvalidRequest, "invalid_request", false), - ( - Reason::CorruptionResetRequired, - "index_corruption_reset_required", - false, - ), - (Reason::Internal, "search_failed", false), - ] { - for lane in ["similarity", "family"] { - let error = clone_lane_unavailable_error(lane, reason); - assert_eq!( - error - .project_route_context() - .expect("clone lane failures are typed project-route errors"), - ( - code, - retryable, - format!("the maintained clone {lane} lane is unavailable: {code}").as_str(), - ) - ); - } - } - } + use super::*; #[test] fn complete_search_preserves_generation_advance_but_not_stale_admission() { @@ -1729,310 +529,23 @@ mod tests { #[test] fn schema_anchor_bound_matches_the_retrieval_kernel_bound() { assert_eq!( - tracedecay_mcp_catalog::SEARCH_MAX_LEXICAL_ANCHORS, + tracedecay_contracts::retrieval::SEARCH_MAX_LEXICAL_ANCHORS, tracedecay_query::retrieval::lexical::MAX_LEXICAL_ANCHORS_V1 ); assert_eq!( - tracedecay_mcp_catalog::SEARCH_MAX_LEXICAL_ANCHOR_BYTES, + tracedecay_contracts::retrieval::SEARCH_MAX_LEXICAL_ANCHOR_BYTES, tracedecay_query::retrieval::lexical::MAX_LEXICAL_ANCHOR_BYTES_V1 ); } - #[tokio::test] - async fn similar_unavailable_wire_preserves_reason_and_retryability() { - let temp = tempfile::tempdir().expect("temp root"); - let admitted = crate::tool_context::tests::scope("similar-unavailable"); - let project = crate::tool_context::tests::project_bundle(temp.path(), &admitted, None); - let authority = tracedecay_query::code_search::CodeIndexSearchAuthorityV1 { - principal: tracedecay_domain::PrincipalId::new("principal.similar-unavailable") - .expect("principal"), - authorization_revision: tracedecay_domain::AuthorizationRevision::new( - "revision.similar-unavailable", - ) - .expect("revision"), - }; - - for (reason, reason_code, retryable) in [ - ( - tracedecay_query::code_search::CodeIndexSearchUnavailableReasonV1::GenerationUnavailable, - "generation_unavailable", - true, - ), - ( - tracedecay_query::code_search::CodeIndexSearchUnavailableReasonV1::CorruptionResetRequired, - "index_corruption_reset_required", - false, - ), - ] { - let executor: tracedecay_query::code_search::CodeIndexSimilarExecutor = - std::sync::Arc::new(move |_| { - Box::pin(async move { - tracedecay_query::code_search::CodeIndexSimilarOutcomeV1::Unavailable(reason) - }) - }); - let code_index = - crate::AdmittedCodeIndex::new(&authority, None, Some(&executor), None, None) - .expect("similar executor admits"); - let ctx = crate::McpToolContext::bind(crate::McpToolBinding { - project: &project, - request: crate::McpRequestAuthoritiesV1 { - code_index: Some(code_index), - ..crate::McpRequestAuthoritiesV1::default() - }, - }) - .expect("admitted similar binding"); - let result = compute_similar( - &ctx, - json!({ - "project_id": admitted.project_id, - "repository_id": admitted.repository_id, - "target": { - "kind": "symbol_occurrence", - "symbol_occurrence_id": "symbol.similar-unavailable", - }, - "match_classes": ["conservative_exact"], - "result_limit": 10, - "work_limit": 20, - }), - ) - .await; - let Err(error) = result else { - panic!("unavailable similar executor must remain a transport failure"); - }; - let response = - crate::tool_error_response(json!(1), "tracedecay_similar", &error); - let wire: Value = serde_json::from_str(&crate::serialize_response_line(&response)) - .expect("JSON-RPC response"); - - assert_eq!(wire["error"]["data"]["reason_code"], reason_code); - assert_eq!(wire["error"]["data"]["retryable"], retryable); - } - } - - #[tokio::test] - async fn redundancy_unavailable_wire_preserves_reason_and_retryability() { - let temp = tempfile::tempdir().expect("temp root"); - let admitted = crate::tool_context::tests::scope("redundancy-unavailable"); - let project = crate::tool_context::tests::project_bundle(temp.path(), &admitted, None); - let authority = tracedecay_query::code_search::CodeIndexSearchAuthorityV1 { - principal: tracedecay_domain::PrincipalId::new("principal.redundancy-unavailable") - .expect("principal"), - authorization_revision: tracedecay_domain::AuthorizationRevision::new( - "revision.redundancy-unavailable", - ) - .expect("revision"), - }; - - for (reason, reason_code, retryable) in [ - ( - tracedecay_query::code_search::CodeIndexSearchUnavailableReasonV1::GenerationUnavailable, - "generation_unavailable", - true, - ), - ( - tracedecay_query::code_search::CodeIndexSearchUnavailableReasonV1::CorruptionResetRequired, - "index_corruption_reset_required", - false, - ), - ] { - let executor: tracedecay_query::code_search::CodeIndexRedundancyExecutor = - std::sync::Arc::new(move |_| { - Box::pin(async move { Err(reason) }) - }); - let code_index = - crate::AdmittedCodeIndex::new(&authority, None, None, Some(&executor), None) - .expect("redundancy executor admits"); - let ctx = crate::McpToolContext::bind(crate::McpToolBinding { - project: &project, - request: crate::McpRequestAuthoritiesV1 { - code_index: Some(code_index), - ..crate::McpRequestAuthoritiesV1::default() - }, - }) - .expect("admitted redundancy binding"); - let result = compute_redundancy( - &ctx, - json!({ - "project_id": admitted.project_id, - "repository_id": admitted.repository_id, - "match_classes": ["conservative_exact"], - "scope": {"kind": "repository"}, - "include_generated_paths": false, - "family_limit": 10, - "member_limit": 10, - "work_limit": 20, - }), - ) - .await; - let Err(error) = result else { - panic!("unavailable redundancy executor must remain a transport failure"); - }; - let response = - crate::tool_error_response(json!(1), "tracedecay_redundancy", &error); - let wire: Value = serde_json::from_str(&crate::serialize_response_line(&response)) - .expect("JSON-RPC response"); - - assert_eq!(wire["error"]["data"]["reason_code"], reason_code); - assert_eq!(wire["error"]["data"]["retryable"], retryable); - assert_ne!( - wire["error"]["data"]["reason_code"], - "verified-code-redundancy-unavailable" - ); - } - } - - #[tokio::test] - async fn missing_clone_lane_executors_emit_shared_capability_unavailable() { - let temp = tempfile::tempdir().expect("temp root"); - let admitted = crate::tool_context::tests::scope("clone-missing-executor"); - let project = crate::tool_context::tests::project_bundle(temp.path(), &admitted, None); - let authority = tracedecay_query::code_search::CodeIndexSearchAuthorityV1 { - principal: tracedecay_domain::PrincipalId::new("principal.clone-missing-executor") - .expect("principal"), - authorization_revision: tracedecay_domain::AuthorizationRevision::new( - "revision.clone-missing-executor", - ) - .expect("revision"), - }; - let shared_code = tracedecay_query::code_search::CodeIndexSearchUnavailableReasonV1::CapabilityUnavailable - .as_str(); - - // Admit the sibling lane so the request is authorized, then omit the - // lane under test, the Codex P2 gap (opaque missing-executor tokens). - let similar_stub: tracedecay_query::code_search::CodeIndexSimilarExecutor = - std::sync::Arc::new(|_| { - Box::pin(async { - tracedecay_query::code_search::CodeIndexSimilarOutcomeV1::NotFound - }) - }); - let redundancy_only = - crate::AdmittedCodeIndex::new(&authority, None, Some(&similar_stub), None, None) - .expect("similar executor admits without redundancy"); - let redundancy_ctx = crate::McpToolContext::bind(crate::McpToolBinding { - project: &project, - request: crate::McpRequestAuthoritiesV1 { - code_index: Some(redundancy_only), - ..crate::McpRequestAuthoritiesV1::default() - }, - }) - .expect("admitted similar-only binding"); - let redundancy_err = compute_redundancy( - &redundancy_ctx, - json!({ - "project_id": admitted.project_id, - "repository_id": admitted.repository_id, - "match_classes": ["conservative_exact"], - "scope": {"kind": "repository"}, - "include_generated_paths": false, - "family_limit": 10, - "member_limit": 10, - "work_limit": 20, - }), - ) - .await - .expect_err("missing redundancy executor must be typed unavailable"); - let redundancy_wire: Value = serde_json::from_str(&crate::serialize_response_line( - &crate::tool_error_response(json!(1), "tracedecay_redundancy", &redundancy_err), - )) - .expect("JSON-RPC response"); - assert_eq!(redundancy_wire["error"]["data"]["reason_code"], shared_code); - assert_eq!(redundancy_wire["error"]["data"]["retryable"], false); - assert_ne!( - redundancy_wire["error"]["data"]["reason_code"], - "verified-code-redundancy-unavailable" - ); - - let redundancy_stub: tracedecay_query::code_search::CodeIndexRedundancyExecutor = - std::sync::Arc::new(|_| { - Box::pin(async { - Err(tracedecay_query::code_search::CodeIndexSearchUnavailableReasonV1::Internal) - }) - }); - let similar_only = - crate::AdmittedCodeIndex::new(&authority, None, None, Some(&redundancy_stub), None) - .expect("redundancy executor admits without similar"); - let similar_ctx = crate::McpToolContext::bind(crate::McpToolBinding { - project: &project, - request: crate::McpRequestAuthoritiesV1 { - code_index: Some(similar_only), - ..crate::McpRequestAuthoritiesV1::default() - }, - }) - .expect("admitted redundancy-only binding"); - let similar_err = compute_similar( - &similar_ctx, - json!({ - "project_id": admitted.project_id, - "repository_id": admitted.repository_id, - "target": { - "kind": "symbol_occurrence", - "symbol_occurrence_id": "symbol.clone-missing-executor", - }, - "match_classes": ["conservative_exact"], - "result_limit": 10, - "work_limit": 20, - }), - ) - .await - .expect_err("missing similar executor must be typed unavailable"); - let similar_wire: Value = serde_json::from_str(&crate::serialize_response_line( - &crate::tool_error_response(json!(1), "tracedecay_similar", &similar_err), - )) - .expect("JSON-RPC response"); - assert_eq!(similar_wire["error"]["data"]["reason_code"], shared_code); - assert_eq!(similar_wire["error"]["data"]["retryable"], false); - assert_ne!( - similar_wire["error"]["data"]["reason_code"], - "verified-code-similarity-unavailable" - ); - } - - fn context_memory_hit(content: &str) -> FactSearchHitV1 { - serde_json::from_value(json!({ - "fact": { - "owner": {"kind": "profile"}, - "fact_id": "fact.0000000000000000000000000000000000000000000000000000000000000000.1111111111111111111111111111111111111111111111111111111111111111", - "content": content, - "category": "project", - "tags": [], - "entities": [], - "trust_score_millionths": 900_000, - "source": {"kind": "application", "operation_id": "operation.context-memory"}, - "source_label": "context-test", - "active_assertion_id": "assertion.context-memory", - "last_event_id": "event.context-memory", - "projected_as_of": 1, - "telemetry": { - "retrieval_count": 0, - "access_count": 0, - "helpful_count": 0, - "unhelpful_count": 0, - "created_at": 1, - "updated_at": 1, - "last_retrieved_at": null, - "last_recalled_at": null, - "last_feedback_at": null - }, - "metadata": {} - }, - "scores": { - "score_millionths": 500_000, - "fts_score_millionths": 250_000, - "jaccard_score_millionths": 250_000, - "holographic_score_millionths": 0, - "trust_score_millionths": 900_000 - }, - "why": null - })) - .expect("canonical context memory hit") - } - /// A warm response must render exactly as it did before coverage existed: /// every lane complete, no coverage section, no added lines. #[test] fn warm_coverage_leaves_the_rendered_body_unchanged() { - let coverage = - coverage_value(&tracedecay_query::code_search::CodeIndexSearchCoverageV1::warm()); + let coverage = serde_json::to_value(search_coverage( + &tracedecay_query::code_search::CodeIndexSearchCoverageV1::warm(), + )) + .expect("coverage JSON"); assert_eq!(coverage["recall"], json!("full")); assert_eq!(coverage["exact"], json!("complete")); @@ -2142,7 +655,7 @@ mod tests { }) .expect("admitted search binding"); - handle_search( + compute_search( &ctx, async { Err(TraceDecayError::project_route( @@ -2156,7 +669,7 @@ mod tests { None, ) .await - .expect("search renders without a graph"); + .expect("search answers without a graph"); assert_eq!( *order.lock().expect("order"), @@ -2246,98 +759,4 @@ mod tests { ) )); } - - #[test] - fn context_markdown_lane_preview_keeps_all_lanes_visible() { - let full = format!( - "## Code Context\n**Query:** q\n\n### Memory Matches\n{}\n### Entry Points\n{}\n### Related Symbols\n{}\n### Code\n{}\n### Index Coverage Hint\n{}\n### Extension Points\n{}\n### Test Coverage\n{}\nseen_node_ids: [{}]\n", - "memory fact with unicode caf\u{e9}\n".repeat(300), - "- **entry** src/lib.rs:1\n".repeat(300), - "- related\n".repeat(500), - "```rust\nfn demo() {}\n```\n".repeat(500), - "hint\n".repeat(500), - "- trait\n".repeat(400), - "- tests/context_test.rs\n".repeat(400), - "\"node-id\",".repeat(400) - ); - - let preview = context_markdown_lane_preview(&full); - - for heading in [ - "## Code Context", - "### Memory Matches", - "### Entry Points", - "### Related Symbols", - "### Code", - "### Index Coverage Hint", - "### Extension Points", - "### Test Coverage", - "seen_node_ids:", - ] { - assert!(preview.contains(heading), "missing {heading}: {preview}"); - } - assert!(preview.len() < full.len()); - assert!(preview.contains("lane truncated")); - assert!(preview.is_char_boundary(preview.len())); - } - - #[test] - fn context_lane_preview_keeps_seen_node_ids_parseable() { - let ids: Vec = (0..100).map(|i| format!("function:{i:032x}")).collect(); - let markdown = format!( - "{} {}\n", - CONTEXT_SEEN_NODE_IDS_LABEL, - serde_json::to_string(&ids) - .unwrap_or_else(|err| panic!("failed to serialize seen node ids: {err}")) - ); - - let preview = context_markdown_lane_preview(&markdown); - let json = match preview.strip_prefix(CONTEXT_SEEN_NODE_IDS_LABEL) { - Some(json) => json.trim(), - None => panic!("preview should keep seen_node_ids label: {preview}"), - }; - let parsed: Vec = serde_json::from_str(json) - .unwrap_or_else(|err| panic!("failed to parse seen node ids: {err}: {json}")); - - assert_eq!(parsed, ids); - assert!(!preview.contains("lane truncated")); - } - - #[test] - fn context_memory_section_keeps_full_content_for_retrieval_handle() { - let content = format!("{}tail-marker", "long memory body ".repeat(100)); - let hit = context_memory_hit(&content); - - let Some(section) = context_memory_section(&[hit], None) else { - panic!("memory hit should render"); - }; - - assert!(section.contains(&content)); - assert!(section.contains("tail-marker")); - assert!(!section.contains("...")); - assert!(section.contains("tracedecay_fact_feedback")); - } - - #[test] - fn context_lane_preview_closes_open_code_fence_before_truncation_note() { - let markdown = format!("### Code\n```rust\n{}\n", "fn demo() {}\n".repeat(1_000)); - - let preview = context_markdown_lane_preview(&markdown); - - assert!(preview.contains("```\n\n... lane truncated")); - } - - #[test] - fn context_lane_preview_ignores_heading_markers_inside_code_fences() { - let markdown = format!( - "### Code\n```markdown\n{}\n```\n### Test Coverage\n- real lane\n", - "### not a lane\n".repeat(1_000) - ); - - let preview = context_markdown_lane_preview(&markdown); - - assert!(preview.contains("### Code")); - assert!(preview.contains("### Test Coverage")); - assert_eq!(preview.matches("lane truncated").count(), 1); - } } diff --git a/crates/tracedecay-mcp/src/handlers/graph/search_evidence.rs b/crates/tracedecay-mcp/src/handlers/graph/search_evidence.rs index 2bb27a9d32..d70c51d59c 100644 --- a/crates/tracedecay-mcp/src/handlers/graph/search_evidence.rs +++ b/crates/tracedecay-mcp/src/handlers/graph/search_evidence.rs @@ -3,6 +3,7 @@ use std::future::Future; use serde_json::Value; use crate::tools::render::Md; +use tracedecay_contracts::retrieval::{PrimitiveUnavailableEvidenceV1, SearchExternalImportHintV1}; use tracedecay_domain::errors::{Result, TraceDecayError}; use tracedecay_graph_query::VerifiedGraphQuery; use tracedecay_query::code_search::CodeIndexSearchDisplayV1; @@ -78,7 +79,7 @@ pub(super) fn bind_verified_graph_to_search( pub(super) struct SearchGraphEvidence<'a> { graph: std::result::Result<&'a VerifiedGraphQuery, &'a TraceDecayError>, - unavailable: Option, + unavailable: Option, } pub(super) fn append_verified_graph_evidence_md(md: &mut Md, value: &Value) { @@ -104,28 +105,27 @@ impl<'a> SearchGraphEvidence<'a> { let unavailable = graph .as_ref() .err() - .map(|error| dependency_hints::unavailable_hint(error)); + .map(|error| dependency_hints::unavailable_evidence(error)); Self { graph, unavailable } } - pub(super) fn enrich_node_id( - &mut self, - result: &mut Value, - display: &CodeIndexSearchDisplayV1, - ) { + /// The one graph symbol a search display names, when the graph resolves + /// it uniquely. A graph read failure is recorded as the response's + /// unavailable graph evidence. + pub(super) fn node_id_for(&mut self, display: &CodeIndexSearchDisplayV1) -> Option { let Ok(graph) = self.graph else { - return; + return None; }; match unique_graph_node_id_for_search_display(graph, display) { - Ok(Some(node_id)) => result["node_id"] = Value::String(node_id), - Ok(None) => {} + Ok(node_id) => node_id, Err(error) => { - self.unavailable = Some(dependency_hints::unavailable_hint(&error)); + self.unavailable = Some(dependency_hints::unavailable_evidence(&error)); + None } } } - pub(super) fn unavailable(&self) -> Option<&Value> { + pub(super) fn unavailable(&self) -> Option<&PrimitiveUnavailableEvidenceV1> { self.unavailable.as_ref() } @@ -136,16 +136,20 @@ impl<'a> SearchGraphEvidence<'a> { query: &str, limit: usize, scope_prefix: Option<&str>, - ) -> Option { + ) -> Option { match self.graph { Ok(graph) => { match dependency_hints::external_import_hint(ctx, graph, query, limit, scope_prefix) { - Ok(hint) => hint, - Err(error) => Some(dependency_hints::unavailable_hint(&error)), + Ok(hint) => hint.map(SearchExternalImportHintV1::Candidates), + Err(error) => Some(SearchExternalImportHintV1::Unavailable( + dependency_hints::unavailable_evidence(&error), + )), } } - Err(error) => Some(dependency_hints::unavailable_hint(error)), + Err(error) => Some(SearchExternalImportHintV1::Unavailable( + dependency_hints::unavailable_evidence(error), + )), } } } diff --git a/crates/tracedecay-mcp/src/handlers/graph_tool.rs b/crates/tracedecay-mcp/src/handlers/graph_tool.rs index 6aef1b0164..83ffb54635 100644 --- a/crates/tracedecay-mcp/src/handlers/graph_tool.rs +++ b/crates/tracedecay-mcp/src/handlers/graph_tool.rs @@ -25,8 +25,8 @@ use crate::handlers::git::{ }; use crate::handlers::graph::{ compute_by_qualified_name, compute_context, compute_derives, compute_find_exact_symbol, - compute_impact, compute_node, compute_redundancy, compute_rename_preview, compute_signature, - compute_similar, not_found_tool_result, render_context, + compute_impact, compute_node, compute_redundancy, compute_rename_preview, compute_search, + compute_signature, compute_similar, not_found_tool_result, render_context, render_search, }; use crate::handlers::grep::{compute_grep, render_grep}; use crate::handlers::health::{ @@ -180,6 +180,16 @@ pub async fn compute_graph_tool( compute_files(&open(operation).await?, request, scope_prefix).await } ApplicationSurfaceOperation::Config => compute_config(ctx.project_root(), args).await, + ApplicationSurfaceOperation::Search => { + compute_search( + ctx, + open(read("code_symbol_search")?), + args, + scope_prefix, + ignored_dependency_admission, + ) + .await + } ApplicationSurfaceOperation::Retrieve => { compute_retrieve(&ctx.store_layout().response_handle_root, &args).await } @@ -284,6 +294,7 @@ pub fn render_graph_tool( Vec::new(), || render_files_md(files), ), + GraphToolResultV1::Search(search) => render_search(response_handle_root, args, search)?, GraphToolResultV1::Retrieve(RetrieveResultV1::Page(page)) => { render_retrieved_page(args, page)? } diff --git a/crates/tracedecay-mcp/src/handlers/support.rs b/crates/tracedecay-mcp/src/handlers/support.rs index b61aa565aa..6fddeedfb5 100644 --- a/crates/tracedecay-mcp/src/handlers/support.rs +++ b/crates/tracedecay-mcp/src/handlers/support.rs @@ -17,7 +17,14 @@ use crate::tools::render; /// than treated as "start from the beginning", which would silently restart /// a page walk instead of reporting the tampered envelope. pub fn retrieval_cursor(args: &Value) -> Result> { - let Some(encoded) = args.get("cursor").and_then(Value::as_str) else { + decode_retrieval_cursor(args.get("cursor").and_then(Value::as_str)) +} + +/// Decodes and validates one encoded authenticated retrieval continuation. +pub fn decode_retrieval_cursor( + encoded: Option<&str>, +) -> Result> { + let Some(encoded) = encoded else { return Ok(None); }; if encoded.len() > 4_096 { diff --git a/crates/tracedecay-mcp/src/tools/binding.rs b/crates/tracedecay-mcp/src/tools/binding.rs index cb6d97e4fe..61ac342752 100644 --- a/crates/tracedecay-mcp/src/tools/binding.rs +++ b/crates/tracedecay-mcp/src/tools/binding.rs @@ -47,7 +47,6 @@ pub const INTERNAL_DAEMON_TOOL_NAMES: &[&str] = &[ pub enum McpToolDispatchGroup { ApplicationSurface, MultiRoot, - Graph, Info, Admin, Git, @@ -82,8 +81,7 @@ pub fn tool_branch_sensitivity(tool_name: &str) -> BranchSensitivity { } match dispatch_group_for_tool(tool_name) { Some( - McpToolDispatchGroup::Graph - | McpToolDispatchGroup::Info + McpToolDispatchGroup::Info | McpToolDispatchGroup::Admin | McpToolDispatchGroup::Git | McpToolDispatchGroup::Health @@ -297,6 +295,7 @@ fn application_surface_branch_sensitivity( | ApplicationSurfaceOperation::Files | ApplicationSurfaceOperation::Config | ApplicationSurfaceOperation::Retrieve + | ApplicationSurfaceOperation::Search | HealthRead | HealthDelta | DiagnosticsRead @@ -346,9 +345,6 @@ macro_rules! binding_groups { /// access are one slice; a different access starts a new slice. #[rustfmt::skip] const BINDING_GROUPS: &[BindingGroup] = binding_groups![ - [Some(McpToolDispatchGroup::Graph), RegisteredProjectAccess::ActiveProjectOnly, - "tracedecay_search", "tracedecay_context", "tracedecay_impact", - "tracedecay_node", "tracedecay_similar", "tracedecay_redundancy", "tracedecay_rename_preview"], [Some(McpToolDispatchGroup::Info), RegisteredProjectAccess::ActiveProjectOnly, "tracedecay_status", "tracedecay_remote_status", "tracedecay_active_project", "tracedecay_project_list", "tracedecay_project_search"], diff --git a/crates/tracedecay-mcp/src/tools/catalog_discovery.rs b/crates/tracedecay-mcp/src/tools/catalog_discovery.rs index fd4ff67345..dcf201bfe6 100644 --- a/crates/tracedecay-mcp/src/tools/catalog_discovery.rs +++ b/crates/tracedecay-mcp/src/tools/catalog_discovery.rs @@ -464,12 +464,13 @@ mod tests { assert!( definitions .iter() - .any(|definition| definition.name == "tracedecay_search"), + .any(|definition| definition.name == "tracedecay_dashboard"), "legacy production tools remain discoverable until cataloged" ); assert!( definitions.iter().all(|definition| { - definition.name != "tracedecay_context" + definition.name != "tracedecay_search" + && definition.name != "tracedecay_context" && definition.name != "tracedecay_git_preview" }), "catalog-bound tools require explicit capability authority" diff --git a/crates/tracedecay-tool-catalog/src/operation.rs b/crates/tracedecay-tool-catalog/src/operation.rs index bebdc30993..c586c31df6 100644 --- a/crates/tracedecay-tool-catalog/src/operation.rs +++ b/crates/tracedecay-tool-catalog/src/operation.rs @@ -216,6 +216,7 @@ application_surface_operations! { SkillView => "skill_view"; HermesSkillBridge => "hermes_skill_bridge"; Analytics => "analytics"; + Search => "search"; HealthRead => "health_read"; HealthDelta => "health_delta"; StorageStatus => "storage_status"; @@ -342,6 +343,7 @@ impl ApplicationSurfaceOperation { Self::SkillView, Self::HermesSkillBridge, Self::Analytics, + Self::Search, ]; pub fn is_graph_tool(self) -> bool { diff --git a/crates/tracedecay/src/mcp/server/cancel_candidate_journey.rs b/crates/tracedecay/src/mcp/server/cancel_candidate_journey.rs index ad78f7a2ae..5395ac840f 100644 --- a/crates/tracedecay/src/mcp/server/cancel_candidate_journey.rs +++ b/crates/tracedecay/src/mcp/server/cancel_candidate_journey.rs @@ -308,12 +308,16 @@ async fn open_search_server( ) .await .expect("mcp host graph"); - let server = McpServer::new_with_context( - McpServerConstructionContext::direct(cg, None) - .with_code_index_search_executor(executor) - .with_code_index_search_authority(authority), - ) - .await; + // Search is answered by the project's graph-tool owner, so the server + // mounts the daemon invocation service in process, as project open does. + let server = + crate::daemon::retained_test_support::mcp_server_with_project_retained_owner_for_test( + McpServerConstructionContext::direct(cg, None) + .with_code_index_search_executor(executor) + .with_code_index_search_authority(authority), + ) + .await + .expect("graph-tool owner MCP server"); HeldServer { server, _profile: profile, diff --git a/crates/tracedecay/src/mcp/tools/handlers/dispatch_groups.rs b/crates/tracedecay/src/mcp/tools/handlers/dispatch_groups.rs index be1ffa0226..43d87ab7a7 100644 --- a/crates/tracedecay/src/mcp/tools/handlers/dispatch_groups.rs +++ b/crates/tracedecay/src/mcp/tools/handlers/dispatch_groups.rs @@ -12,7 +12,6 @@ use tracedecay_contracts::code_index_freshness::{ CodeIndexFreshnessReader, CodeIndexReadinessWaitOutcomeV1, CodeIndexReadinessWaitV1, }; use tracedecay_dashboard_api::AdmittedDoctorReportV1; -use tracedecay_mcp::handlers::graph as portable_graph; use tracedecay_mcp::handlers::info as portable_info; use tracedecay_mcp::handlers::{ VerifiedGraphOpenFuture, unknown_tool_error, verified_read_operation, @@ -106,46 +105,6 @@ async fn admitted_graph_query_for_operation( Ok(query) } -/// Dispatch code-graph navigation and lookup tools (`tracedecay_search`, -/// `tracedecay_callers`, ...). Returns `None` when `tool_name` belongs to a -/// different domain so the caller can try the next dispatch group. -#[hotpath::measure(future = true, label = "mcp.dispatch.graph")] -pub(super) async fn dispatch_graph_tools( - tool_name: &str, - cg: &TraceDecay, - args: Value, - selected_scope_prefix: Option<&str>, - options: ToolCallRegistryOptions<'_>, -) -> Result { - dispatch_graph_tools_inner(tool_name, cg, args, selected_scope_prefix, options).await -} - -fn dispatch_graph_tools_inner<'a>( - tool_name: &'a str, - cg: &'a TraceDecay, - args: Value, - selected_scope_prefix: Option<&'a str>, - options: ToolCallRegistryOptions<'a>, -) -> std::pin::Pin> + Send + 'a>> { - // Erase the deeply nested match-arm futures before they reach the - // measured wrapper so every profiling feature can compute its layout. - Box::pin(async move { - let project = admitted_project_authorities(cg, &options)?; - let snapshots = AdmittedRequestSnapshotsV1::default(); - let freshness = graph_freshness_reader(tool_name, &options); - let ctx = admitted_tool_context(&options, &project, &snapshots, freshness)?; - portable_graph::dispatch_tool( - &ctx, - &verified_graph_open(&options), - tool_name, - args, - selected_scope_prefix, - options.code_index_ignored_dependency_admission.as_deref(), - ) - .await - }) -} - /// Dispatch project-info, registry, and file-inspection tools /// (`tracedecay_status`, `tracedecay_project_list`, ...). #[allow(clippy::too_many_arguments)] 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 7678ca070c..29eb3af956 100644 --- a/crates/tracedecay/src/mcp/tools/handlers/dispatch_test_support.rs +++ b/crates/tracedecay/src/mcp/tools/handlers/dispatch_test_support.rs @@ -388,9 +388,6 @@ pub(super) async fn concrete_dispatch_group_accepts( McpToolDispatchGroup::MultiRoot => { owned(handle_multi_root(tool_name, invalid_args, None, None, None, None).await) } - McpToolDispatchGroup::Graph => { - owned(dispatch_graph_tools(tool_name, cg, invalid_args, None, options).await) - } McpToolDispatchGroup::Info => { owned(dispatch_info_tools(tool_name, cg, invalid_args, None, None, options).await) } diff --git a/crates/tracedecay/src/mcp/tools/handlers/dispatch_tests.rs b/crates/tracedecay/src/mcp/tools/handlers/dispatch_tests.rs index a73289e458..d2f7a6185e 100644 --- a/crates/tracedecay/src/mcp/tools/handlers/dispatch_tests.rs +++ b/crates/tracedecay/src/mcp/tools/handlers/dispatch_tests.rs @@ -1805,12 +1805,10 @@ async fn a_stale_served_graph_read_carries_the_typed_freshness_trailer() { .await .unwrap(); - let stale = handle_tool_call_with_registry_options( + let stale = crate::mcp::tools::handlers::dispatch_test_support::dispatch_on_graph_authority( &cg, "tracedecay_search", json!({ "query": "probe" }), - None, - None, verified_graph_stale_options(&cg, ToolCallRegistryOptions::default()), ) .await @@ -1829,16 +1827,15 @@ async fn a_stale_served_graph_read_carries_the_typed_freshness_trailer() { "a rebuild-in-flight serve must state the seat age and the rebuild: {rendered}", ); - let unverified = handle_tool_call_with_registry_options( - &cg, - "tracedecay_search", - json!({ "query": "probe" }), - None, - None, - verified_graph_wedged_options(&cg, ToolCallRegistryOptions::default()), - ) - .await - .expect("an unverified stale serve still answers"); + let unverified = + crate::mcp::tools::handlers::dispatch_test_support::dispatch_on_graph_authority( + &cg, + "tracedecay_search", + json!({ "query": "probe" }), + verified_graph_wedged_options(&cg, ToolCallRegistryOptions::default()), + ) + .await + .expect("an unverified stale serve still answers"); let rendered = serde_json::to_string(&unverified.value).unwrap(); assert!( rendered.contains("source freshness remains unverified"), @@ -1849,12 +1846,10 @@ async fn a_stale_served_graph_read_carries_the_typed_freshness_trailer() { "an unverified route must not present itself as a rebuild: {rendered}", ); - let current = handle_tool_call_with_registry_options( + let current = crate::mcp::tools::handlers::dispatch_test_support::dispatch_on_graph_authority( &cg, "tracedecay_search", json!({ "query": "probe" }), - None, - None, verified_graph_options(&cg, ToolCallRegistryOptions::default()), ) .await diff --git a/crates/tracedecay/src/mcp/tools/handlers/graph_search_dispatch_tests.rs b/crates/tracedecay/src/mcp/tools/handlers/graph_search_dispatch_tests.rs index 0bac2d4916..682986afac 100644 --- a/crates/tracedecay/src/mcp/tools/handlers/graph_search_dispatch_tests.rs +++ b/crates/tracedecay/src/mcp/tools/handlers/graph_search_dispatch_tests.rs @@ -160,7 +160,7 @@ async fn completed_primary_search_is_not_retried_after_graph_admission_case() { }); let options = search_test_options(&cg, executor); - let result = crate::mcp::tools::handlers::handle_tool_call_with_registry_options( + let result = crate::mcp::tools::handlers::dispatch_test_support::dispatch_on_graph_authority( &cg, "tracedecay_search", json!({ @@ -168,8 +168,6 @@ async fn completed_primary_search_is_not_retried_after_graph_admission_case() { "limit": 5, "format": "json", }), - None, - None, options, ) .await @@ -230,7 +228,7 @@ async fn generation_mismatch_retry_cannot_erase_a_complete_sparse_search_case() }); let options = search_test_options(&cg, executor); - let result = crate::mcp::tools::handlers::handle_tool_call_with_registry_options( + let result = crate::mcp::tools::handlers::dispatch_test_support::dispatch_on_graph_authority( &cg, "tracedecay_search", json!({ @@ -238,8 +236,6 @@ async fn generation_mismatch_retry_cannot_erase_a_complete_sparse_search_case() "limit": 5, "format": "json", }), - None, - None, options, ) .await @@ -365,12 +361,10 @@ async fn search_opens_with_a_freshness_verdict_from_typed_state_case() { )), ..search_test_options(&cg, executor.clone()) }; - let result = crate::mcp::tools::handlers::handle_tool_call_with_registry_options( + let result = crate::mcp::tools::handlers::dispatch_test_support::dispatch_on_graph_authority( &cg, "tracedecay_search", json!({"query": "SparseLexicalWidget", "limit": 5}), - None, - None, settled, ) .await @@ -390,12 +384,10 @@ async fn search_opens_with_a_freshness_verdict_from_typed_state_case() { )), ..search_test_options(&cg, executor.clone()) }; - let result = crate::mcp::tools::handlers::handle_tool_call_with_registry_options( + let result = crate::mcp::tools::handlers::dispatch_test_support::dispatch_on_graph_authority( &cg, "tracedecay_search", json!({"query": "SparseLexicalWidget", "limit": 5, "format": "json"}), - None, - None, rebuilding, ) .await @@ -412,12 +404,10 @@ async fn search_opens_with_a_freshness_verdict_from_typed_state_case() { ); assert_eq!(payload["results"].as_array().map(Vec::len), Some(1)); - let result = crate::mcp::tools::handlers::handle_tool_call_with_registry_options( + let result = crate::mcp::tools::handlers::dispatch_test_support::dispatch_on_graph_authority( &cg, "tracedecay_search", json!({"query": "SparseLexicalWidget", "limit": 5}), - None, - None, crate::mcp::tools::handlers::ToolCallRegistryOptions { code_index_freshness_reader: Some(freshness_reader( Some("generation.mcp-verified-graph-fixture.1"), @@ -478,7 +468,7 @@ async fn search_forwards_lexical_routing_and_renders_route_evidence_case() { )), ..search_test_options(&cg, executor.clone()) }; - let result = crate::mcp::tools::handlers::handle_tool_call_with_registry_options( + let result = crate::mcp::tools::handlers::dispatch_test_support::dispatch_on_graph_authority( &cg, "tracedecay_search", json!({ @@ -487,8 +477,6 @@ async fn search_forwards_lexical_routing_and_renders_route_evidence_case() { "prefer_symbol": true, "limit": 5, }), - None, - None, options, ) .await @@ -510,7 +498,7 @@ async fn search_forwards_lexical_routing_and_renders_route_evidence_case() { "{text}" ); - let result = crate::mcp::tools::handlers::handle_tool_call_with_registry_options( + let result = crate::mcp::tools::handlers::dispatch_test_support::dispatch_on_graph_authority( &cg, "tracedecay_search", json!({ @@ -518,8 +506,6 @@ async fn search_forwards_lexical_routing_and_renders_route_evidence_case() { "lexical_anchors": ["SparseLexicalWidget"], "format": "json", }), - None, - None, search_test_options(&cg, executor.clone()), ) .await @@ -541,23 +527,19 @@ async fn search_forwards_lexical_routing_and_renders_route_evidence_case() { let too_many: Vec = (0..=tracedecay_query::retrieval::lexical::MAX_LEXICAL_ANCHORS_V1) .map(|index| format!("anchor_{index}")) .collect(); - let error = crate::mcp::tools::handlers::handle_tool_call_with_registry_options( + let error = crate::mcp::tools::handlers::dispatch_test_support::dispatch_on_graph_authority( &cg, "tracedecay_search", json!({"query": "sparse widget", "lexical_anchors": too_many}), - None, - None, search_test_options(&cg, executor.clone()), ) .await .expect_err("anchor bounds are enforced before any lane runs"); assert!(error.to_string().contains("at most 8 anchors"), "{error}"); - let error = crate::mcp::tools::handlers::handle_tool_call_with_registry_options( + let error = crate::mcp::tools::handlers::dispatch_test_support::dispatch_on_graph_authority( &cg, "tracedecay_search", json!({"query": "sparse widget", "lexical_anchors": [""]}), - None, - None, search_test_options(&cg, executor), ) .await diff --git a/crates/tracedecay/src/mcp/tools/handlers/mod.rs b/crates/tracedecay/src/mcp/tools/handlers/mod.rs index 3e90e172b3..1aebb07833 100644 --- a/crates/tracedecay/src/mcp/tools/handlers/mod.rs +++ b/crates/tracedecay/src/mcp/tools/handlers/mod.rs @@ -165,8 +165,8 @@ use tracedecay_tool_catalog::{ApplicationSurfaceOperation, BindingSurface}; use super::LegacyToolCompatibilityOwner; use dispatch_groups::{ - dispatch_admin_tools, dispatch_application_surface_tools, dispatch_graph_tools, - dispatch_health_tools, dispatch_info_tools, dispatch_session_workflow_tools, + dispatch_admin_tools, dispatch_application_surface_tools, dispatch_health_tools, + dispatch_info_tools, dispatch_session_workflow_tools, }; use tool_call_support::{boxed_send, rejected_tool_project_selector_present}; use tracedecay_api::{WorkHttpRequest, WorkflowHttpRequest}; @@ -517,7 +517,6 @@ pub fn handle_tool_call_with_registry_options<'a>( "registered project selection was not resolved before handler dispatch", )); } - let selected_scope_prefix = scope_prefix; // Classify before moving `args` so large payloads are not cloned into every // group probe. Application-surface tools still run before catalog checks. let dispatch_group = classify_mcp_tool_dispatch_group(tool_name); @@ -609,16 +608,6 @@ pub fn handle_tool_call_with_registry_options<'a>( let served_code_graph = options.served_code_graph.clone(); let dispatched = async { match dispatch_group { - Some(McpToolDispatchGroup::Graph) => { - boxed_send(dispatch_graph_tools( - tool_name, - cg, - args, - selected_scope_prefix, - options, - )) - .await - } Some(McpToolDispatchGroup::Info) => { boxed_send(dispatch_info_tools( tool_name, diff --git a/crates/tracedecay/src/mcp/tools/handlers/search_graph_independence_tests.rs b/crates/tracedecay/src/mcp/tools/handlers/search_graph_independence_tests.rs index c6d580999d..f32c89bf40 100644 --- a/crates/tracedecay/src/mcp/tools/handlers/search_graph_independence_tests.rs +++ b/crates/tracedecay/src/mcp/tools/handlers/search_graph_independence_tests.rs @@ -147,7 +147,7 @@ async fn tracedecay_search_preserves_lexical_results_when_graph_admission_is_mis let mut options = lexical_search_options(&cg); options.verified_graph_query_port = None; - let result = handle_tool_call_with_registry_options( + let result = crate::mcp::tools::handlers::dispatch_test_support::dispatch_on_graph_authority( &cg, "tracedecay_search", json!({ @@ -155,8 +155,6 @@ async fn tracedecay_search_preserves_lexical_results_when_graph_admission_is_mis "limit": 5, "format": "json", }), - None, - None, options, ) .await @@ -210,7 +208,7 @@ async fn tracedecay_search_refuses_foreign_generation_graph_evidence_without_era .await .expect("registered mismatch fixture"); - let result = handle_tool_call_with_registry_options( + let result = crate::mcp::tools::handlers::dispatch_test_support::dispatch_on_graph_authority( &cg, "tracedecay_search", json!({ @@ -218,8 +216,6 @@ async fn tracedecay_search_refuses_foreign_generation_graph_evidence_without_era "limit": 5, "format": "json", }), - None, - None, lexical_search_options(&cg), ) .await @@ -272,7 +268,7 @@ async fn tracedecay_search_does_not_wait_for_slow_graph_admission() { options.verified_graph_query_port = Some(Arc::new(PendingVerifiedGraphQueryPort)); let result = tokio::time::timeout( std::time::Duration::from_secs(1), - handle_tool_call_with_registry_options( + crate::mcp::tools::handlers::dispatch_test_support::dispatch_on_graph_authority( &cg, "tracedecay_search", json!({ @@ -280,8 +276,6 @@ async fn tracedecay_search_does_not_wait_for_slow_graph_admission() { "limit": 1, "format": "json", }), - None, - None, options, ), ) @@ -318,7 +312,7 @@ async fn tracedecay_search_does_not_wait_for_slow_graph_admission() { markdown_options.verified_graph_query_port = Some(Arc::new(PendingVerifiedGraphQueryPort)); let markdown = tokio::time::timeout( std::time::Duration::from_secs(1), - handle_tool_call_with_registry_options( + crate::mcp::tools::handlers::dispatch_test_support::dispatch_on_graph_authority( &cg, "tracedecay_search", json!({ @@ -326,8 +320,6 @@ async fn tracedecay_search_does_not_wait_for_slow_graph_admission() { "limit": 1, "format": "markdown", }), - None, - None, markdown_options, ), ) diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/schema_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/schema_test.rs index 0287f02661..3dd6b26f8c 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/schema_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/schema_test.rs @@ -13,13 +13,13 @@ async fn schema_required_arguments_match_representative_handler_parsers() { .expect("production project server"); wait_for_current_graph(&server).await; - // Direct `args.get(...).ok_or(...)` parser style. + // Typed request decode. assert_schema_requires(&tools, "tracedecay_search", &["query"]); expect_real_server_missing_argument_error( &server, "tracedecay_search", json!({}), - "missing required parameter: query", + "invalid arguments for tracedecay_search: missing field `query`", ) .await; diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/search_behavior_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/search_behavior_test.rs index 965f7be192..e4f6232ef7 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/search_behavior_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/search_behavior_test.rs @@ -29,7 +29,7 @@ fn search_displays(payload: &Value) -> Vec { } #[tokio::test] -async fn search_returns_the_named_symbol_and_rejects_a_missing_query() { +async fn search_returns_the_named_symbol_and_refuses_arguments_outside_its_typed_request() { let fixture = production_composition_fixture_with_sources(|project| { fs::create_dir_all(project.join("src")).expect("search fixture sources"); fs::write(project.join("src/ledger.rs"), LEDGER_SOURCE).expect("write ledger source"); @@ -40,21 +40,48 @@ async fn search_returns_the_named_symbol_and_rejects_a_missing_query() { .server(&fixture.project_root) .expect("production search server"); + let refusal = |detail: &str| { + json!({ + "code": -32603, + "message": format!("tool execution failed: config error: invalid arguments for tracedecay_search: {detail}"), + "data": { + "tool": "tracedecay_search", + "cli_fallback": "This tool is also available from the shell: `tracedecay tool search ...` (`tracedecay tool search --help` for parameters). If MCP calls keep failing or timing out, fall back to that CLI instead of querying .tracedecay databases directly.", + }, + }) + }; let missing = handle_real_server_tool_call_raw(&server, "tracedecay_search", json!({})).await; - assert_eq!(missing["error"]["code"], -32602, "{missing}"); assert_eq!( - missing["error"]["message"], "missing required parameter: query", + missing["error"], + refusal("missing field `query`"), "{missing}" ); + // Arguments outside the typed request are refused, not silently ignored. + let unknown = handle_real_server_tool_call_raw( + &server, + "tracedecay_search", + json!({"query": "ledger_post_entry", "semantic_mode": "hybrid"}), + ) + .await; assert_eq!( - missing["error"]["data"], - json!({ - "tool": "tracedecay_search", - "reason_code": "missing_required_parameter", - "retryable": false, - "detail": "missing required parameter: query", - }), - "{missing}" + unknown["error"], + refusal( + "unknown field `semantic_mode`, expected one of `query`, `limit`, `cursor`, \ + `lexical_anchors`, `prefer_symbol`, `lexical_aliases`, `lexical_phrases`, \ + `lexical_proximities`, `lexical_field_filters`, `lazy_index_ignored_dependencies`" + ), + "{unknown}" + ); + let untyped_limit = handle_real_server_tool_call_raw( + &server, + "tracedecay_search", + json!({"query": "ledger_post_entry", "limit": "5"}), + ) + .await; + assert_eq!( + untyped_limit["error"], + refusal("invalid type: string \"5\", expected u64"), + "{untyped_limit}" ); warm_code_index_search(&server, "ledger_post_entry").await; diff --git a/sdks/typescript/src/operations.ts b/sdks/typescript/src/operations.ts index 8d89eefe7a..88641f6917 100644 --- a/sdks/typescript/src/operations.ts +++ b/sdks/typescript/src/operations.ts @@ -72,6 +72,7 @@ export type AnalyzerRestartPolicyV1 = "restart_on_configuration_change" | "manua export type AnalyzerSettingsV1 = { readonly schema_version: number; readonly selections: readonly AnalyzerLanguageSelectionV1[] }; export type AnalyzerStructuredValueV1 = { readonly kind: "boolean"; readonly value: boolean; readonly [key: string]: unknown } | { readonly kind: "integer"; readonly value: number; readonly [key: string]: unknown } | { readonly kind: "text"; readonly value: string; readonly [key: string]: unknown } | { readonly kind: "text_list"; readonly value: readonly string[]; readonly [key: string]: unknown } | { readonly kind: "object"; readonly value: { readonly [key: string]: AnalyzerStructuredValueV1 }; readonly [key: string]: unknown }; export type AnchorExpandResult = { readonly anchors: readonly RetrievalAnchorId[] }; +export type ApplicationProblemDetailV1_2 = { readonly cause: string; readonly kind: "parked"; readonly remedy: string; readonly retries_on_wake: boolean } | { readonly active: number; readonly committed: number; readonly kind: "stale_refresh_frontier"; readonly requested: number } | { readonly deadline_ms: number; readonly kind: "lock_deadline"; readonly resource: string } | { readonly authority: string; readonly found_version?: number | null; readonly kind: "reset_required"; readonly reason: string; readonly remedy: string; readonly required_version?: number | null } | { readonly file?: string | null; readonly kind: "diagnostics_unsupported"; readonly searched: readonly DiagnosticsSearchedTsconfigV1[] } | { readonly generation?: string | null; readonly kind: "diagnostics_pending"; readonly producer: string }; export type ApplyWorkRelationReplanRequestV1 = { readonly mutation: WorkProductMutationIdentityV1; readonly proposal_id: ProposalId; readonly selection: WorkProductSelectionScopeV1 }; export type AstGrepResult = { readonly diff?: string | null; readonly dry_run?: boolean; readonly file_path: string; readonly message: string; readonly pattern: string; readonly rewrite: string; readonly success: boolean; readonly [key: string]: unknown }; export type AstGrepSearchMatchV1 = { readonly column: number; readonly file: string; readonly lang: string; readonly line: number; readonly line_text: string; readonly match: string }; @@ -268,6 +269,7 @@ export type DiagnosticProvenanceV1 = { readonly analyzer_revision: ComponentVers export type DiagnosticRecordStateV1 = { readonly kind: "current"; readonly [key: string]: unknown } | { readonly cleared_in_generation: CodeGenerationId; readonly kind: "cleared"; readonly [key: string]: unknown }; export type DiagnosticSeverityV1 = "error" | "warning" | "information" | "hint"; export type DiagnosticsPrimitiveScope = "workspace" | { readonly package: string } | { readonly file: string }; +export type DiagnosticsSearchedTsconfigV1 = { readonly path: string; readonly present: boolean }; export type DisclosureClass = "metadata" | "evidence" | "sensitive"; export type DistributionFileV1 = { readonly file: string; readonly kinds: readonly DistributionKindCountV1[] }; export type DistributionKindCountV1 = { readonly count: number; readonly kind: string }; @@ -792,6 +794,24 @@ export type ScopeSetRevision = number; export type ScopeSourceBinding = { readonly authority: AuthorityRef; readonly binding_id: SourceBindingId; readonly source_kind: SourceKindV1; readonly source_locator_digest: LocatorDigest }; export type ScopeUnavailableReasonV1 = "authority_unavailable" | "root_missing" | "store_unavailable"; export type ScoreDomainId = string; +export type SearchCompleteV1 = { readonly code_generation: string; readonly coverage: SearchCoverageV1; readonly external_import_hint?: SearchExternalImportHintV1 | null; readonly freshness: PrimitiveSearchFreshnessV1; readonly lexical_anchors?: readonly ContextLexicalAnchorV1[] | null; readonly lexical_routes?: readonly SearchLexicalRouteV1[] | null; readonly next_cursor?: string | null; readonly query_fallback_digest: string; readonly results: readonly SearchResultRowV1[]; readonly scope_prefix?: string | null; readonly scope_prefix_applied?: boolean | null; readonly verified_graph_evidence?: PrimitiveUnavailableEvidenceV1 | null }; +export type SearchCoverageV1 = { readonly exact: SearchLaneStatusV1; readonly graph: SearchLaneStatusV1; readonly lexical: SearchLaneStatusV1; readonly recall: PrimitiveRecallV1 }; +export type SearchExternalImportCandidatesV1 = { readonly candidates: readonly SearchExternalImportV1[]; readonly evidence: string; readonly message: string; readonly resolution_status: string; readonly suggested_action: string }; +export type SearchExternalImportHintV1 = SearchExternalImportCandidatesV1 | PrimitiveUnavailableEvidenceV1; +export type SearchExternalImportV1 = { readonly import_file: string; readonly line: number; readonly module: string; readonly symbol?: string | null }; +export type SearchLaneStateV1 = { readonly generation: string; readonly status: "stale" } | { readonly generation?: string | null; readonly reason?: string | null; readonly status: "partial" } | { readonly reason: string; readonly status: "unavailable" }; +export type SearchLaneStatusV1 = PrimitiveLaneCompleteV1 | SearchLaneStateV1; +export type SearchLexicalAliasV1 = { readonly alternative: string; readonly strict_query: string }; +export type SearchLexicalAlternativeReasonV1 = "configured_vocabulary_alias"; +export type SearchLexicalFieldFilterV1 = { readonly field: SearchLexicalFieldV1; readonly include: boolean }; +export type SearchLexicalFieldV1 = "symbol_name" | "qualified_name" | "path" | "signature" | "documentation" | "body_text" | "preamble_text" | "exact_term" | "subtoken"; +export type SearchLexicalProximityV1 = { readonly maximum_gap: number; readonly terms: readonly string[] }; +export type SearchLexicalRouteV1 = { readonly label: string; readonly route: "query" } | { readonly anchor: string; readonly label: string; readonly route: "anchor" } | { readonly label: string; readonly route: "preferred_symbol"; readonly tokens: readonly string[] } | { readonly label: string; readonly route: "identifier_split"; readonly strict_query: string; readonly terms: readonly string[] } | { readonly alternative: string; readonly label: string; readonly reason: SearchLexicalAlternativeReasonV1; readonly route: "alias"; readonly strict_query: string }; +export type SearchResultDisplayV1 = { readonly kind: string; readonly name: string; readonly path: string; readonly qualified_name: string }; +export type SearchResultRowV1 = { readonly candidate: FusedCandidate; readonly display?: SearchResultDisplayV1 | null; readonly final_ordinal: number; readonly lexical_routes?: readonly SearchRouteMatchV1[] | null; readonly node_id?: string | null }; +export type SearchRouteMatchV1 = { readonly matched_terms: readonly string[]; readonly route: string; readonly score_micros: number; readonly spelling_variants?: readonly SearchSpellingVariantV1[] }; +export type SearchSpellingVariantV1 = { readonly alternative: string; readonly query: string }; +export type SearchUnavailableV1 = { readonly code_generation?: string | null; readonly coverage: SearchCoverageV1; readonly detail?: ApplicationProblemDetailV1_2 | null; readonly freshness: PrimitiveSearchFreshnessV1; readonly query_fallback_digest?: string | null; readonly reason: string; readonly results: readonly SearchResultRowV1[]; readonly status: PrimitiveUnavailableStatusV1; readonly verified_graph_evidence?: PrimitiveUnavailableEvidenceV1 | null }; export type SensitiveFilesystemLocatorV1 = { readonly locator_digest: LocatorDigest; readonly sealed_value_digest: ManifestDigest }; export type SensitivityV1 = "unclassified" | "non_sensitive" | "sensitive" | "secret"; export type SessionCorrelationHitV1 = { readonly branch?: string | null; readonly commit_sha?: string | null; readonly committed_at?: number | null; readonly confidence?: number | null; readonly event_count: number; readonly evidence?: string | null; readonly evidence_message_id?: string | null; readonly first_ts?: number | null; readonly last_ts?: number | null; readonly provider: string; readonly relation?: string | null; readonly session_id: string; readonly sources: readonly string[]; readonly span_count: number; readonly span_overlap_kind?: string | null; readonly worktree?: string | null }; @@ -1434,6 +1454,8 @@ export type OperationApplicationReplaceSymbolRequest = { readonly dry_run?: bool export type OperationApplicationReplaceSymbolResult = EditResult | MultiEditResult | InsertResult | AstGrepResult | MoveResult | RenameResult | SourceEditFailedResultV1 | SourceEditCancelledResultV1 | SourceEditTimedOutResultV1 | SourceEditEffectUnknownResultV1 | SourceEditReconciledResultV1 | SourceEditDurableEffectPayloadV1; export type OperationApplicationRetrieveRequest = { readonly handle: string; readonly max_chars?: number | null; readonly offset?: number | null }; export type OperationApplicationRetrieveResult = RetrievedPageV1 | RetrieveHandleMissingV1 | RetrieveHandleExpiredV1; +export type OperationApplicationSearchRequest = { readonly cursor?: string | null; readonly lazy_index_ignored_dependencies?: boolean | null; readonly lexical_aliases?: readonly SearchLexicalAliasV1[] | null; readonly lexical_anchors?: readonly string[] | null; readonly lexical_field_filters?: readonly SearchLexicalFieldFilterV1[] | null; readonly lexical_phrases?: readonly string[] | null; readonly lexical_proximities?: readonly SearchLexicalProximityV1[] | null; readonly limit?: number | null; readonly prefer_symbol?: boolean | null; readonly query: string }; +export type OperationApplicationSearchResult = SearchCompleteV1 | SearchUnavailableV1; export type OperationApplicationSessionLookupRequest = { readonly meta: RetrievalRequestMeta; readonly session_id: SessionId }; export type OperationApplicationSessionLookupResult = { readonly anchors: readonly RetrievalAnchorId[] }; export type OperationApplicationSessionRefreshBeginRequest = { readonly handle?: string | null; readonly scope: SessionRefreshScopeV1; readonly session: SessionRefreshSessionV1; readonly source: SessionRefreshSourceV1; readonly target: SessionRefreshTargetV1 }; @@ -1650,6 +1672,7 @@ const DEFINITIONS = { AnalyzerSettingsV1: {"additionalProperties":false,"description":"Canonical analyzer settings. A changed selection produces a new\nconfiguration revision/digest; cache invalidation remains owned elsewhere.","properties":{"schema_version":{"format":"uint16","maximum":65535,"minimum":0,"type":"integer"},"selections":{"items":{"$ref":"#/$defs/AnalyzerLanguageSelectionV1"},"type":"array"}},"required":["schema_version","selections"],"type":"object"}, AnalyzerStructuredValueV1: {"description":"A structured analyzer option value. This deliberately excludes raw\nenvironment values, commands, credential material, and transport blobs.","oneOf":[{"properties":{"kind":{"const":"boolean","type":"string"},"value":{"type":"boolean"}},"required":["kind","value"],"type":"object"},{"properties":{"kind":{"const":"integer","type":"string"},"value":{"format":"int64","type":"integer"}},"required":["kind","value"],"type":"object"},{"properties":{"kind":{"const":"text","type":"string"},"value":{"type":"string"}},"required":["kind","value"],"type":"object"},{"properties":{"kind":{"const":"text_list","type":"string"},"value":{"items":{"type":"string"},"type":"array"}},"required":["kind","value"],"type":"object"},{"properties":{"kind":{"const":"object","type":"string"},"value":{"additionalProperties":{"$ref":"#/$defs/AnalyzerStructuredValueV1"},"type":"object"}},"required":["kind","value"],"type":"object"}]}, AnchorExpandResult: {"additionalProperties":false,"properties":{"anchors":{"items":{"$ref":"#/$defs/RetrievalAnchorId"},"type":"array"}},"required":["anchors"],"type":"object"}, + ApplicationProblemDetailV1_2: {"description":"The structured facts behind a problem. Adapters read these fields; the\nproblem's `message` is only their one human rendering.","oneOf":[{"additionalProperties":false,"description":"The worktree's code index is parked until the operator applies\n`remedy`; repeating the request cannot change the answer.","properties":{"cause":{"type":"string"},"kind":{"const":"parked","type":"string"},"remedy":{"type":"string"},"retries_on_wake":{"type":"boolean"}},"required":["kind","cause","remedy","retries_on_wake"],"type":"object"},{"additionalProperties":false,"description":"A session refresh asked to begin from a source frontier the\ncommitted projection has already passed.","properties":{"active":{"format":"uint64","minimum":0,"type":"integer"},"committed":{"format":"uint64","minimum":0,"type":"integer"},"kind":{"const":"stale_refresh_frontier","type":"string"},"requested":{"format":"uint64","minimum":0,"type":"integer"}},"required":["kind","requested","committed","active"],"type":"object"},{"additionalProperties":false,"description":"A writer lock stayed held by other writers past its admission\ndeadline.","properties":{"deadline_ms":{"format":"uint64","minimum":0,"type":"integer"},"kind":{"const":"lock_deadline","type":"string"},"resource":{"type":"string"}},"required":["kind","resource","deadline_ms"],"type":"object"},{"additionalProperties":false,"description":"A persisted store whose shape this binary does not open. It is served\nin this typed state until the operator runs `remedy`, which deletes\nthe old data; nothing is migrated or backed up.","properties":{"authority":{"type":"string"},"found_version":{"format":"int64","type":["integer","null"]},"kind":{"const":"reset_required","type":"string"},"reason":{"type":"string"},"remedy":{"type":"string"},"required_version":{"format":"int64","type":["integer","null"]}},"required":["kind","authority","reason","remedy"],"type":"object"},{"additionalProperties":false,"description":"No compiler runs automatically for the diagnostics scope: no tsconfig\nowns `file`, or, for a workspace read (`file` null), none exists under\nthe project root. `searched` lists the owner search's candidates,\nnearest first; it is empty for a workspace read or a file outside the\nproject root.","properties":{"file":{"type":["string","null"]},"kind":{"const":"diagnostics_unsupported","type":"string"},"searched":{"items":{"$ref":"#/$defs/DiagnosticsSearchedTsconfigV1"},"type":"array"}},"required":["kind","searched"],"type":"object"},{"additionalProperties":false,"description":"The TypeScript `producer` has not published diagnostics for the\ncurrent code generation yet. `generation` is the last generation it\npublished, null when it has published none.","properties":{"generation":{"type":["string","null"]},"kind":{"const":"diagnostics_pending","type":"string"},"producer":{"type":"string"}},"required":["kind","producer"],"type":"object"}]}, ApplyWorkRelationReplanRequestV1: {"additionalProperties":false,"properties":{"mutation":{"$ref":"#/$defs/WorkProductMutationIdentityV1"},"proposal_id":{"$ref":"#/$defs/ProposalId"},"selection":{"$ref":"#/$defs/WorkProductSelectionScopeV1"}},"required":["selection","proposal_id","mutation"],"type":"object"}, AstGrepResult: {"description":"Result of an ast-grep rewrite operation.","properties":{"diff":{"description":"Bounded preview of the would-be change. Populated only on a successful\ndry run; `None` for real edits and for failures.","type":["string","null"]},"dry_run":{"description":"True when this was a dry run: the rewrite was resolved (via the built-in\nliteral fallback or an ast-grep preview run) but nothing was written.","type":"boolean"},"file_path":{"type":"string"},"message":{"type":"string"},"pattern":{"type":"string"},"rewrite":{"type":"string"},"success":{"type":"boolean"}},"required":["success","file_path","pattern","rewrite","message"],"type":"object"}, AstGrepSearchMatchV1: {"additionalProperties":false,"properties":{"column":{"format":"uint32","minimum":0,"type":"integer"},"file":{"type":"string"},"lang":{"type":"string"},"line":{"format":"uint32","minimum":0,"type":"integer"},"line_text":{"type":"string"},"match":{"type":"string"}},"required":["file","line","column","lang","match","line_text"],"type":"object"}, @@ -1846,6 +1869,7 @@ const DEFINITIONS = { DiagnosticRecordStateV1: {"description":"Current-vs-stale typing for a durable diagnostic record. Publication is\nversion-monotone: a newer clean generation clears the prior publication\ndeterministically, and stale findings cannot cross snapshots.\nStale and historical records remain queryable through\napplication APIs but are excluded from active publication.","oneOf":[{"description":"Current for exactly the clean generation named by the record.","properties":{"kind":{"const":"current","type":"string"}},"required":["kind"],"type":"object"},{"description":"A clean generation completed and deterministically removed this\nfinding (resolution, deletion, source-revision drift, or content or\ngeneration change).","properties":{"cleared_in_generation":{"$ref":"#/$defs/CodeGenerationId"},"kind":{"const":"cleared","type":"string"}},"required":["kind","cleared_in_generation"],"type":"object"}]}, DiagnosticSeverityV1: {"description":"Diagnostic severity. Source severity is preserved exactly; TraceDecay\nnever raises severity because several producers agree.","enum":["error","warning","information","hint"],"type":"string"}, DiagnosticsPrimitiveScope: {"oneOf":[{"enum":["workspace"],"type":"string"},{"additionalProperties":false,"properties":{"package":{"type":"string"}},"required":["package"],"type":"object"},{"additionalProperties":false,"properties":{"file":{"type":"string"}},"required":["file"],"type":"object"}]}, + DiagnosticsSearchedTsconfigV1: {"additionalProperties":false,"description":"One tsconfig location the diagnostics owner search checked.","properties":{"path":{"description":"Relative to the project root, forward-slash separated.","type":"string"},"present":{"description":"The config exists, but neither it nor its references include the\nfile.","type":"boolean"}},"required":["path","present"],"type":"object"}, DisclosureClass: {"description":"Disclosure ceiling carried by an immutable grant and revalidated at sinks.","enum":["metadata","evidence","sensitive"],"type":"string"}, DistributionFileV1: {"additionalProperties":false,"properties":{"file":{"type":"string"},"kinds":{"items":{"$ref":"#/$defs/DistributionKindCountV1"},"type":"array"}},"required":["file","kinds"],"type":"object"}, DistributionKindCountV1: {"additionalProperties":false,"properties":{"count":{"format":"uint64","minimum":0,"type":"integer"},"kind":{"type":"string"}},"required":["kind","count"],"type":"object"}, @@ -2370,6 +2394,24 @@ const DEFINITIONS = { ScopeSourceBinding: {"additionalProperties":false,"description":"A source-to-authority binding. It stores only the source kind, a redacted\nlocator digest, and the pre-resolved authority reference.","properties":{"authority":{"$ref":"#/$defs/AuthorityRef"},"binding_id":{"$ref":"#/$defs/SourceBindingId"},"source_kind":{"$ref":"#/$defs/SourceKindV1"},"source_locator_digest":{"$ref":"#/$defs/LocatorDigest"}},"required":["binding_id","source_kind","source_locator_digest","authority"],"type":"object"}, ScopeUnavailableReasonV1: {"description":"Typed explanation for a root that could not return usable data.","enum":["authority_unavailable","root_missing","store_unavailable"],"type":"string"}, ScoreDomainId: {"description":"Strongly typed canonical identity: `ScoreDomainId`.","type":"string"}, + SearchCompleteV1: {"additionalProperties":false,"properties":{"code_generation":{"type":"string"},"coverage":{"$ref":"#/$defs/SearchCoverageV1"},"external_import_hint":{"anyOf":[{"$ref":"#/$defs/SearchExternalImportHintV1"},{"type":"null"}]},"freshness":{"$ref":"#/$defs/PrimitiveSearchFreshnessV1","description":"Freshness of the served code generation, derived from the typed lane\ncoverage and the daemon scheduler's worktree state."},"lexical_anchors":{"description":"Outcome of every caller `lexical_anchors` entry, beside the routes.","items":{"$ref":"#/$defs/ContextLexicalAnchorV1"},"type":["array","null"]},"lexical_routes":{"description":"Every lexical route fused into this page; present only when a route\nbeyond the strict query ran or ranked a result.","items":{"$ref":"#/$defs/SearchLexicalRouteV1"},"type":["array","null"]},"next_cursor":{"description":"Authenticated continuation for the next page; null on the last page.","type":["string","null"]},"query_fallback_digest":{"type":"string"},"results":{"items":{"$ref":"#/$defs/SearchResultRowV1"},"type":"array"},"scope_prefix":{"description":"The session's scope prefix. Ranked candidates carry no file path, so\nthe scope is reported and not applied.","type":["string","null"]},"scope_prefix_applied":{"type":["boolean","null"]},"verified_graph_evidence":{"anyOf":[{"$ref":"#/$defs/PrimitiveUnavailableEvidenceV1"},{"type":"null"}]}},"required":["freshness","code_generation","query_fallback_digest","coverage","results"],"type":"object"}, + SearchCoverageV1: {"additionalProperties":false,"description":"Per-lane recall of one search, so a full-recall answer is told apart from\none produced while a lane was down.","properties":{"exact":{"$ref":"#/$defs/SearchLaneStatusV1"},"graph":{"$ref":"#/$defs/SearchLaneStatusV1"},"lexical":{"$ref":"#/$defs/SearchLaneStatusV1"},"recall":{"$ref":"#/$defs/PrimitiveRecallV1"}},"required":["exact","lexical","graph","recall"],"type":"object"}, + SearchExternalImportCandidatesV1: {"additionalProperties":false,"properties":{"candidates":{"items":{"$ref":"#/$defs/SearchExternalImportV1"},"type":"array"},"evidence":{"type":"string"},"message":{"type":"string"},"resolution_status":{"type":"string"},"suggested_action":{"type":"string"}},"required":["message","evidence","resolution_status","candidates","suggested_action"],"type":"object"}, + SearchExternalImportHintV1: {"anyOf":[{"$ref":"#/$defs/SearchExternalImportCandidatesV1"},{"$ref":"#/$defs/PrimitiveUnavailableEvidenceV1"}],"description":"Advisory evidence that a sparse search matched external-module imports,\nor why that evidence could not be read."}, + SearchExternalImportV1: {"additionalProperties":false,"properties":{"import_file":{"type":"string"},"line":{"format":"uint32","minimum":0,"type":"integer"},"module":{"type":"string"},"symbol":{"description":"The imported name; null for a whole-module import.","type":["string","null"]}},"required":["module","import_file","line"],"type":"object"}, + SearchLaneStateV1: {"description":"A lane that did not serve the current complete generation. A partial\nlane names its generation and reason when it has them, null otherwise.","oneOf":[{"additionalProperties":false,"properties":{"generation":{"type":"string"},"status":{"const":"stale","type":"string"}},"required":["status","generation"],"type":"object"},{"additionalProperties":false,"properties":{"generation":{"type":["string","null"]},"reason":{"type":["string","null"]},"status":{"const":"partial","type":"string"}},"required":["status"],"type":"object"},{"additionalProperties":false,"properties":{"reason":{"type":"string"},"status":{"const":"unavailable","type":"string"}},"required":["status","reason"],"type":"object"}]}, + SearchLaneStatusV1: {"anyOf":[{"$ref":"#/$defs/PrimitiveLaneCompleteV1"},{"$ref":"#/$defs/SearchLaneStateV1"}]}, + SearchLexicalAliasV1: {"additionalProperties":false,"description":"One named query-time vocabulary alias.","properties":{"alternative":{"maxLength":128,"type":"string"},"strict_query":{"maxLength":128,"type":"string"}},"required":["strict_query","alternative"],"type":"object"}, + SearchLexicalAlternativeReasonV1: {"description":"Why the lexical lane tried a configured alternative.","enum":["configured_vocabulary_alias"],"type":"string"}, + SearchLexicalFieldFilterV1: {"additionalProperties":false,"properties":{"field":{"$ref":"#/$defs/SearchLexicalFieldV1"},"include":{"type":"boolean"}},"required":["field","include"],"type":"object"}, + SearchLexicalFieldV1: {"description":"A lexical posting field a search may include or exclude.","enum":["symbol_name","qualified_name","path","signature","documentation","body_text","preamble_text","exact_term","subtoken"],"type":"string"}, + SearchLexicalProximityV1: {"additionalProperties":false,"description":"Ordered terms that must occur in one field within `maximum_gap`\nintervening tokens of each other.","properties":{"maximum_gap":{"format":"uint32","maximum":8,"minimum":0,"type":"integer"},"terms":{"items":{"type":"string"},"maxItems":8,"minItems":2,"type":"array"}},"required":["terms","maximum_gap"],"type":"object"}, + SearchLexicalRouteV1: {"description":"One lexical route fused into the page, with its display label.","oneOf":[{"additionalProperties":false,"description":"The caller's strict lexical query.","properties":{"label":{"type":"string"},"route":{"const":"query","type":"string"}},"required":["route","label"],"type":"object"},{"additionalProperties":false,"description":"One caller-supplied exact identifier or term.","properties":{"anchor":{"type":"string"},"label":{"type":"string"},"route":{"const":"anchor","type":"string"}},"required":["route","anchor","label"],"type":"object"},{"additionalProperties":false,"description":"Identifier-shaped tokens of the query, restricted to symbol names.","properties":{"label":{"type":"string"},"route":{"const":"preferred_symbol","type":"string"},"tokens":{"items":{"type":"string"},"type":"array"}},"required":["route","tokens","label"],"type":"object"},{"additionalProperties":false,"description":"Identifier and path components recovered from the strict query.","properties":{"label":{"type":"string"},"route":{"const":"identifier_split","type":"string"},"strict_query":{"type":"string"},"terms":{"items":{"type":"string"},"type":"array"}},"required":["route","strict_query","terms","label"],"type":"object"},{"additionalProperties":false,"description":"A configured query-time vocabulary alternative.","properties":{"alternative":{"type":"string"},"label":{"type":"string"},"reason":{"$ref":"#/$defs/SearchLexicalAlternativeReasonV1"},"route":{"const":"alias","type":"string"},"strict_query":{"type":"string"}},"required":["route","strict_query","alternative","reason","label"],"type":"object"}]}, + SearchResultDisplayV1: {"additionalProperties":false,"properties":{"kind":{"type":"string"},"name":{"type":"string"},"path":{"type":"string"},"qualified_name":{"type":"string"}},"required":["name","qualified_name","kind","path"],"type":"object"}, + SearchResultRowV1: {"additionalProperties":false,"description":"One ranked candidate with its generation-bound display metadata.","properties":{"candidate":{"$ref":"#/$defs/FusedCandidate"},"display":{"anyOf":[{"$ref":"#/$defs/SearchResultDisplayV1"},{"type":"null"}]},"final_ordinal":{"format":"uint32","minimum":0,"type":"integer"},"lexical_routes":{"description":"The lexical routes that ranked this candidate.","items":{"$ref":"#/$defs/SearchRouteMatchV1"},"type":["array","null"]},"node_id":{"description":"The graph symbol to read with `tracedecay_source_body`.","type":["string","null"]}},"required":["candidate","final_ordinal"],"type":"object"}, + SearchRouteMatchV1: {"additionalProperties":false,"description":"One route that ranked a candidate, named by its route label.","properties":{"matched_terms":{"items":{"type":"string"},"type":"array"},"route":{"type":"string"},"score_micros":{"format":"uint64","minimum":0,"type":"integer"},"spelling_variants":{"items":{"$ref":"#/$defs/SearchSpellingVariantV1"},"type":"array"}},"required":["route","score_micros","matched_terms"],"type":"object"}, + SearchSpellingVariantV1: {"additionalProperties":false,"properties":{"alternative":{"type":"string"},"query":{"type":"string"}},"required":["query","alternative"],"type":"object"}, + SearchUnavailableV1: {"additionalProperties":false,"properties":{"code_generation":{"type":["string","null"]},"coverage":{"$ref":"#/$defs/SearchCoverageV1"},"detail":{"anyOf":[{"$ref":"#/$defs/ApplicationProblemDetailV1"},{"type":"null"}],"description":"Present when indexing is parked: its cause, remedy, and wake retry."},"freshness":{"$ref":"#/$defs/PrimitiveSearchFreshnessV1"},"query_fallback_digest":{"description":"Always null: no fallback subpayload was produced.","type":["string","null"]},"reason":{"type":"string"},"results":{"description":"Always empty: no generation answered.","items":{"$ref":"#/$defs/SearchResultRowV1"},"type":"array"},"status":{"$ref":"#/$defs/PrimitiveUnavailableStatusV1"},"verified_graph_evidence":{"anyOf":[{"$ref":"#/$defs/PrimitiveUnavailableEvidenceV1"},{"type":"null"}]}},"required":["freshness","results","status","reason","coverage"],"type":"object"}, SensitiveFilesystemLocatorV1: {"additionalProperties":false,"description":"Reference-only filesystem locator. The raw path is sealed outside this\ncontract; only the privacy-bound locator digest and sealed-value digest are\nrepresentable here.","properties":{"locator_digest":{"$ref":"#/$defs/LocatorDigest"},"sealed_value_digest":{"$ref":"#/$defs/ManifestDigest"}},"required":["locator_digest","sealed_value_digest"],"type":"object"}, SensitivityV1: {"enum":["unclassified","non_sensitive","sensitive","secret"],"type":"string"}, SessionCorrelationHitV1: {"additionalProperties":false,"properties":{"branch":{"type":["string","null"]},"commit_sha":{"type":["string","null"]},"committed_at":{"format":"int64","type":["integer","null"]},"confidence":{"format":"int64","type":["integer","null"]},"event_count":{"format":"int64","type":"integer"},"evidence":{"type":["string","null"]},"evidence_message_id":{"type":["string","null"]},"first_ts":{"format":"int64","type":["integer","null"]},"last_ts":{"format":"int64","type":["integer","null"]},"provider":{"type":"string"},"relation":{"type":["string","null"]},"session_id":{"type":"string"},"sources":{"items":{"type":"string"},"type":"array"},"span_count":{"format":"int64","type":"integer"},"span_overlap_kind":{"type":["string","null"]},"worktree":{"type":["string","null"]}},"required":["provider","session_id","event_count","span_count","sources"],"type":"object"}, @@ -2976,6 +3018,8 @@ const SCHEMAS: readonly CanonicalJsonSchema[] = [ {"$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"description":"Exact public input accepted by `tracedecay_replace_symbol`.","properties":{"dry_run":{"default":false,"type":"boolean"},"expected_state":{"description":"Exact `preview_digest`/`expected_state` returned by the dry run. Apply\nre-resolves the typed plan and rejects any candidate-state drift.","type":["string","null"]},"idempotency_key":{"type":["string","null"]},"new_source":{"type":"string"},"symbol":{"type":"string"},"verify":{"default":false,"type":"boolean"}},"required":["symbol","new_source"],"title":"ReplaceSymbolSurfaceRequestV1","type":"object"}, {"$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"properties":{"handle":{"description":"The required `handle` argument copied exactly from a truncated MCP\nresponse envelope.","type":"string"},"max_chars":{"description":"Maximum characters requested for this page. Values above the safe\nresponse-frame budget are clamped.","format":"uint64","minimum":1,"type":["integer","null"]},"offset":{"default":0,"description":"Character offset into the immutable stored response (default: 0). Use\nthe prior page's next_offset.","format":"uint64","minimum":0,"type":["integer","null"]}},"required":["handle"],"title":"RetrieveSurfaceRequestV1","type":"object"}, {"$defs":{"RetrieveHandleExpiredV1":DEFINITIONS.RetrieveHandleExpiredV1,"RetrieveHandleMissingV1":DEFINITIONS.RetrieveHandleMissingV1,"RetrievedPageV1":DEFINITIONS.RetrievedPageV1},"$schema":"https://json-schema.org/draft/2020-12/schema","anyOf":[{"$ref":"#/$defs/RetrievedPageV1"},{"$ref":"#/$defs/RetrieveHandleMissingV1"},{"$ref":"#/$defs/RetrieveHandleExpiredV1"}],"description":"What a retrieve found under the handle.","title":"RetrieveResultV1"}, + {"$defs":{"SearchLexicalAliasV1":DEFINITIONS.SearchLexicalAliasV1,"SearchLexicalFieldFilterV1":DEFINITIONS.SearchLexicalFieldFilterV1,"SearchLexicalFieldV1":DEFINITIONS.SearchLexicalFieldV1,"SearchLexicalProximityV1":DEFINITIONS.SearchLexicalProximityV1},"$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"properties":{"cursor":{"description":"Authenticated opaque continuation returned as next_cursor. Repeat the same query and lexical options with it.","type":["string","null"]},"lazy_index_ignored_dependencies":{"description":"Opt in to bounded indexing of ignored dependency entry files when an import hint matches (default: false).","type":["boolean","null"]},"lexical_aliases":{"description":"Named query-time vocabulary aliases. The strict query always ranks first. Alias-only hits follow it with the strict query, alternative, and configured-vocabulary reason disclosed.","items":{"$ref":"#/$defs/SearchLexicalAliasV1"},"maxItems":8,"type":["array","null"]},"lexical_anchors":{"description":"Exact identifiers or technical terms (e.g. 'reserve_stock', 'Foo::bar', 'E0308') that the answer must be about. Each is ranked through the lexical lane as its own route: a hit carrying an anchor outranks every hit that carries none, exact hits included, every anchor with matches keeps at least its best sites through the lane cap, and `lexical_anchors` in the response reports each anchor's outcome (`matched` rows, `admitted` sites this response returns, `dropped` admitted sites it could not carry with the reason, `unmatched`, or `not_served`). Ranked retrieval, not exhaustive grep (use tracedecay_grep for that). Each result names the routes that ranked it. At most 8 anchors, each one whitespace-free term of at most 128 bytes, no repeats.","items":{"type":"string"},"maxItems":8,"type":["array","null"]},"lexical_field_filters":{"description":"Include or exclude lexical symbol_name, qualified_name, path, signature, documentation, body_text, preamble_text, exact_term, or subtoken fields.","items":{"$ref":"#/$defs/SearchLexicalFieldFilterV1"},"maxItems":9,"type":["array","null"]},"lexical_phrases":{"description":"Exact lexical phrases to rank through n-gram candidate pruning.","items":{"type":"string"},"maxItems":4,"type":["array","null"]},"lexical_proximities":{"description":"Ordered lexical terms that must occur within the bounded intervening-token gap.","items":{"$ref":"#/$defs/SearchLexicalProximityV1"},"maxItems":4,"type":["array","null"]},"limit":{"description":"Maximum number of results to return (default: 10)","format":"uint64","minimum":0,"type":["integer","null"]},"prefer_symbol":{"description":"Add a lexical route restricted to symbol-name matches for the identifier-shaped words of the query (default: false). Query words such as class/struct/function/find/explain are ignored; 'Foo::bar' and 'Foo.bar' contribute 'bar'.","type":["boolean","null"]},"query":{"description":"Search query string to match against symbol names","type":"string"}},"required":["query"],"title":"SearchSurfaceRequestV1","type":"object"}, + {"$defs":{"ApplicationProblemDetailV1":DEFINITIONS.ApplicationProblemDetailV1_2,"CalibrationProfileId":DEFINITIONS.CalibrationProfileId,"CandidateContribution":DEFINITIONS.CandidateContribution,"CodeIndexBuildBlockedReasonV1":DEFINITIONS.CodeIndexBuildBlockedReasonV1,"CodeIndexConvergenceParkedV1":DEFINITIONS.CodeIndexConvergenceParkedV1,"CodeIndexStalenessStateV1":DEFINITIONS.CodeIndexStalenessStateV1,"ComponentRevision":DEFINITIONS.ComponentRevision,"ContextLexicalAnchorV1":DEFINITIONS.ContextLexicalAnchorV1,"DiagnosticsSearchedTsconfigV1":DEFINITIONS.DiagnosticsSearchedTsconfigV1,"EvidenceRole":DEFINITIONS.EvidenceRole,"ExactClass":DEFINITIONS.ExactClass,"FileOccurrenceId":DEFINITIONS.FileOccurrenceId,"FixedPointScore":DEFINITIONS.FixedPointScore,"FreshnessCompatibilityV1":DEFINITIONS.FreshnessCompatibilityV1,"FusedCandidate":DEFINITIONS.FusedCandidate,"LexicalAnchorDropReasonV1":DEFINITIONS.LexicalAnchorDropReasonV1,"LexicalAnchorDropV1":DEFINITIONS.LexicalAnchorDropV1,"LogicalCopyClusterId":DEFINITIONS.LogicalCopyClusterId,"LogicalEvidenceId":DEFINITIONS.LogicalEvidenceId,"OccurrenceProvenance":DEFINITIONS.OccurrenceProvenance,"PrimitiveFreshnessStateV1":DEFINITIONS.PrimitiveFreshnessStateV1,"PrimitiveIndexingStateV1":DEFINITIONS.PrimitiveIndexingStateV1,"PrimitiveLaneCompleteV1":DEFINITIONS.PrimitiveLaneCompleteV1,"PrimitiveRecallV1":DEFINITIONS.PrimitiveRecallV1,"PrimitiveSearchFreshnessV1":DEFINITIONS.PrimitiveSearchFreshnessV1,"PrimitiveUnavailableEvidenceV1":DEFINITIONS.PrimitiveUnavailableEvidenceV1,"PrimitiveUnavailableStatusV1":DEFINITIONS.PrimitiveUnavailableStatusV1,"RankingDecision":DEFINITIONS.RankingDecision,"RankingDecisionKind":DEFINITIONS.RankingDecisionKind,"RepositoryId":DEFINITIONS.RepositoryId,"RetrievalAnchorId":DEFINITIONS.RetrievalAnchorId,"RetrieverKind":DEFINITIONS.RetrieverKind,"ScoreDomainId":DEFINITIONS.ScoreDomainId,"SearchCompleteV1":DEFINITIONS.SearchCompleteV1,"SearchCoverageV1":DEFINITIONS.SearchCoverageV1,"SearchExternalImportCandidatesV1":DEFINITIONS.SearchExternalImportCandidatesV1,"SearchExternalImportHintV1":DEFINITIONS.SearchExternalImportHintV1,"SearchExternalImportV1":DEFINITIONS.SearchExternalImportV1,"SearchLaneStateV1":DEFINITIONS.SearchLaneStateV1,"SearchLaneStatusV1":DEFINITIONS.SearchLaneStatusV1,"SearchLexicalAlternativeReasonV1":DEFINITIONS.SearchLexicalAlternativeReasonV1,"SearchLexicalRouteV1":DEFINITIONS.SearchLexicalRouteV1,"SearchResultDisplayV1":DEFINITIONS.SearchResultDisplayV1,"SearchResultRowV1":DEFINITIONS.SearchResultRowV1,"SearchRouteMatchV1":DEFINITIONS.SearchRouteMatchV1,"SearchSpellingVariantV1":DEFINITIONS.SearchSpellingVariantV1,"SearchUnavailableV1":DEFINITIONS.SearchUnavailableV1,"SessionOrThreadId":DEFINITIONS.SessionOrThreadId,"SourceFreshness":DEFINITIONS.SourceFreshness,"SourceInstanceKey":DEFINITIONS.SourceInstanceKey,"SourceNamespace":DEFINITIONS.SourceNamespace,"SourceOccurrenceId":DEFINITIONS.SourceOccurrenceId,"UtcMicros":DEFINITIONS.UtcMicros},"$schema":"https://json-schema.org/draft/2020-12/schema","anyOf":[{"$ref":"#/$defs/SearchCompleteV1"},{"$ref":"#/$defs/SearchUnavailableV1"}],"description":"What a search served: a ranked page, or the typed reason no generation\ncould answer.","title":"SearchResultV1"}, {"$defs":{"PageRequest":DEFINITIONS.PageRequest,"ResultProjection":DEFINITIONS.ResultProjection,"RetrievalOrder":DEFINITIONS.RetrievalOrder,"RetrievalRequestMeta":DEFINITIONS.RetrievalRequestMeta,"SessionId":DEFINITIONS.SessionId,"TemporalModeV1":DEFINITIONS.TemporalModeV1,"UtcMicros":DEFINITIONS.UtcMicros},"$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"properties":{"meta":{"$ref":"#/$defs/RetrievalRequestMeta"},"session_id":{"$ref":"#/$defs/SessionId"}},"required":["session_id","meta"],"title":"SessionLookupRequest","type":"object"}, {"$defs":{"RetrievalAnchorId":DEFINITIONS.RetrievalAnchorId},"$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"properties":{"anchors":{"items":{"$ref":"#/$defs/RetrievalAnchorId"},"type":"array"}},"required":["anchors"],"title":"SessionLookupResult","type":"object"}, {"$defs":{"SessionRefreshFrontierV1":DEFINITIONS.SessionRefreshFrontierV1,"SessionRefreshGrainV1":DEFINITIONS.SessionRefreshGrainV1,"SessionRefreshScopeV1":DEFINITIONS.SessionRefreshScopeV1,"SessionRefreshSessionV1":DEFINITIONS.SessionRefreshSessionV1,"SessionRefreshSourceV1":DEFINITIONS.SessionRefreshSourceV1,"SessionRefreshTargetV1":DEFINITIONS.SessionRefreshTargetV1,"TemporalModeV1":DEFINITIONS.TemporalModeV1,"UtcMicros":DEFINITIONS.UtcMicros},"$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"description":"Exact route-selected session-refresh request body.\n\nEach current route selects the action itself; `scope` selects the mounted\nsession-store owner. Project-scoped requests are served under project-open\nadmission, profile-scoped requests by the authenticated profile authority.","properties":{"handle":{"type":["string","null"]},"scope":{"$ref":"#/$defs/SessionRefreshScopeV1"},"session":{"$ref":"#/$defs/SessionRefreshSessionV1"},"source":{"$ref":"#/$defs/SessionRefreshSourceV1"},"target":{"$ref":"#/$defs/SessionRefreshTargetV1"}},"required":["scope","session","source","target"],"title":"SessionRefreshActionRequestV1","type":"object"}, @@ -3757,412 +3801,416 @@ export const OPERATIONS = [ requestSchema: { schemaId: "schema.application.primitive.retrieve.request", revision: 1 }, resultSchema: { schemaId: "schema.application.primitive.retrieve.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":120000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","unavailable","partial"], decodeRequest: decoder(241), decodeResult: decoder(242) }, + { operation: "search", operationId: "operation.application.search", transport: { kind: "mcp_tool", toolName: "tracedecay_search" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.mcp.search.v1", + requestSchema: { schemaId: "schema.application.primitive.search.request", revision: 1 }, resultSchema: { schemaId: "schema.application.primitive.search.result", revision: 1 }, + cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":120000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","unavailable","partial"], + decodeRequest: decoder(243), decodeResult: decoder(244) }, { operation: "session_lookup", operationId: "operation.application.session_lookup", transport: { kind: "mcp_tool", toolName: "tracedecay_session_lookup" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.mcp.session_lookup.v1", requestSchema: { schemaId: "schema.application.primitive.session-lookup.request", revision: 1 }, resultSchema: { schemaId: "schema.application.primitive.session-lookup.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":10000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","unavailable","partial"], - decodeRequest: decoder(243), decodeResult: decoder(244) }, + decodeRequest: decoder(245), decodeResult: decoder(246) }, { operation: "application_session_refresh_begin", operationId: "operation.application.session_refresh_begin", transport: { kind: "http", route: "/application/retained/session_refresh_begin", method: "POST" }, effect: "administrative", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.session_refresh_begin.v1", requestSchema: { schemaId: "schema.application.retained.session-refresh-begin.request", revision: 1 }, resultSchema: { schemaId: "schema.application.retained.session-refresh-begin.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","reconciling","after_commit"]}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","cancelled","timed_out","failed","unavailable","effect_unknown","partial"], - decodeRequest: decoder(245), decodeResult: decoder(246), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.session_refresh_begin.v1", "schema.application.retained.session-refresh-begin.result", 1, ["completed","cancelled","timed_out","failed","unavailable","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","reconciling","after_commit"]}, "durable_effect", "required", decoder(246)) }, + decodeRequest: decoder(247), decodeResult: decoder(248), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.session_refresh_begin.v1", "schema.application.retained.session-refresh-begin.result", 1, ["completed","cancelled","timed_out","failed","unavailable","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","reconciling","after_commit"]}, "durable_effect", "required", decoder(248)) }, { operation: "application_session_refresh_cancel", operationId: "operation.application.session_refresh_cancel", transport: { kind: "http", route: "/application/retained/session_refresh_cancel", method: "POST" }, effect: "administrative", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.session_refresh_cancel.v1", requestSchema: { schemaId: "schema.application.retained.session-refresh-cancel.request", revision: 1 }, resultSchema: { schemaId: "schema.application.retained.session-refresh-cancel.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","reconciling","after_commit"]}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","cancelled","timed_out","failed","unavailable","effect_unknown","partial"], - decodeRequest: decoder(245), decodeResult: decoder(247), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.session_refresh_cancel.v1", "schema.application.retained.session-refresh-cancel.result", 1, ["completed","cancelled","timed_out","failed","unavailable","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","reconciling","after_commit"]}, "durable_effect", "required", decoder(247)) }, + decodeRequest: decoder(247), decodeResult: decoder(249), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.session_refresh_cancel.v1", "schema.application.retained.session-refresh-cancel.result", 1, ["completed","cancelled","timed_out","failed","unavailable","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","reconciling","after_commit"]}, "durable_effect", "required", decoder(249)) }, { operation: "application_session_refresh_status", operationId: "operation.application.session_refresh_status", transport: { kind: "http", route: "/application/retained/session_refresh_status", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.session_refresh_status.v1", requestSchema: { schemaId: "schema.application.retained.session-refresh-status.request", revision: 1 }, resultSchema: { schemaId: "schema.application.retained.session-refresh-status.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":30000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","unavailable","partial"], - decodeRequest: decoder(245), decodeResult: decoder(248), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.session_refresh_status.v1", "schema.application.retained.session-refresh-status.result", 1, ["completed","cancelled","timed_out","failed","unavailable","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(248)) }, + decodeRequest: decoder(247), decodeResult: decoder(250), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.session_refresh_status.v1", "schema.application.retained.session-refresh-status.result", 1, ["completed","cancelled","timed_out","failed","unavailable","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(250)) }, { operation: "application_sessions_for", operationId: "operation.application.sessions_for", transport: { kind: "http", route: "/application/retained/sessions_for", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.sessions_for.v1", requestSchema: { schemaId: "schema.application.retained.sessions-for.request", revision: 1 }, resultSchema: { schemaId: "schema.application.retained.sessions-for.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":30000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","unavailable","partial"], - decodeRequest: decoder(249), decodeResult: decoder(250), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.sessions_for.v1", "schema.application.retained.sessions-for.result", 1, ["completed","cancelled","timed_out","failed","unavailable","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(250)) }, + decodeRequest: decoder(251), decodeResult: decoder(252), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.sessions_for.v1", "schema.application.retained.sessions-for.result", 1, ["completed","cancelled","timed_out","failed","unavailable","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(252)) }, { operation: "signature", operationId: "operation.application.signature", transport: { kind: "mcp_tool", toolName: "tracedecay_signature" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.mcp.signature.v1", requestSchema: { schemaId: "schema.application.primitive.signature.request", revision: 1 }, resultSchema: { schemaId: "schema.application.primitive.signature.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":120000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","unavailable","partial"], - decodeRequest: decoder(102), decodeResult: decoder(251) }, + decodeRequest: decoder(102), decodeResult: decoder(253) }, { operation: "similar", operationId: "operation.application.similar", transport: { kind: "mcp_tool", toolName: "tracedecay_similar" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.mcp.similar.v1", requestSchema: { schemaId: "schema.application.primitive.similar.request", revision: 1 }, resultSchema: { schemaId: "schema.application.primitive.similar.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":10000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","unavailable","partial"], - decodeRequest: decoder(252), decodeResult: decoder(253) }, + decodeRequest: decoder(254), decodeResult: decoder(255) }, { operation: "skill_list", operationId: "operation.application.skill_list", transport: { kind: "mcp_tool", toolName: "tracedecay_skill_list" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.mcp.skill_list.v1", requestSchema: { schemaId: "schema.application.primitive.skill-list.request", revision: 1 }, resultSchema: { schemaId: "schema.application.primitive.skill-list.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":120000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","unavailable","partial"], - decodeRequest: decoder(254), decodeResult: decoder(255) }, + decodeRequest: decoder(256), decodeResult: decoder(257) }, { operation: "skill_view", operationId: "operation.application.skill_view", transport: { kind: "mcp_tool", toolName: "tracedecay_skill_view" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.mcp.skill_view.v1", requestSchema: { schemaId: "schema.application.primitive.skill-view.request", revision: 1 }, resultSchema: { schemaId: "schema.application.primitive.skill-view.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":120000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","unavailable","partial"], - decodeRequest: decoder(256), decodeResult: decoder(257) }, + decodeRequest: decoder(258), decodeResult: decoder(259) }, { operation: "application_source_body", operationId: "operation.application.source_body", transport: { kind: "http", route: "/application/primitives/source_body", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.source_body.v1", requestSchema: { schemaId: "schema.application.primitive.source-body.request", revision: 1 }, resultSchema: { schemaId: "schema.application.primitive.source-body.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":10000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","unavailable","partial"], - decodeRequest: decoder(258), decodeResult: decoder(259), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.source_body.v1", "schema.application.primitive.source-body.result", 1, ["completed","cancelled","timed_out","failed","unavailable","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(259)) }, + decodeRequest: decoder(260), decodeResult: decoder(261), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.source_body.v1", "schema.application.primitive.source-body.result", 1, ["completed","cancelled","timed_out","failed","unavailable","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(261)) }, { operation: "source_edit_reconcile", operationId: "operation.application.source_edit_reconcile", transport: { kind: "mcp_tool", toolName: "tracedecay_source_edit_reconcile" }, effect: "source_edit", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.mcp.source_edit_reconcile.v1", requestSchema: { schemaId: "schema.application.source-edit.reconcile.request", revision: 1 }, resultSchema: { schemaId: "schema.application.source-edit.reconcile.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","cancelled","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(260), decodeResult: decoder(10) }, + decodeRequest: decoder(262), decodeResult: decoder(10) }, { operation: "source_edit_rollback", operationId: "operation.application.source_edit_rollback", transport: { kind: "mcp_tool", toolName: "tracedecay_source_edit_rollback" }, effect: "source_edit", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.mcp.source_edit_rollback.v1", requestSchema: { schemaId: "schema.application.source-edit.rollback.request", revision: 1 }, resultSchema: { schemaId: "schema.application.source-edit.rollback.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","cancelled","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(261), decodeResult: decoder(10) }, + decodeRequest: decoder(263), decodeResult: decoder(10) }, { operation: "application_source_lines", operationId: "operation.application.source_lines", transport: { kind: "http", route: "/application/primitives/source_lines", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.source_lines.v1", requestSchema: { schemaId: "schema.application.primitive.source-lines.request", revision: 1 }, resultSchema: { schemaId: "schema.application.primitive.source-lines.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":10000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","unavailable","partial"], - decodeRequest: decoder(262), decodeResult: decoder(263), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.source_lines.v1", "schema.application.primitive.source-lines.result", 1, ["completed","cancelled","timed_out","failed","unavailable","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(263)) }, + decodeRequest: decoder(264), decodeResult: decoder(265), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.source_lines.v1", "schema.application.primitive.source-lines.result", 1, ["completed","cancelled","timed_out","failed","unavailable","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(265)) }, { operation: "application_source_outline", operationId: "operation.application.source_outline", transport: { kind: "http", route: "/application/primitives/source_outline", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.source_outline.v1", requestSchema: { schemaId: "schema.application.primitive.source-outline.request", revision: 1 }, resultSchema: { schemaId: "schema.application.primitive.source-outline.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":10000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","unavailable","partial"], - decodeRequest: decoder(264), decodeResult: decoder(265), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.source_outline.v1", "schema.application.primitive.source-outline.result", 1, ["completed","cancelled","timed_out","failed","unavailable","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(265)) }, + decodeRequest: decoder(266), decodeResult: decoder(267), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.source_outline.v1", "schema.application.primitive.source-outline.result", 1, ["completed","cancelled","timed_out","failed","unavailable","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(267)) }, { operation: "stack_snapshot", operationId: "operation.application.stack_snapshot", transport: { kind: "mcp_tool", toolName: "tracedecay_stack_snapshot" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.mcp.stack_snapshot.v1", requestSchema: { schemaId: "schema.application.native-integration.stack-snapshot.request", revision: 1 }, resultSchema: { schemaId: "schema.application.native-integration.stack-snapshot.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":30000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","partial"], - decodeRequest: decoder(266), decodeResult: decoder(7) }, + decodeRequest: decoder(268), decodeResult: decoder(7) }, { operation: "application_storage_status", operationId: "operation.application.storage_status", transport: { kind: "http", route: "/application/primitives/storage_status", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.storage_status.v1", requestSchema: { schemaId: "schema.application.primitive.storage-status.request", revision: 1 }, resultSchema: { schemaId: "schema.application.primitive.storage-status.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":10000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","unavailable","partial"], - decodeRequest: decoder(267), decodeResult: decoder(268), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.storage_status.v1", "schema.application.primitive.storage-status.result", 1, ["completed","cancelled","timed_out","failed","unavailable","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(268)) }, + decodeRequest: decoder(269), decodeResult: decoder(270), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.storage_status.v1", "schema.application.primitive.storage-status.result", 1, ["completed","cancelled","timed_out","failed","unavailable","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(270)) }, { operation: "str_replace", operationId: "operation.application.str_replace", transport: { kind: "mcp_tool", toolName: "tracedecay_str_replace" }, effect: "source_edit", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.mcp.str_replace.v1", requestSchema: { schemaId: "schema.application.source-edit.str-replace.request", revision: 1 }, resultSchema: { schemaId: "schema.application.source-edit.str-replace.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","cancelled","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(269), decodeResult: decoder(10) }, + decodeRequest: decoder(271), decodeResult: decoder(10) }, { operation: "test_map", operationId: "operation.application.test_map", transport: { kind: "mcp_tool", toolName: "tracedecay_test_map" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.mcp.test_map.v1", requestSchema: { schemaId: "schema.application.primitive.test-map.request", revision: 1 }, resultSchema: { schemaId: "schema.application.primitive.test-map.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":120000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","unavailable","partial"], - decodeRequest: decoder(270), decodeResult: decoder(271) }, + decodeRequest: decoder(272), decodeResult: decoder(273) }, { operation: "application_test_results", operationId: "operation.application.test_results", transport: { kind: "http", route: "/application/tests/results", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.test_results.v1", requestSchema: { schemaId: "schema.application.feedback.test-results.request", revision: 1 }, resultSchema: { schemaId: "schema.application.feedback.test-results.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":15000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","unavailable","partial"], - decodeRequest: decoder(272), decodeResult: decoder(273), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.test_results.v1", "schema.application.feedback.test-results.result", 1, ["completed","cancelled","timed_out","failed","unavailable","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(273)) }, + decodeRequest: decoder(274), decodeResult: decoder(275), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.test_results.v1", "schema.application.feedback.test-results.result", 1, ["completed","cancelled","timed_out","failed","unavailable","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(275)) }, { operation: "test_risk", operationId: "operation.application.test_risk", transport: { kind: "mcp_tool", toolName: "tracedecay_test_risk" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.mcp.test_risk.v1", requestSchema: { schemaId: "schema.application.primitive.test-risk.request", revision: 1 }, resultSchema: { schemaId: "schema.application.primitive.test-risk.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":120000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","unavailable","partial"], - decodeRequest: decoder(274), decodeResult: decoder(275) }, + decodeRequest: decoder(276), decodeResult: decoder(277) }, { operation: "todos", operationId: "operation.application.todos", transport: { kind: "mcp_tool", toolName: "tracedecay_todos" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.mcp.todos.v1", requestSchema: { schemaId: "schema.application.primitive.todos.request", revision: 1 }, resultSchema: { schemaId: "schema.application.primitive.todos.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":10000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","unavailable","partial"], - decodeRequest: decoder(276), decodeResult: decoder(277) }, + decodeRequest: decoder(278), decodeResult: decoder(279) }, { operation: "unmounted_files", operationId: "operation.application.unmounted_files", transport: { kind: "mcp_tool", toolName: "tracedecay_unmounted_files" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.mcp.unmounted_files.v1", requestSchema: { schemaId: "schema.application.primitive.unmounted-files.request", revision: 1 }, resultSchema: { schemaId: "schema.application.primitive.unmounted-files.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":120000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","unavailable","partial"], - decodeRequest: decoder(278), decodeResult: decoder(279) }, + decodeRequest: decoder(280), decodeResult: decoder(281) }, { operation: "unsafe_patterns", operationId: "operation.application.unsafe_patterns", transport: { kind: "mcp_tool", toolName: "tracedecay_unsafe_patterns" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.mcp.unsafe_patterns.v1", requestSchema: { schemaId: "schema.application.primitive.unsafe-patterns.request", revision: 1 }, resultSchema: { schemaId: "schema.application.primitive.unsafe-patterns.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":120000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","unavailable","partial"], - decodeRequest: decoder(280), decodeResult: decoder(281) }, + decodeRequest: decoder(282), decodeResult: decoder(283) }, { operation: "application_workflows", operationId: "operation.application.workflows", transport: { kind: "http", route: "/application/retained/workflows", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.workflows.v1", requestSchema: { schemaId: "schema.application.retained.workflows.request", revision: 1 }, resultSchema: { schemaId: "schema.application.retained.workflows.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":30000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","unavailable","partial"], - decodeRequest: decoder(282), decodeResult: decoder(283), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflows.v1", "schema.application.retained.workflows.result", 1, ["completed","cancelled","timed_out","failed","unavailable","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(283)) }, + decodeRequest: decoder(284), decodeResult: decoder(285), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflows.v1", "schema.application.retained.workflows.result", 1, ["completed","cancelled","timed_out","failed","unavailable","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(285)) }, { operation: "application_worktree_cleanup_confirm", operationId: "operation.application.worktree_cleanup_confirm", transport: { kind: "http", route: "/application/native-integration/worktree_cleanup_confirm", method: "POST" }, effect: "preview", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.worktree_cleanup_confirm.v1", requestSchema: { schemaId: "schema.application.native-integration.worktree-cleanup-confirm.request", revision: 1 }, resultSchema: { schemaId: "schema.application.native-integration.worktree-cleanup-confirm.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":30000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","partial"], - decodeRequest: decoder(284), decodeResult: decoder(7), + decodeRequest: decoder(286), decodeResult: decoder(7), decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.worktree_cleanup_confirm.v1", "schema.application.native-integration.worktree-cleanup-confirm.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(7)) }, { operation: "application_worktree_cleanup_inspect", operationId: "operation.application.worktree_cleanup_inspect", transport: { kind: "http", route: "/application/native-integration/worktree_cleanup_inspect", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.worktree_cleanup_inspect.v1", requestSchema: { schemaId: "schema.application.native-integration.worktree-cleanup-inspect.request", revision: 1 }, resultSchema: { schemaId: "schema.application.native-integration.worktree-cleanup-inspect.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":30000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","partial"], - decodeRequest: decoder(285), decodeResult: decoder(7), + decodeRequest: decoder(287), decodeResult: decoder(7), decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.worktree_cleanup_inspect.v1", "schema.application.native-integration.worktree-cleanup-inspect.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(7)) }, { operation: "application_worktree_cleanup_reconcile", operationId: "operation.application.worktree_cleanup_reconcile", transport: { kind: "http", route: "/application/native-integration/worktree_cleanup_reconcile", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.worktree_cleanup_reconcile.v1", requestSchema: { schemaId: "schema.application.native-integration.worktree-cleanup-reconcile.request", revision: 1 }, resultSchema: { schemaId: "schema.application.native-integration.worktree-cleanup-reconcile.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":30000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","partial"], - decodeRequest: decoder(286), decodeResult: decoder(7), + decodeRequest: decoder(288), decodeResult: decoder(7), decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.worktree_cleanup_reconcile.v1", "schema.application.native-integration.worktree-cleanup-reconcile.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(7)) }, { operation: "application_worktree_cleanup_remove", operationId: "operation.application.worktree_cleanup_remove", transport: { kind: "http", route: "/application/native-integration/worktree_cleanup_remove", method: "POST" }, effect: "administrative", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.worktree_cleanup_remove.v1", requestSchema: { schemaId: "schema.application.native-integration.worktree-cleanup-remove.request", revision: 1 }, resultSchema: { schemaId: "schema.application.native-integration.worktree-cleanup-remove.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","cancelled","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(287), decodeResult: decoder(7), + decodeRequest: decoder(289), decodeResult: decoder(7), decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.worktree_cleanup_remove.v1", "schema.application.native-integration.worktree-cleanup-remove.result", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(7)) }, { operation: "application_worktree_inventory", operationId: "operation.application.worktree_inventory", transport: { kind: "http", route: "/application/native-integration/worktree_inventory", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.worktree_inventory.v1", requestSchema: { schemaId: "schema.application.native-integration.worktree-inventory.request", revision: 1 }, resultSchema: { schemaId: "schema.application.native-integration.worktree-inventory.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":30000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","partial"], - decodeRequest: decoder(288), decodeResult: decoder(7), + decodeRequest: decoder(290), decodeResult: decoder(7), decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.worktree_inventory.v1", "schema.application.native-integration.worktree-inventory.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(7)) }, { operation: "handoff_issue_task_handoff", operationId: "operation.handoff.issue_task_handoff", transport: { kind: "http", route: "/application/handoff/issue-task", method: "POST" }, effect: "administrative", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.handoff.issue_task_handoff", requestSchema: { schemaId: "schema.handoff.issue_task_handoff.request", revision: 1 }, resultSchema: { schemaId: "schema.handoff.issue_task_handoff.result", revision: 1 }, cancellation: {"mode":"not_cancellable"}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(289), decodeResult: decoder(290), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.handoff.issue_task_handoff", "schema.handoff.issue_task_handoff.result", 1, ["completed","timed_out","failed","effect_unknown","partial"], {"mode":"not_cancellable"}, "durable_effect", "required", decoder(290)) }, + decodeRequest: decoder(291), decodeResult: decoder(292), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.handoff.issue_task_handoff", "schema.handoff.issue_task_handoff.result", 1, ["completed","timed_out","failed","effect_unknown","partial"], {"mode":"not_cancellable"}, "durable_effect", "required", decoder(292)) }, { operation: "handoff_list_task_handoffs", operationId: "operation.handoff.list_task_handoffs", transport: { kind: "http", route: "/application/handoff/list-task", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.handoff.list_task_handoffs", requestSchema: { schemaId: "schema.handoff.list_task_handoffs.request", revision: 1 }, resultSchema: { schemaId: "schema.handoff.list_task_handoffs.result", revision: 1 }, cancellation: {"mode":"not_cancellable"}, deadline: {"behavior":"return_operation_receipt","maximum_millis":30000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","timed_out","failed","partial"], - decodeRequest: decoder(291), decodeResult: decoder(292), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.handoff.list_task_handoffs", "schema.handoff.list_task_handoffs.result", 1, ["completed","timed_out","failed","partial"], {"mode":"not_cancellable"}, "operation", "not_required", decoder(292)) }, + decodeRequest: decoder(293), decodeResult: decoder(294), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.handoff.list_task_handoffs", "schema.handoff.list_task_handoffs.result", 1, ["completed","timed_out","failed","partial"], {"mode":"not_cancellable"}, "operation", "not_required", decoder(294)) }, { operation: "handoff_open_investigation_handoff", operationId: "operation.handoff.open_investigation_handoff", transport: { kind: "http", route: "/application/handoff/open-investigation", method: "POST" }, effect: "administrative", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.handoff.open_investigation_handoff", requestSchema: { schemaId: "schema.handoff.open_investigation_handoff.request", revision: 1 }, resultSchema: { schemaId: "schema.handoff.open_investigation_handoff.result", revision: 1 }, cancellation: {"mode":"not_cancellable"}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(293), decodeResult: decoder(294), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.handoff.open_investigation_handoff", "schema.handoff.open_investigation_handoff.result", 1, ["completed","timed_out","failed","effect_unknown","partial"], {"mode":"not_cancellable"}, "durable_effect", "required", decoder(294)) }, + decodeRequest: decoder(295), decodeResult: decoder(296), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.handoff.open_investigation_handoff", "schema.handoff.open_investigation_handoff.result", 1, ["completed","timed_out","failed","effect_unknown","partial"], {"mode":"not_cancellable"}, "durable_effect", "required", decoder(296)) }, { operation: "handoff_open_task_handoff", operationId: "operation.handoff.open_task_handoff", transport: { kind: "http", route: "/application/handoff/open-task", method: "POST" }, effect: "administrative", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.handoff.open_task_handoff", requestSchema: { schemaId: "schema.handoff.open_task_handoff.request", revision: 1 }, resultSchema: { schemaId: "schema.handoff.open_task_handoff.result", revision: 1 }, cancellation: {"mode":"not_cancellable"}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(295), decodeResult: decoder(296), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.handoff.open_task_handoff", "schema.handoff.open_task_handoff.result", 1, ["completed","timed_out","failed","effect_unknown","partial"], {"mode":"not_cancellable"}, "durable_effect", "required", decoder(296)) }, + decodeRequest: decoder(297), decodeResult: decoder(298), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.handoff.open_task_handoff", "schema.handoff.open_task_handoff.result", 1, ["completed","timed_out","failed","effect_unknown","partial"], {"mode":"not_cancellable"}, "durable_effect", "required", decoder(298)) }, { operation: "multi_root_execute", operationId: "operation.multi_root.execute", transport: { kind: "http", route: "/application/multi-root/execute", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.multi_root.execute.v1", requestSchema: { schemaId: "schema.tracedecay.multi-root.execute-request.v1", revision: 1 }, resultSchema: { schemaId: "schema.tracedecay.multi-root.execute-result.v1", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":30000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","partial"], - decodeRequest: decoder(297), decodeResult: decoder(298), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.multi_root.execute.v1", "schema.tracedecay.multi-root.execute-result.v1", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(298)) }, + decodeRequest: decoder(299), decodeResult: decoder(300), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.multi_root.execute.v1", "schema.tracedecay.multi-root.execute-result.v1", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(300)) }, { operation: "multi_root_scope_set_compare_and_swap", operationId: "operation.multi_root.scope_set_compare_and_swap", transport: { kind: "http", route: "/application/multi-root/scope-set/compare-and-swap", method: "POST" }, effect: "administrative", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.multi_root.scope_set_compare_and_swap.v1", requestSchema: { schemaId: "schema.tracedecay.multi-root.scope-set-compare-and-swap-request.v1", revision: 1 }, resultSchema: { schemaId: "schema.tracedecay.multi-root.scope-set-compare-and-swap-result.v1", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","cancelled","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(299), decodeResult: decoder(300), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.multi_root.scope_set_compare_and_swap.v1", "schema.tracedecay.multi-root.scope-set-compare-and-swap-result.v1", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(300)) }, + decodeRequest: decoder(301), decodeResult: decoder(302), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.multi_root.scope_set_compare_and_swap.v1", "schema.tracedecay.multi-root.scope-set-compare-and-swap-result.v1", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(302)) }, { operation: "multi_root_scope_set_read", operationId: "operation.multi_root.scope_set_read", transport: { kind: "http", route: "/application/multi-root/scope-set/read", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.multi_root.scope_set_read.v1", requestSchema: { schemaId: "schema.tracedecay.multi-root.scope-set-read-request.v1", revision: 1 }, resultSchema: { schemaId: "schema.tracedecay.multi-root.scope-set-read-result.v1", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":30000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","partial"], - decodeRequest: decoder(301), decodeResult: decoder(302), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.multi_root.scope_set_read.v1", "schema.tracedecay.multi-root.scope-set-read-result.v1", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(302)) }, + decodeRequest: decoder(303), decodeResult: decoder(304), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.multi_root.scope_set_read.v1", "schema.tracedecay.multi-root.scope-set-read-result.v1", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(304)) }, { operation: "work_accept_proposal", operationId: "operation.work.accept_proposal", transport: { kind: "http", route: "/application/work/accept-proposal", method: "POST" }, effect: "administrative", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.work.accept_proposal", requestSchema: { schemaId: "schema.work.accept_proposal.request", revision: 1 }, resultSchema: { schemaId: "schema.work.accept_proposal.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","cancelled","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(303), decodeResult: decoder(304), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.accept_proposal", "schema.work.accept_proposal.result", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(304)) }, + decodeRequest: decoder(305), decodeResult: decoder(306), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.accept_proposal", "schema.work.accept_proposal.result", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(306)) }, { operation: "work_adjudicate_duplicate", operationId: "operation.work.adjudicate_duplicate", transport: { kind: "http", route: "/application/work/adjudicate-duplicate", method: "POST" }, effect: "administrative", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.work.adjudicate_duplicate", requestSchema: { schemaId: "schema.work.adjudicate_duplicate.request", revision: 1 }, resultSchema: { schemaId: "schema.work.adjudicate_duplicate.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","cancelled","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(305), decodeResult: decoder(306), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.adjudicate_duplicate", "schema.work.adjudicate_duplicate.result", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(306)) }, + decodeRequest: decoder(307), decodeResult: decoder(308), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.adjudicate_duplicate", "schema.work.adjudicate_duplicate.result", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(308)) }, { operation: "work_adjudicate_leak", operationId: "operation.work.adjudicate_leak", transport: { kind: "http", route: "/application/work/adjudicate-leak", method: "POST" }, effect: "administrative", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.work.adjudicate_leak", requestSchema: { schemaId: "schema.work.adjudicate_leak.request", revision: 1 }, resultSchema: { schemaId: "schema.work.adjudicate_leak.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","cancelled","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(307), decodeResult: decoder(308), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.adjudicate_leak", "schema.work.adjudicate_leak.result", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(308)) }, + decodeRequest: decoder(309), decodeResult: decoder(310), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.adjudicate_leak", "schema.work.adjudicate_leak.result", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(310)) }, { operation: "work_admit_execution", operationId: "operation.work.admit_execution", transport: { kind: "http", route: "/application/work/admit-execution", method: "POST" }, effect: "administrative", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.work.admit_execution", requestSchema: { schemaId: "schema.work.admit_execution.request", revision: 1 }, resultSchema: { schemaId: "schema.work.admit_execution.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","cancelled","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(309), decodeResult: decoder(310), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.admit_execution", "schema.work.admit_execution.result", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(310)) }, + decodeRequest: decoder(311), decodeResult: decoder(312), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.admit_execution", "schema.work.admit_execution.result", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(312)) }, { operation: "work_admit_placement", operationId: "operation.work.admit_placement", transport: { kind: "http", route: "/application/work/admit-placement", method: "POST" }, effect: "administrative", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.work.admit_placement", requestSchema: { schemaId: "schema.work.admit_placement.request", revision: 1 }, resultSchema: { schemaId: "schema.work.admit_placement.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","cancelled","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(311), decodeResult: decoder(312), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.admit_placement", "schema.work.admit_placement.result", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(312)) }, + decodeRequest: decoder(313), decodeResult: decoder(314), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.admit_placement", "schema.work.admit_placement.result", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(314)) }, { operation: "work_attempt_status", operationId: "operation.work.attempt_status", transport: { kind: "http", route: "/application/work/attempt-status", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.work.attempt_status", requestSchema: { schemaId: "schema.work.attempt_status.request", revision: 1 }, resultSchema: { schemaId: "schema.work.attempt_status.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":30000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","partial"], - decodeRequest: decoder(313), decodeResult: decoder(314), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.attempt_status", "schema.work.attempt_status.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(314)) }, + decodeRequest: decoder(315), decodeResult: decoder(316), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.attempt_status", "schema.work.attempt_status.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(316)) }, { operation: "work_cancel_attempt", operationId: "operation.work.cancel_attempt", transport: { kind: "http", route: "/application/work/cancel-attempt", method: "POST" }, effect: "administrative", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.work.cancel_attempt", requestSchema: { schemaId: "schema.work.cancel_attempt.request", revision: 1 }, resultSchema: { schemaId: "schema.work.cancel_attempt.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","cancelled","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(315), decodeResult: decoder(314), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.cancel_attempt", "schema.work.cancel_attempt.result", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(314)) }, + decodeRequest: decoder(317), decodeResult: decoder(316), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.cancel_attempt", "schema.work.cancel_attempt.result", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(316)) }, { operation: "work_compare_proposal", operationId: "operation.work.compare_proposal", transport: { kind: "http", route: "/application/work/compare-proposal", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.work.compare_proposal", requestSchema: { schemaId: "schema.work.compare_proposal.request", revision: 1 }, resultSchema: { schemaId: "schema.work.compare_proposal.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":30000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","partial"], - decodeRequest: decoder(316), decodeResult: decoder(317), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.compare_proposal", "schema.work.compare_proposal.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(317)) }, + decodeRequest: decoder(318), decodeResult: decoder(319), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.compare_proposal", "schema.work.compare_proposal.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(319)) }, { operation: "work_create", operationId: "operation.work.create", transport: { kind: "http", route: "/application/work/create", method: "POST" }, effect: "administrative", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.work.create", requestSchema: { schemaId: "schema.work.create.request", revision: 1 }, resultSchema: { schemaId: "schema.work.create.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","cancelled","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(318), decodeResult: decoder(304), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.create", "schema.work.create.result", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(304)) }, + decodeRequest: decoder(320), decodeResult: decoder(306), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.create", "schema.work.create.result", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(306)) }, { operation: "work_execution_history", operationId: "operation.work.execution_history", transport: { kind: "http", route: "/application/work/execution-history", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.work.execution_history", requestSchema: { schemaId: "schema.work.execution_history.request", revision: 1 }, resultSchema: { schemaId: "schema.work.execution_history.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":30000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","partial"], - decodeRequest: decoder(319), decodeResult: decoder(320), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.execution_history", "schema.work.execution_history.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(320)) }, + decodeRequest: decoder(321), decodeResult: decoder(322), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.execution_history", "schema.work.execution_history.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(322)) }, { operation: "work_experience", operationId: "operation.work.experience", transport: { kind: "http", route: "/application/work/experience", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.work.experience", requestSchema: { schemaId: "schema.work.experience.request", revision: 1 }, resultSchema: { schemaId: "schema.work.experience.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":30000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","partial"], - decodeRequest: decoder(321), decodeResult: decoder(322), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.experience", "schema.work.experience.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(322)) }, + decodeRequest: decoder(323), decodeResult: decoder(324), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.experience", "schema.work.experience.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(324)) }, { operation: "work_generate_proposal", operationId: "operation.work.generate_proposal", transport: { kind: "http", route: "/application/work/generate-proposal", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.work.generate_proposal", requestSchema: { schemaId: "schema.work.generate_proposal.request", revision: 1 }, resultSchema: { schemaId: "schema.work.generate_proposal.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":30000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","partial"], - decodeRequest: decoder(323), decodeResult: decoder(324), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.generate_proposal", "schema.work.generate_proposal.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(324)) }, + decodeRequest: decoder(325), decodeResult: decoder(326), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.generate_proposal", "schema.work.generate_proposal.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(326)) }, { operation: "work_hydrate_artifacts", operationId: "operation.work.hydrate_artifacts", transport: { kind: "http", route: "/application/work/hydrate-artifacts", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.work.hydrate_artifacts", requestSchema: { schemaId: "schema.work.hydrate_artifacts.request", revision: 1 }, resultSchema: { schemaId: "schema.work.hydrate_artifacts.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":30000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","partial"], - decodeRequest: decoder(325), decodeResult: decoder(326), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.hydrate_artifacts", "schema.work.hydrate_artifacts.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(326)) }, + decodeRequest: decoder(327), decodeResult: decoder(328), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.hydrate_artifacts", "schema.work.hydrate_artifacts.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(328)) }, { operation: "work_list_attempts", operationId: "operation.work.list_attempts", transport: { kind: "http", route: "/application/work/list-attempts", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.work.list_attempts", requestSchema: { schemaId: "schema.work.list_attempts.request", revision: 1 }, resultSchema: { schemaId: "schema.work.list_attempts.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":30000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","partial"], - decodeRequest: decoder(319), decodeResult: decoder(327), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.list_attempts", "schema.work.list_attempts.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(327)) }, + decodeRequest: decoder(321), decodeResult: decoder(329), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.list_attempts", "schema.work.list_attempts.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(329)) }, { operation: "work_mutate_graph", operationId: "operation.work.mutate_graph", transport: { kind: "http", route: "/application/work/mutate-graph", method: "POST" }, effect: "administrative", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.work.mutate_graph", requestSchema: { schemaId: "schema.work.mutate_graph.request", revision: 1 }, resultSchema: { schemaId: "schema.work.mutate_graph.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","cancelled","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(328), decodeResult: decoder(304), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.mutate_graph", "schema.work.mutate_graph.result", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(304)) }, + decodeRequest: decoder(330), decodeResult: decoder(306), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.mutate_graph", "schema.work.mutate_graph.result", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(306)) }, { operation: "work_pause_run", operationId: "operation.work.pause_run", transport: { kind: "http", route: "/application/work/pause-run", method: "POST" }, effect: "administrative", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.work.pause_run", requestSchema: { schemaId: "schema.work.pause_run.request", revision: 1 }, resultSchema: { schemaId: "schema.work.pause_run.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","cancelled","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(329), decodeResult: decoder(330), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.pause_run", "schema.work.pause_run.result", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(330)) }, + decodeRequest: decoder(331), decodeResult: decoder(332), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.pause_run", "schema.work.pause_run.result", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(332)) }, { operation: "work_placement_preflight", operationId: "operation.work.placement_preflight", transport: { kind: "http", route: "/application/work/placement-preflight", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.work.placement_preflight", requestSchema: { schemaId: "schema.work.placement_preflight.request", revision: 1 }, resultSchema: { schemaId: "schema.work.placement_preflight.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":30000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","partial"], - decodeRequest: decoder(331), decodeResult: decoder(332), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.placement_preflight", "schema.work.placement_preflight.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(332)) }, + decodeRequest: decoder(333), decodeResult: decoder(334), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.placement_preflight", "schema.work.placement_preflight.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(334)) }, { operation: "work_placement_status", operationId: "operation.work.placement_status", transport: { kind: "http", route: "/application/work/placement-status", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.work.placement_status", requestSchema: { schemaId: "schema.work.placement_status.request", revision: 1 }, resultSchema: { schemaId: "schema.work.placement_status.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":30000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","partial"], - decodeRequest: decoder(333), decodeResult: decoder(334), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.placement_status", "schema.work.placement_status.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(334)) }, + decodeRequest: decoder(335), decodeResult: decoder(336), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.placement_status", "schema.work.placement_status.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(336)) }, { operation: "work_prepare_duplicate_adjudication", operationId: "operation.work.prepare_duplicate_adjudication", transport: { kind: "http", route: "/application/work/prepare-duplicate-adjudication", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.work.prepare_duplicate_adjudication", requestSchema: { schemaId: "schema.work.prepare_duplicate_adjudication.request", revision: 1 }, resultSchema: { schemaId: "schema.work.prepare_duplicate_adjudication.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":30000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","partial"], - decodeRequest: decoder(335), decodeResult: decoder(305), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.prepare_duplicate_adjudication", "schema.work.prepare_duplicate_adjudication.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(305)) }, + decodeRequest: decoder(337), decodeResult: decoder(307), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.prepare_duplicate_adjudication", "schema.work.prepare_duplicate_adjudication.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(307)) }, { operation: "work_prepare_graph_mutation", operationId: "operation.work.prepare_graph_mutation", transport: { kind: "http", route: "/application/work/prepare-graph-mutation", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.work.prepare_graph_mutation", requestSchema: { schemaId: "schema.work.prepare_graph_mutation.request", revision: 1 }, resultSchema: { schemaId: "schema.work.prepare_graph_mutation.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":30000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","partial"], - decodeRequest: decoder(336), decodeResult: decoder(328), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.prepare_graph_mutation", "schema.work.prepare_graph_mutation.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(328)) }, + decodeRequest: decoder(338), decodeResult: decoder(330), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.prepare_graph_mutation", "schema.work.prepare_graph_mutation.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(330)) }, { operation: "work_release_placement", operationId: "operation.work.release_placement", transport: { kind: "http", route: "/application/work/release-placement", method: "POST" }, effect: "administrative", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.work.release_placement", requestSchema: { schemaId: "schema.work.release_placement.request", revision: 1 }, resultSchema: { schemaId: "schema.work.release_placement.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","cancelled","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(337), decodeResult: decoder(312), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.release_placement", "schema.work.release_placement.result", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(312)) }, + decodeRequest: decoder(339), decodeResult: decoder(314), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.release_placement", "schema.work.release_placement.result", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(314)) }, { operation: "work_resume_attempts", operationId: "operation.work.resume_attempts", transport: { kind: "http", route: "/application/work/resume-attempts", method: "POST" }, effect: "administrative", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.work.resume_attempts", requestSchema: { schemaId: "schema.work.resume_attempts.request", revision: 1 }, resultSchema: { schemaId: "schema.work.resume_attempts.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","cancelled","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(338), decodeResult: decoder(339), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.resume_attempts", "schema.work.resume_attempts.result", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(339)) }, + decodeRequest: decoder(340), decodeResult: decoder(341), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.resume_attempts", "schema.work.resume_attempts.result", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(341)) }, { operation: "work_resume_run", operationId: "operation.work.resume_run", transport: { kind: "http", route: "/application/work/resume-run", method: "POST" }, effect: "administrative", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.work.resume_run", requestSchema: { schemaId: "schema.work.resume_run.request", revision: 1 }, resultSchema: { schemaId: "schema.work.resume_run.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","cancelled","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(340), decodeResult: decoder(330), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.resume_run", "schema.work.resume_run.result", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(330)) }, + decodeRequest: decoder(342), decodeResult: decoder(332), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.resume_run", "schema.work.resume_run.result", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(332)) }, { operation: "work_retrieve_evidence", operationId: "operation.work.retrieve_evidence", transport: { kind: "http", route: "/application/work/retrieve-evidence", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.work.retrieve_evidence", requestSchema: { schemaId: "schema.work.retrieve_evidence.request", revision: 1 }, resultSchema: { schemaId: "schema.work.retrieve_evidence.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":30000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","partial"], - decodeRequest: decoder(341), decodeResult: decoder(342), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.retrieve_evidence", "schema.work.retrieve_evidence.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(342)) }, + decodeRequest: decoder(343), decodeResult: decoder(344), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.retrieve_evidence", "schema.work.retrieve_evidence.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(344)) }, { operation: "work_retry_attempt", operationId: "operation.work.retry_attempt", transport: { kind: "http", route: "/application/work/retry-attempt", method: "POST" }, effect: "administrative", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.work.retry_attempt", requestSchema: { schemaId: "schema.work.retry_attempt.request", revision: 1 }, resultSchema: { schemaId: "schema.work.retry_attempt.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","cancelled","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(343), decodeResult: decoder(344), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.retry_attempt", "schema.work.retry_attempt.result", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(344)) }, + decodeRequest: decoder(345), decodeResult: decoder(346), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.retry_attempt", "schema.work.retry_attempt.result", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(346)) }, { operation: "work_review_proposal", operationId: "operation.work.review_proposal", transport: { kind: "http", route: "/application/work/review-proposal", method: "POST" }, effect: "administrative", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.work.review_proposal", requestSchema: { schemaId: "schema.work.review_proposal.request", revision: 1 }, resultSchema: { schemaId: "schema.work.review_proposal.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","cancelled","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(345), decodeResult: decoder(304), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.review_proposal", "schema.work.review_proposal.result", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(304)) }, + decodeRequest: decoder(347), decodeResult: decoder(306), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.review_proposal", "schema.work.review_proposal.result", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(306)) }, { operation: "work_run_control", operationId: "operation.work.run_control", transport: { kind: "http", route: "/application/work/run-control", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.work.run_control", requestSchema: { schemaId: "schema.work.run_control.request", revision: 1 }, resultSchema: { schemaId: "schema.work.run_control.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":30000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","partial"], - decodeRequest: decoder(346), decodeResult: decoder(347), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.run_control", "schema.work.run_control.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(347)) }, + decodeRequest: decoder(348), decodeResult: decoder(349), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.run_control", "schema.work.run_control.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(349)) }, { operation: "work_start_attempt", operationId: "operation.work.start_attempt", transport: { kind: "http", route: "/application/work/start-attempt", method: "POST" }, effect: "administrative", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.work.start_attempt", requestSchema: { schemaId: "schema.work.start_attempt.request", revision: 1 }, resultSchema: { schemaId: "schema.work.start_attempt.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","cancelled","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(348), decodeResult: decoder(314), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.start_attempt", "schema.work.start_attempt.result", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(314)) }, + decodeRequest: decoder(350), decodeResult: decoder(316), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.start_attempt", "schema.work.start_attempt.result", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(316)) }, { operation: "work_synthesize", operationId: "operation.work.synthesize", transport: { kind: "http", route: "/application/work/synthesize", method: "POST" }, effect: "administrative", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.work.synthesize", requestSchema: { schemaId: "schema.work.synthesize.request", revision: 1 }, resultSchema: { schemaId: "schema.work.synthesize.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","cancelled","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(349), decodeResult: decoder(350), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.synthesize", "schema.work.synthesize.result", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(350)) }, + decodeRequest: decoder(351), decodeResult: decoder(352), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.synthesize", "schema.work.synthesize.result", 1, ["completed","cancelled","timed_out","failed","effect_unknown","partial"], {"mode":"cooperative","points":["before_admission","before_effect","effect_in_flight","after_commit"]}, "durable_effect", "required", decoder(352)) }, { operation: "work_topology", operationId: "operation.work.topology", transport: { kind: "http", route: "/application/work/topology", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.work.topology", requestSchema: { schemaId: "schema.work.topology.request", revision: 1 }, resultSchema: { schemaId: "schema.work.topology.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":30000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","partial"], - decodeRequest: decoder(351), decodeResult: decoder(352), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.topology", "schema.work.topology.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(352)) }, + decodeRequest: decoder(353), decodeResult: decoder(354), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.topology", "schema.work.topology.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(354)) }, { operation: "work_topology_metrics", operationId: "operation.work.topology_metrics", transport: { kind: "http", route: "/application/work/topology-metrics", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.work.topology_metrics", requestSchema: { schemaId: "schema.work.topology_metrics.request", revision: 1 }, resultSchema: { schemaId: "schema.work.topology_metrics.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":30000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","partial"], - decodeRequest: decoder(353), decodeResult: decoder(354), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.topology_metrics", "schema.work.topology_metrics.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(354)) }, + decodeRequest: decoder(355), decodeResult: decoder(356), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.topology_metrics", "schema.work.topology_metrics.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(356)) }, { operation: "work_views", operationId: "operation.work.views", transport: { kind: "http", route: "/application/work/views", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.work.views", requestSchema: { schemaId: "schema.work.views.request", revision: 1 }, resultSchema: { schemaId: "schema.work.views.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":30000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","partial"], - decodeRequest: decoder(355), decodeResult: decoder(356), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.views", "schema.work.views.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(356)) }, + decodeRequest: decoder(357), decodeResult: decoder(358), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.work.views", "schema.work.views.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(358)) }, { operation: "workflow_activate_definition", operationId: "operation.workflow.activate_definition", transport: { kind: "http", route: "/application/workflow/activate-definition", method: "POST" }, effect: "administrative", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.workflow.activate_definition", requestSchema: { schemaId: "schema.workflow.activate_definition.request", revision: 1 }, resultSchema: { schemaId: "schema.workflow.activate_definition.result", revision: 1 }, cancellation: {"mode":"not_cancellable"}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(357), decodeResult: decoder(358), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflow.activate_definition", "schema.workflow.activate_definition.result", 1, ["completed","timed_out","failed","effect_unknown","partial"], {"mode":"not_cancellable"}, "durable_effect", "required", decoder(358)) }, + decodeRequest: decoder(359), decodeResult: decoder(360), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflow.activate_definition", "schema.workflow.activate_definition.result", 1, ["completed","timed_out","failed","effect_unknown","partial"], {"mode":"not_cancellable"}, "durable_effect", "required", decoder(360)) }, { operation: "workflow_cancel_run", operationId: "operation.workflow.cancel_run", transport: { kind: "http", route: "/application/workflow/cancel-run", method: "POST" }, effect: "administrative", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.workflow.cancel_run", requestSchema: { schemaId: "schema.workflow.cancel_run.request", revision: 1 }, resultSchema: { schemaId: "schema.workflow.cancel_run.result", revision: 1 }, cancellation: {"mode":"not_cancellable"}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(359), decodeResult: decoder(360), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflow.cancel_run", "schema.workflow.cancel_run.result", 1, ["completed","timed_out","failed","effect_unknown","partial"], {"mode":"not_cancellable"}, "durable_effect", "required", decoder(360)) }, + decodeRequest: decoder(361), decodeResult: decoder(362), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflow.cancel_run", "schema.workflow.cancel_run.result", 1, ["completed","timed_out","failed","effect_unknown","partial"], {"mode":"not_cancellable"}, "durable_effect", "required", decoder(362)) }, { operation: "workflow_definition_history", operationId: "operation.workflow.definition_history", transport: { kind: "http", route: "/application/workflow/definition-history", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.workflow.definition_history", requestSchema: { schemaId: "schema.workflow.definition_history.request", revision: 1 }, resultSchema: { schemaId: "schema.workflow.definition_history.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":30000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","partial"], - decodeRequest: decoder(361), decodeResult: decoder(362), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflow.definition_history", "schema.workflow.definition_history.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(362)) }, + decodeRequest: decoder(363), decodeResult: decoder(364), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflow.definition_history", "schema.workflow.definition_history.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(364)) }, { operation: "workflow_diff_definition", operationId: "operation.workflow.diff_definition", transport: { kind: "http", route: "/application/workflow/diff-definition", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.workflow.diff_definition", requestSchema: { schemaId: "schema.workflow.diff_definition.request", revision: 1 }, resultSchema: { schemaId: "schema.workflow.diff_definition.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":30000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","partial"], - decodeRequest: decoder(363), decodeResult: decoder(364), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflow.diff_definition", "schema.workflow.diff_definition.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(364)) }, + decodeRequest: decoder(365), decodeResult: decoder(366), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflow.diff_definition", "schema.workflow.diff_definition.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(366)) }, { operation: "workflow_get_definition", operationId: "operation.workflow.get_definition", transport: { kind: "http", route: "/application/workflow/get-definition", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.workflow.get_definition", requestSchema: { schemaId: "schema.workflow.get_definition.request", revision: 1 }, resultSchema: { schemaId: "schema.workflow.get_definition.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":30000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","partial"], - decodeRequest: decoder(365), decodeResult: decoder(366), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflow.get_definition", "schema.workflow.get_definition.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(366)) }, + decodeRequest: decoder(367), decodeResult: decoder(368), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflow.get_definition", "schema.workflow.get_definition.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(368)) }, { operation: "workflow_get_run", operationId: "operation.workflow.get_run", transport: { kind: "http", route: "/application/workflow/get-run", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.workflow.get_run", requestSchema: { schemaId: "schema.workflow.get_run.request", revision: 1 }, resultSchema: { schemaId: "schema.workflow.get_run.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":30000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","partial"], - decodeRequest: decoder(367), decodeResult: decoder(360), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflow.get_run", "schema.workflow.get_run.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(360)) }, + decodeRequest: decoder(369), decodeResult: decoder(362), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflow.get_run", "schema.workflow.get_run.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(362)) }, { operation: "workflow_handoff_issue", operationId: "operation.workflow.handoff_issue", transport: { kind: "http", route: "/application/workflow/handoff-issue", method: "POST" }, effect: "administrative", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.workflow.handoff_issue", requestSchema: { schemaId: "schema.workflow.handoff_issue.request", revision: 1 }, resultSchema: { schemaId: "schema.workflow.handoff_issue.result", revision: 1 }, cancellation: {"mode":"not_cancellable"}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(368), decodeResult: decoder(369), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflow.handoff_issue", "schema.workflow.handoff_issue.result", 1, ["completed","timed_out","failed","effect_unknown","partial"], {"mode":"not_cancellable"}, "durable_effect", "required", decoder(369)) }, + decodeRequest: decoder(370), decodeResult: decoder(371), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflow.handoff_issue", "schema.workflow.handoff_issue.result", 1, ["completed","timed_out","failed","effect_unknown","partial"], {"mode":"not_cancellable"}, "durable_effect", "required", decoder(371)) }, { operation: "workflow_handoff_redeem", operationId: "operation.workflow.handoff_redeem", transport: { kind: "http", route: "/application/workflow/handoff-redeem", method: "POST" }, effect: "administrative", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.workflow.handoff_redeem", requestSchema: { schemaId: "schema.workflow.handoff_redeem.request", revision: 1 }, resultSchema: { schemaId: "schema.workflow.handoff_redeem.result", revision: 1 }, cancellation: {"mode":"not_cancellable"}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(370), decodeResult: decoder(371), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflow.handoff_redeem", "schema.workflow.handoff_redeem.result", 1, ["completed","timed_out","failed","effect_unknown","partial"], {"mode":"not_cancellable"}, "durable_effect", "required", decoder(371)) }, + decodeRequest: decoder(372), decodeResult: decoder(373), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflow.handoff_redeem", "schema.workflow.handoff_redeem.result", 1, ["completed","timed_out","failed","effect_unknown","partial"], {"mode":"not_cancellable"}, "durable_effect", "required", decoder(373)) }, { operation: "workflow_list_definitions", operationId: "operation.workflow.list_definitions", transport: { kind: "http", route: "/application/workflow/list-definitions", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.workflow.list_definitions", requestSchema: { schemaId: "schema.workflow.list_definitions.request", revision: 1 }, resultSchema: { schemaId: "schema.workflow.list_definitions.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":30000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","partial"], - decodeRequest: decoder(372), decodeResult: decoder(362), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflow.list_definitions", "schema.workflow.list_definitions.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(362)) }, + decodeRequest: decoder(374), decodeResult: decoder(364), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflow.list_definitions", "schema.workflow.list_definitions.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(364)) }, { operation: "workflow_pause_run", operationId: "operation.workflow.pause_run", transport: { kind: "http", route: "/application/workflow/pause-run", method: "POST" }, effect: "administrative", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.workflow.pause_run", requestSchema: { schemaId: "schema.workflow.pause_run.request", revision: 1 }, resultSchema: { schemaId: "schema.workflow.pause_run.result", revision: 1 }, cancellation: {"mode":"not_cancellable"}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(373), decodeResult: decoder(360), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflow.pause_run", "schema.workflow.pause_run.result", 1, ["completed","timed_out","failed","effect_unknown","partial"], {"mode":"not_cancellable"}, "durable_effect", "required", decoder(360)) }, + decodeRequest: decoder(375), decodeResult: decoder(362), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflow.pause_run", "schema.workflow.pause_run.result", 1, ["completed","timed_out","failed","effect_unknown","partial"], {"mode":"not_cancellable"}, "durable_effect", "required", decoder(362)) }, { operation: "workflow_register_definition", operationId: "operation.workflow.register_definition", transport: { kind: "http", route: "/application/workflow/register-definition", method: "POST" }, effect: "administrative", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.workflow.register_definition", requestSchema: { schemaId: "schema.workflow.register_definition.request", revision: 1 }, resultSchema: { schemaId: "schema.workflow.register_definition.result", revision: 1 }, cancellation: {"mode":"not_cancellable"}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(374), decodeResult: decoder(366), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflow.register_definition", "schema.workflow.register_definition.result", 1, ["completed","timed_out","failed","effect_unknown","partial"], {"mode":"not_cancellable"}, "durable_effect", "required", decoder(366)) }, + decodeRequest: decoder(376), decodeResult: decoder(368), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflow.register_definition", "schema.workflow.register_definition.result", 1, ["completed","timed_out","failed","effect_unknown","partial"], {"mode":"not_cancellable"}, "durable_effect", "required", decoder(368)) }, { operation: "workflow_reject_definition", operationId: "operation.workflow.reject_definition", transport: { kind: "http", route: "/application/workflow/reject-definition", method: "POST" }, effect: "administrative", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.workflow.reject_definition", requestSchema: { schemaId: "schema.workflow.reject_definition.request", revision: 1 }, resultSchema: { schemaId: "schema.workflow.reject_definition.result", revision: 1 }, cancellation: {"mode":"not_cancellable"}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(375), decodeResult: decoder(358), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflow.reject_definition", "schema.workflow.reject_definition.result", 1, ["completed","timed_out","failed","effect_unknown","partial"], {"mode":"not_cancellable"}, "durable_effect", "required", decoder(358)) }, + decodeRequest: decoder(377), decodeResult: decoder(360), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflow.reject_definition", "schema.workflow.reject_definition.result", 1, ["completed","timed_out","failed","effect_unknown","partial"], {"mode":"not_cancellable"}, "durable_effect", "required", decoder(360)) }, { operation: "workflow_resume_run", operationId: "operation.workflow.resume_run", transport: { kind: "http", route: "/application/workflow/resume-run", method: "POST" }, effect: "administrative", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.workflow.resume_run", requestSchema: { schemaId: "schema.workflow.resume_run.request", revision: 1 }, resultSchema: { schemaId: "schema.workflow.resume_run.result", revision: 1 }, cancellation: {"mode":"not_cancellable"}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(376), decodeResult: decoder(360), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflow.resume_run", "schema.workflow.resume_run.result", 1, ["completed","timed_out","failed","effect_unknown","partial"], {"mode":"not_cancellable"}, "durable_effect", "required", decoder(360)) }, + decodeRequest: decoder(378), decodeResult: decoder(362), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflow.resume_run", "schema.workflow.resume_run.result", 1, ["completed","timed_out","failed","effect_unknown","partial"], {"mode":"not_cancellable"}, "durable_effect", "required", decoder(362)) }, { operation: "workflow_retire_definition", operationId: "operation.workflow.retire_definition", transport: { kind: "http", route: "/application/workflow/retire-definition", method: "POST" }, effect: "administrative", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.workflow.retire_definition", requestSchema: { schemaId: "schema.workflow.retire_definition.request", revision: 1 }, resultSchema: { schemaId: "schema.workflow.retire_definition.result", revision: 1 }, cancellation: {"mode":"not_cancellable"}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(377), decodeResult: decoder(358), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflow.retire_definition", "schema.workflow.retire_definition.result", 1, ["completed","timed_out","failed","effect_unknown","partial"], {"mode":"not_cancellable"}, "durable_effect", "required", decoder(358)) }, + decodeRequest: decoder(379), decodeResult: decoder(360), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflow.retire_definition", "schema.workflow.retire_definition.result", 1, ["completed","timed_out","failed","effect_unknown","partial"], {"mode":"not_cancellable"}, "durable_effect", "required", decoder(360)) }, { operation: "workflow_start_run", operationId: "operation.workflow.start_run", transport: { kind: "http", route: "/application/workflow/start-run", method: "POST" }, effect: "administrative", idempotency: "required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.workflow.start_run", requestSchema: { schemaId: "schema.workflow.start_run.request", revision: 1 }, resultSchema: { schemaId: "schema.workflow.start_run.result", revision: 1 }, cancellation: {"mode":"not_cancellable"}, deadline: {"behavior":"return_effect_receipt","maximum_millis":30000}, reconciliation: "required", receipt: "durable_effect", terminalStates: ["completed","timed_out","failed","effect_unknown","partial"], - decodeRequest: decoder(378), decodeResult: decoder(360), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflow.start_run", "schema.workflow.start_run.result", 1, ["completed","timed_out","failed","effect_unknown","partial"], {"mode":"not_cancellable"}, "durable_effect", "required", decoder(360)) }, + decodeRequest: decoder(380), decodeResult: decoder(362), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflow.start_run", "schema.workflow.start_run.result", 1, ["completed","timed_out","failed","effect_unknown","partial"], {"mode":"not_cancellable"}, "durable_effect", "required", decoder(362)) }, { operation: "workflow_validate_definition", operationId: "operation.workflow.validate_definition", transport: { kind: "http", route: "/application/workflow/validate-definition", method: "POST" }, effect: "read", idempotency: "not_required", requestIdControl: "server_minted", resultSemantics: "schema_only", bindingId: "binding.http.workflow.validate_definition", requestSchema: { schemaId: "schema.workflow.validate_definition.request", revision: 1 }, resultSchema: { schemaId: "schema.workflow.validate_definition.result", revision: 1 }, cancellation: {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, deadline: {"behavior":"return_operation_receipt","maximum_millis":30000}, reconciliation: "not_required", receipt: "operation", terminalStates: ["completed","cancelled","timed_out","failed","partial"], - decodeRequest: decoder(379), decodeResult: decoder(380), - decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflow.validate_definition", "schema.workflow.validate_definition.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(380)) }, + decodeRequest: decoder(381), decodeResult: decoder(382), + decodeSuccess: (value: unknown) => decodeHttpSuccessEnvelope(value, "binding.http.workflow.validate_definition", "schema.workflow.validate_definition.result", 1, ["completed","cancelled","timed_out","failed","partial"], {"mode":"cooperative","points":["before_admission","before_read","during_read"]}, "operation", "not_required", decoder(382)) }, ] as const; export type Operation = (typeof OPERATIONS)[number];