From 86272c6d6489f4afb08d416a9adae8ff3ccabc5f Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sun, 27 Sep 2026 04:41:23 +0000 Subject: [PATCH] refactor(mcp): answer search through the owner tracedecay_search is an ApplicationSurfaceOperation the project's graph-tool owner answers, on MCP and `tracedecay tool` alike. Its arguments decode against a typed request in the contracts crate, so an unknown key or a mistyped limit is refused instead of being silently ignored or defaulted. Its result is a typed catalog result that serializes to the JSON the tool already emitted: the freshness verdict, per-lane coverage (a partial lane keeps its null generation and reason), ranked rows with display and route evidence, the lexical route and anchor receipts with admitted/dropped counts, the scope report, graph evidence, the external import hint, and parked indexing detail on an unavailable search. Every surface renders it through one renderer; the stale-graph trailer rides the owner's served generation. The 2,345-line graph search module is split by tool: search keeps the request decode, the owner computation, and the renderer; context, find_exact_symbol, the clone family, and rename_preview move to their own modules. The root graph dispatch group, its portable dispatch table, and the Graph binding group are deleted, and the search schema bounds move to the contracts crate beside the request they describe. --- .../src/http/application_operation_owner.rs | 3 +- crates/tracedecay-contracts/src/graph_tool.rs | 9 +- .../src/retrieval/catalog.rs | 9 +- .../tracedecay-contracts/src/retrieval/mod.rs | 11 + .../src/retrieval/search_surface.rs | 297 +++ .../tracedecay-contracts/src/sdk_catalog.rs | 3 +- .../src/application_surface.rs | 3 +- .../src/application_surface/invocation.rs | 1 + .../src/contract/mod.rs | 3 +- .../tracedecay-mcp-catalog/src/definitions.rs | 3 +- .../src/definitions/application.rs | 1 + .../src/definitions/graph.rs | 170 +- crates/tracedecay-mcp-catalog/src/lib.rs | 7 +- .../src/handlers/dependency_hints.rs | 44 +- .../src/handlers/graph/clones.rs | 617 ++++++ .../src/handlers/graph/context.rs | 644 ++++++ .../src/handlers/graph/dispatch.rs | 37 - .../src/handlers/graph/exact_symbol.rs | 85 + .../src/handlers/graph/lexical_routing.rs | 346 +-- .../tracedecay-mcp/src/handlers/graph/mod.rs | 16 +- .../src/handlers/graph/rename_preview.rs | 287 +++ .../src/handlers/graph/search.rs | 1909 ++--------------- .../src/handlers/graph/search_evidence.rs | 36 +- .../tracedecay-mcp/src/handlers/graph_tool.rs | 15 +- crates/tracedecay-mcp/src/handlers/support.rs | 9 +- crates/tracedecay-mcp/src/tools/binding.rs | 8 +- .../src/tools/catalog_discovery.rs | 5 +- .../tracedecay-tool-catalog/src/operation.rs | 2 + .../mcp/server/cancel_candidate_journey.rs | 16 +- .../src/mcp/tools/handlers/dispatch_groups.rs | 41 - .../tools/handlers/dispatch_test_support.rs | 3 - .../src/mcp/tools/handlers/dispatch_tests.rs | 27 +- .../handlers/graph_search_dispatch_tests.rs | 36 +- .../tracedecay/src/mcp/tools/handlers/mod.rs | 15 +- .../search_graph_independence_tests.rs | 16 +- .../mcp_suite/mcp_handler_test/schema_test.rs | 4 +- .../mcp_handler_test/search_behavior_test.rs | 49 +- sdks/typescript/src/operations.ts | 346 +-- 38 files changed, 2737 insertions(+), 2396 deletions(-) create mode 100644 crates/tracedecay-contracts/src/retrieval/search_surface.rs create mode 100644 crates/tracedecay-mcp/src/handlers/graph/clones.rs create mode 100644 crates/tracedecay-mcp/src/handlers/graph/context.rs delete mode 100644 crates/tracedecay-mcp/src/handlers/graph/dispatch.rs create mode 100644 crates/tracedecay-mcp/src/handlers/graph/exact_symbol.rs create mode 100644 crates/tracedecay-mcp/src/handlers/graph/rename_preview.rs 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];