From a787c20d8937bbdd2129ed5ede8a860b9d1b5929 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Tue, 30 Jun 2026 11:39:37 -0400 Subject: [PATCH 1/5] {"schema":"decodex/commit/1","summary":"Split MCP core tool modules","authority":"manual"} --- apps/elf-mcp/src/app/server.rs | 8 +- apps/elf-mcp/src/app/server/tools/core.rs | 336 +----------------- .../src/app/server/tools/core/ingest.rs | 49 +++ .../src/app/server/tools/core/memory.rs | 116 ++++++ .../src/app/server/tools/core/notes.rs | 89 +++++ .../src/app/server/tools/core/search.rs | 74 ++++ .../src/app/server/tools/core/sharing.rs | 59 +++ 7 files changed, 399 insertions(+), 332 deletions(-) create mode 100644 apps/elf-mcp/src/app/server/tools/core/ingest.rs create mode 100644 apps/elf-mcp/src/app/server/tools/core/memory.rs create mode 100644 apps/elf-mcp/src/app/server/tools/core/notes.rs create mode 100644 apps/elf-mcp/src/app/server/tools/core/search.rs create mode 100644 apps/elf-mcp/src/app/server/tools/core/sharing.rs diff --git a/apps/elf-mcp/src/app/server.rs b/apps/elf-mcp/src/app/server.rs index 33910068..f9eec358 100644 --- a/apps/elf-mcp/src/app/server.rs +++ b/apps/elf-mcp/src/app/server.rs @@ -30,7 +30,13 @@ const HEADER_AUTHORIZATION: &str = "Authorization"; impl ElfMcp { pub(in crate::app::server) fn tool_router() -> ToolRouter { - Self::core_tool_router() + Self::docs_tool_router() + Self::admin_tool_router() + Self::core_ingest_tool_router() + + Self::core_memory_tool_router() + + Self::core_notes_tool_router() + + Self::core_search_tool_router() + + Self::core_sharing_tool_router() + + Self::docs_tool_router() + + Self::admin_tool_router() } } diff --git a/apps/elf-mcp/src/app/server/tools/core.rs b/apps/elf-mcp/src/app/server/tools/core.rs index e848c476..1e4bb337 100644 --- a/apps/elf-mcp/src/app/server/tools/core.rs +++ b/apps/elf-mcp/src/app/server/tools/core.rs @@ -1,331 +1,5 @@ -use color_eyre::Result; -use rmcp::{ - ErrorData, - model::{CallToolResult, JsonObject}, -}; - -use crate::app::server::{ - ElfMcp, HttpMethod, - schemas::{ - core_blocks_get_schema, dreaming_review_queue_schema, entity_memory_get_schema, - events_ingest_schema, graph_query_schema, graph_report_schema, notes_get_schema, - notes_ingest_schema, notes_list_schema, notes_patch_schema, notes_publish_schema, - notes_unpublish_schema, recall_debug_panel_schema, searches_create_schema, - searches_get_schema, searches_notes_schema, searches_timeline_schema, - space_grant_revoke_schema, space_grant_upsert_schema, space_grants_list_schema, - work_journal_entry_create_schema, work_journal_entry_get_schema, - work_journal_session_readback_schema, - }, - support, -}; - -#[rmcp::tool_router(router = core_tool_router, vis = "pub(in crate::app::server)")] -impl ElfMcp { - #[rmcp::tool( - name = "elf_notes_ingest", - description = "Ingest deterministic notes into ELF. This tool never calls an LLM.", - input_schema = notes_ingest_schema() - )] - async fn elf_notes_ingest(&self, params: JsonObject) -> Result { - self.forward(HttpMethod::Post, "/v2/notes/ingest", params, None).await - } - - #[rmcp::tool( - name = "elf_graph_query", - description = "Query graph entities and relations by structured criteria.", - input_schema = graph_query_schema() - )] - async fn elf_graph_query(&self, params: JsonObject) -> Result { - self.forward(HttpMethod::Post, "/v2/graph/query", params, None).await - } - - #[rmcp::tool( - name = "elf_graph_report", - description = "Build a source-backed graph topic map with current, historical, future, inferred, ambiguous, stale, and superseded fact markers.", - input_schema = graph_report_schema() - )] - async fn elf_graph_report(&self, params: JsonObject) -> Result { - self.forward(HttpMethod::Post, "/v2/graph/report", params, None).await - } - - #[rmcp::tool( - name = "elf_events_ingest", - description = "Ingest an event by extracting evidence-bound notes using the configured LLM extractor.", - input_schema = events_ingest_schema() - )] - async fn elf_events_ingest(&self, params: JsonObject) -> Result { - self.forward(HttpMethod::Post, "/v2/events/ingest", params, None).await - } - - #[rmcp::tool( - name = "elf_work_journal_entry_create", - description = "Capture one source-adjacent Work Journal entry with source refs, redaction, next-step, rejected-option, and promotion-boundary metadata. Journal content is not authoritative memory.", - input_schema = work_journal_entry_create_schema() - )] - async fn elf_work_journal_entry_create( - &self, - params: JsonObject, - ) -> Result { - self.forward(HttpMethod::Post, "/v2/work-journal/entries", params, None).await - } - - #[rmcp::tool( - name = "elf_work_journal_entry_get", - description = "Fetch one readable Work Journal entry by entry_id.", - input_schema = work_journal_entry_get_schema() - )] - async fn elf_work_journal_entry_get( - &self, - mut params: JsonObject, - ) -> Result { - let entry_id = support::take_required_string(&mut params, "entry_id")?; - let path = format!("/v2/work-journal/entries/{entry_id}"); - - self.forward(HttpMethod::Get, &path, JsonObject::new(), None).await - } - - #[rmcp::tool( - name = "elf_work_journal_session_readback", - description = "Read newest Work Journal entries for a session and return a where_stopped projection with journal evidence. Current-fact answers still require accepted memory or knowledge authority.", - input_schema = work_journal_session_readback_schema() - )] - async fn elf_work_journal_session_readback( - &self, - mut params: JsonObject, - ) -> Result { - // read_profile is part of the MCP server configuration and is not client-controlled. - let _ = support::take_optional_string(&mut params, "read_profile")?; - - self.forward(HttpMethod::Post, "/v2/work-journal/readback", params, None).await - } - - #[rmcp::tool( - name = "elf_core_blocks_get", - description = "Fetch core memory blocks explicitly attached to the configured agent and read profile. This is separate from archival search.", - input_schema = core_blocks_get_schema() - )] - async fn elf_core_blocks_get( - &self, - mut params: JsonObject, - ) -> Result { - // read_profile is part of the MCP server configuration and is not client-controlled. - let _ = support::take_optional_string(&mut params, "read_profile")?; - - self.forward(HttpMethod::Get, "/v2/core-blocks", params, None).await - } - - #[rmcp::tool( - name = "elf_entity_memory_get", - description = "Fetch an entity-scoped memory view across attached core blocks and graph-linked archival notes.", - input_schema = entity_memory_get_schema() - )] - async fn elf_entity_memory_get( - &self, - mut params: JsonObject, - ) -> Result { - // read_profile is part of the MCP server configuration and is not client-controlled. - let _ = support::take_optional_string(&mut params, "read_profile")?; - - self.forward(HttpMethod::Get, "/v2/entity-memory", params, None).await - } - - #[rmcp::tool( - name = "elf_dreaming_review_queue", - description = "List source-backed Dreaming review queue proposals with variants, affected refs, lint flags, policy gates, and review audit.", - input_schema = dreaming_review_queue_schema() - )] - async fn elf_dreaming_review_queue( - &self, - params: JsonObject, - ) -> Result { - self.forward(HttpMethod::Get, "/v2/admin/dreaming/review-queue", params, None).await - } - - #[rmcp::tool( - name = "elf_recall_debug_panel", - description = "Build an agent-facing cross-layer recall/debug panel and deterministic recall_trace over memory traces, source documents, knowledge pages, graph facts, and Dreaming proposals.", - input_schema = recall_debug_panel_schema() - )] - pub(in crate::app::server) async fn elf_recall_debug_panel( - &self, - params: JsonObject, - ) -> Result { - support::reject_context_override_params(¶ms)?; - - self.forward(HttpMethod::Post, "/v2/recall-debug/panel", params, None).await - } - - #[rmcp::tool( - name = "elf_searches_create", - description = "Create a search session using quick-find or planned-search mode. Response includes optional trajectory_summary for staged retrieval progress.", - input_schema = searches_create_schema() - )] - async fn elf_searches_create( - &self, - mut params: JsonObject, - ) -> Result { - // read_profile is part of the MCP server configuration and is not client-controlled. - let _ = support::take_optional_string(&mut params, "read_profile")?; - - self.forward(HttpMethod::Post, "/v2/searches", params, None).await - } - - #[rmcp::tool( - name = "elf_searches_get", - description = "Fetch a search session index view by search_id, including optional trajectory_summary.", - input_schema = searches_get_schema() - )] - async fn elf_searches_get(&self, mut params: JsonObject) -> Result { - let search_id = support::take_required_string(&mut params, "search_id")?; - let path = format!("/v2/searches/{search_id}"); - - self.forward(HttpMethod::Get, &path, params, None).await - } - - #[rmcp::tool( - name = "elf_searches_timeline", - description = "Build a timeline view from a search session.", - input_schema = searches_timeline_schema() - )] - async fn elf_searches_timeline( - &self, - mut params: JsonObject, - ) -> Result { - let search_id = support::take_required_string(&mut params, "search_id")?; - let path = format!("/v2/searches/{search_id}/timeline"); - - self.forward(HttpMethod::Get, &path, params, None).await - } - - #[rmcp::tool( - name = "elf_searches_notes", - description = "Fetch note details for selected note_ids from a search session. l0/l1 strip evidence/source_ref; l2 returns full detail.", - input_schema = searches_notes_schema() - )] - async fn elf_searches_notes( - &self, - mut params: JsonObject, - ) -> Result { - let search_id = support::take_required_string(&mut params, "search_id")?; - let path = format!("/v2/searches/{search_id}/notes"); - - self.forward(HttpMethod::Post, &path, params, None).await - } - - #[rmcp::tool( - name = "elf_notes_list", - description = "List notes in a tenant and project with optional filters.", - input_schema = notes_list_schema() - )] - async fn elf_notes_list(&self, params: JsonObject) -> Result { - self.forward(HttpMethod::Get, "/v2/notes", params, None).await - } - - #[rmcp::tool( - name = "elf_notes_get", - description = "Fetch a single note by note_id.", - input_schema = notes_get_schema() - )] - async fn elf_notes_get(&self, mut params: JsonObject) -> Result { - let note_id = support::take_required_string(&mut params, "note_id")?; - let path = format!("/v2/notes/{note_id}"); - - self.forward(HttpMethod::Get, &path, JsonObject::new(), None).await - } - - #[rmcp::tool( - name = "elf_notes_patch", - description = "Patch a note by note_id. Only provided fields are updated.", - input_schema = notes_patch_schema() - )] - async fn elf_notes_patch(&self, mut params: JsonObject) -> Result { - let note_id = support::take_required_string(&mut params, "note_id")?; - let path = format!("/v2/notes/{note_id}"); - - self.forward(HttpMethod::Patch, &path, params, None).await - } - - #[rmcp::tool( - name = "elf_notes_delete", - description = "Delete a note by note_id.", - input_schema = notes_get_schema() - )] - async fn elf_notes_delete(&self, mut params: JsonObject) -> Result { - let note_id = support::take_required_string(&mut params, "note_id")?; - let path = format!("/v2/notes/{note_id}"); - - self.forward(HttpMethod::Delete, &path, JsonObject::new(), None).await - } - - #[rmcp::tool( - name = "elf_notes_publish", - description = "Publish a note from agent_private into a shared space (team_shared or org_shared).", - input_schema = notes_publish_schema() - )] - async fn elf_notes_publish(&self, mut params: JsonObject) -> Result { - let note_id = support::take_required_string(&mut params, "note_id")?; - let path = format!("/v2/notes/{note_id}/publish"); - - self.forward(HttpMethod::Post, &path, params, None).await - } - - #[rmcp::tool( - name = "elf_notes_unpublish", - description = "Unpublish a shared note back into agent_private scope.", - input_schema = notes_unpublish_schema() - )] - async fn elf_notes_unpublish( - &self, - mut params: JsonObject, - ) -> Result { - let note_id = support::take_required_string(&mut params, "note_id")?; - let path = format!("/v2/notes/{note_id}/unpublish"); - - self.forward(HttpMethod::Post, &path, params, None).await - } - - #[rmcp::tool( - name = "elf_space_grants_list", - description = "List sharing grants for a space (team_shared or org_shared).", - input_schema = space_grants_list_schema() - )] - async fn elf_space_grants_list( - &self, - mut params: JsonObject, - ) -> Result { - let space = support::take_required_string(&mut params, "space")?; - let path = format!("/v2/spaces/{space}/grants"); - - self.forward(HttpMethod::Get, &path, params, None).await - } - - #[rmcp::tool( - name = "elf_space_grant_upsert", - description = "Upsert a sharing grant for a space (team_shared or org_shared).", - input_schema = space_grant_upsert_schema() - )] - async fn elf_space_grant_upsert( - &self, - mut params: JsonObject, - ) -> Result { - let space = support::take_required_string(&mut params, "space")?; - let path = format!("/v2/spaces/{space}/grants"); - - self.forward(HttpMethod::Post, &path, params, None).await - } - - #[rmcp::tool( - name = "elf_space_grant_revoke", - description = "Revoke a sharing grant for a space (team_shared or org_shared).", - input_schema = space_grant_revoke_schema() - )] - async fn elf_space_grant_revoke( - &self, - mut params: JsonObject, - ) -> Result { - let space = support::take_required_string(&mut params, "space")?; - let path = format!("/v2/spaces/{space}/grants/revoke"); - - self.forward(HttpMethod::Post, &path, params, None).await - } -} +mod ingest; +mod memory; +mod notes; +mod search; +mod sharing; diff --git a/apps/elf-mcp/src/app/server/tools/core/ingest.rs b/apps/elf-mcp/src/app/server/tools/core/ingest.rs new file mode 100644 index 00000000..9dbe8908 --- /dev/null +++ b/apps/elf-mcp/src/app/server/tools/core/ingest.rs @@ -0,0 +1,49 @@ +use color_eyre::Result; +use rmcp::{ + ErrorData, + model::{CallToolResult, JsonObject}, +}; + +use crate::app::server::{ + ElfMcp, HttpMethod, + schemas::{events_ingest_schema, graph_query_schema, graph_report_schema, notes_ingest_schema}, +}; + +#[rmcp::tool_router(router = core_ingest_tool_router, vis = "pub(in crate::app::server)")] +impl ElfMcp { + #[rmcp::tool( + name = "elf_notes_ingest", + description = "Ingest deterministic notes into ELF. This tool never calls an LLM.", + input_schema = notes_ingest_schema() + )] + async fn elf_notes_ingest(&self, params: JsonObject) -> Result { + self.forward(HttpMethod::Post, "/v2/notes/ingest", params, None).await + } + + #[rmcp::tool( + name = "elf_graph_query", + description = "Query graph entities and relations by structured criteria.", + input_schema = graph_query_schema() + )] + async fn elf_graph_query(&self, params: JsonObject) -> Result { + self.forward(HttpMethod::Post, "/v2/graph/query", params, None).await + } + + #[rmcp::tool( + name = "elf_graph_report", + description = "Build a source-backed graph topic map with current, historical, future, inferred, ambiguous, stale, and superseded fact markers.", + input_schema = graph_report_schema() + )] + async fn elf_graph_report(&self, params: JsonObject) -> Result { + self.forward(HttpMethod::Post, "/v2/graph/report", params, None).await + } + + #[rmcp::tool( + name = "elf_events_ingest", + description = "Ingest an event by extracting evidence-bound notes using the configured LLM extractor.", + input_schema = events_ingest_schema() + )] + async fn elf_events_ingest(&self, params: JsonObject) -> Result { + self.forward(HttpMethod::Post, "/v2/events/ingest", params, None).await + } +} diff --git a/apps/elf-mcp/src/app/server/tools/core/memory.rs b/apps/elf-mcp/src/app/server/tools/core/memory.rs new file mode 100644 index 00000000..88c487e1 --- /dev/null +++ b/apps/elf-mcp/src/app/server/tools/core/memory.rs @@ -0,0 +1,116 @@ +use color_eyre::Result; +use rmcp::{ + ErrorData, + model::{CallToolResult, JsonObject}, +}; + +use crate::app::server::{ + ElfMcp, HttpMethod, + schemas::{ + core_blocks_get_schema, dreaming_review_queue_schema, entity_memory_get_schema, + recall_debug_panel_schema, work_journal_entry_create_schema, work_journal_entry_get_schema, + work_journal_session_readback_schema, + }, + support, +}; + +#[rmcp::tool_router(router = core_memory_tool_router, vis = "pub(in crate::app::server)")] +impl ElfMcp { + #[rmcp::tool( + name = "elf_work_journal_entry_create", + description = "Capture one source-adjacent Work Journal entry with source refs, redaction, next-step, rejected-option, and promotion-boundary metadata. Journal content is not authoritative memory.", + input_schema = work_journal_entry_create_schema() + )] + async fn elf_work_journal_entry_create( + &self, + params: JsonObject, + ) -> Result { + self.forward(HttpMethod::Post, "/v2/work-journal/entries", params, None).await + } + + #[rmcp::tool( + name = "elf_work_journal_entry_get", + description = "Fetch one readable Work Journal entry by entry_id.", + input_schema = work_journal_entry_get_schema() + )] + async fn elf_work_journal_entry_get( + &self, + mut params: JsonObject, + ) -> Result { + let entry_id = support::take_required_string(&mut params, "entry_id")?; + let path = format!("/v2/work-journal/entries/{entry_id}"); + + self.forward(HttpMethod::Get, &path, JsonObject::new(), None).await + } + + #[rmcp::tool( + name = "elf_work_journal_session_readback", + description = "Read newest Work Journal entries for a session and return a where_stopped projection with journal evidence. Current-fact answers still require accepted memory or knowledge authority.", + input_schema = work_journal_session_readback_schema() + )] + async fn elf_work_journal_session_readback( + &self, + mut params: JsonObject, + ) -> Result { + // read_profile is part of the MCP server configuration and is not client-controlled. + let _ = support::take_optional_string(&mut params, "read_profile")?; + + self.forward(HttpMethod::Post, "/v2/work-journal/readback", params, None).await + } + + #[rmcp::tool( + name = "elf_core_blocks_get", + description = "Fetch core memory blocks explicitly attached to the configured agent and read profile. This is separate from archival search.", + input_schema = core_blocks_get_schema() + )] + async fn elf_core_blocks_get( + &self, + mut params: JsonObject, + ) -> Result { + // read_profile is part of the MCP server configuration and is not client-controlled. + let _ = support::take_optional_string(&mut params, "read_profile")?; + + self.forward(HttpMethod::Get, "/v2/core-blocks", params, None).await + } + + #[rmcp::tool( + name = "elf_entity_memory_get", + description = "Fetch an entity-scoped memory view across attached core blocks and graph-linked archival notes.", + input_schema = entity_memory_get_schema() + )] + async fn elf_entity_memory_get( + &self, + mut params: JsonObject, + ) -> Result { + // read_profile is part of the MCP server configuration and is not client-controlled. + let _ = support::take_optional_string(&mut params, "read_profile")?; + + self.forward(HttpMethod::Get, "/v2/entity-memory", params, None).await + } + + #[rmcp::tool( + name = "elf_dreaming_review_queue", + description = "List source-backed Dreaming review queue proposals with variants, affected refs, lint flags, policy gates, and review audit.", + input_schema = dreaming_review_queue_schema() + )] + async fn elf_dreaming_review_queue( + &self, + params: JsonObject, + ) -> Result { + self.forward(HttpMethod::Get, "/v2/admin/dreaming/review-queue", params, None).await + } + + #[rmcp::tool( + name = "elf_recall_debug_panel", + description = "Build an agent-facing cross-layer recall/debug panel and deterministic recall_trace over memory traces, source documents, knowledge pages, graph facts, and Dreaming proposals.", + input_schema = recall_debug_panel_schema() + )] + pub(in crate::app::server) async fn elf_recall_debug_panel( + &self, + params: JsonObject, + ) -> Result { + support::reject_context_override_params(¶ms)?; + + self.forward(HttpMethod::Post, "/v2/recall-debug/panel", params, None).await + } +} diff --git a/apps/elf-mcp/src/app/server/tools/core/notes.rs b/apps/elf-mcp/src/app/server/tools/core/notes.rs new file mode 100644 index 00000000..4b7cd3ae --- /dev/null +++ b/apps/elf-mcp/src/app/server/tools/core/notes.rs @@ -0,0 +1,89 @@ +use color_eyre::Result; +use rmcp::{ + ErrorData, + model::{CallToolResult, JsonObject}, +}; + +use crate::app::server::{ + ElfMcp, HttpMethod, + schemas::{ + notes_get_schema, notes_list_schema, notes_patch_schema, notes_publish_schema, + notes_unpublish_schema, + }, + support, +}; + +#[rmcp::tool_router(router = core_notes_tool_router, vis = "pub(in crate::app::server)")] +impl ElfMcp { + #[rmcp::tool( + name = "elf_notes_list", + description = "List notes in a tenant and project with optional filters.", + input_schema = notes_list_schema() + )] + async fn elf_notes_list(&self, params: JsonObject) -> Result { + self.forward(HttpMethod::Get, "/v2/notes", params, None).await + } + + #[rmcp::tool( + name = "elf_notes_get", + description = "Fetch a single note by note_id.", + input_schema = notes_get_schema() + )] + async fn elf_notes_get(&self, mut params: JsonObject) -> Result { + let note_id = support::take_required_string(&mut params, "note_id")?; + let path = format!("/v2/notes/{note_id}"); + + self.forward(HttpMethod::Get, &path, JsonObject::new(), None).await + } + + #[rmcp::tool( + name = "elf_notes_patch", + description = "Patch a note by note_id. Only provided fields are updated.", + input_schema = notes_patch_schema() + )] + async fn elf_notes_patch(&self, mut params: JsonObject) -> Result { + let note_id = support::take_required_string(&mut params, "note_id")?; + let path = format!("/v2/notes/{note_id}"); + + self.forward(HttpMethod::Patch, &path, params, None).await + } + + #[rmcp::tool( + name = "elf_notes_delete", + description = "Delete a note by note_id.", + input_schema = notes_get_schema() + )] + async fn elf_notes_delete(&self, mut params: JsonObject) -> Result { + let note_id = support::take_required_string(&mut params, "note_id")?; + let path = format!("/v2/notes/{note_id}"); + + self.forward(HttpMethod::Delete, &path, JsonObject::new(), None).await + } + + #[rmcp::tool( + name = "elf_notes_publish", + description = "Publish a note from agent_private into a shared space (team_shared or org_shared).", + input_schema = notes_publish_schema() + )] + async fn elf_notes_publish(&self, mut params: JsonObject) -> Result { + let note_id = support::take_required_string(&mut params, "note_id")?; + let path = format!("/v2/notes/{note_id}/publish"); + + self.forward(HttpMethod::Post, &path, params, None).await + } + + #[rmcp::tool( + name = "elf_notes_unpublish", + description = "Unpublish a shared note back into agent_private scope.", + input_schema = notes_unpublish_schema() + )] + async fn elf_notes_unpublish( + &self, + mut params: JsonObject, + ) -> Result { + let note_id = support::take_required_string(&mut params, "note_id")?; + let path = format!("/v2/notes/{note_id}/unpublish"); + + self.forward(HttpMethod::Post, &path, params, None).await + } +} diff --git a/apps/elf-mcp/src/app/server/tools/core/search.rs b/apps/elf-mcp/src/app/server/tools/core/search.rs new file mode 100644 index 00000000..e000b2b0 --- /dev/null +++ b/apps/elf-mcp/src/app/server/tools/core/search.rs @@ -0,0 +1,74 @@ +use color_eyre::Result; +use rmcp::{ + ErrorData, + model::{CallToolResult, JsonObject}, +}; + +use crate::app::server::{ + ElfMcp, HttpMethod, + schemas::{ + searches_create_schema, searches_get_schema, searches_notes_schema, + searches_timeline_schema, + }, + support, +}; + +#[rmcp::tool_router(router = core_search_tool_router, vis = "pub(in crate::app::server)")] +impl ElfMcp { + #[rmcp::tool( + name = "elf_searches_create", + description = "Create a search session using quick-find or planned-search mode. Response includes optional trajectory_summary for staged retrieval progress.", + input_schema = searches_create_schema() + )] + async fn elf_searches_create( + &self, + mut params: JsonObject, + ) -> Result { + // read_profile is part of the MCP server configuration and is not client-controlled. + let _ = support::take_optional_string(&mut params, "read_profile")?; + + self.forward(HttpMethod::Post, "/v2/searches", params, None).await + } + + #[rmcp::tool( + name = "elf_searches_get", + description = "Fetch a search session index view by search_id, including optional trajectory_summary.", + input_schema = searches_get_schema() + )] + async fn elf_searches_get(&self, mut params: JsonObject) -> Result { + let search_id = support::take_required_string(&mut params, "search_id")?; + let path = format!("/v2/searches/{search_id}"); + + self.forward(HttpMethod::Get, &path, params, None).await + } + + #[rmcp::tool( + name = "elf_searches_timeline", + description = "Build a timeline view from a search session.", + input_schema = searches_timeline_schema() + )] + async fn elf_searches_timeline( + &self, + mut params: JsonObject, + ) -> Result { + let search_id = support::take_required_string(&mut params, "search_id")?; + let path = format!("/v2/searches/{search_id}/timeline"); + + self.forward(HttpMethod::Get, &path, params, None).await + } + + #[rmcp::tool( + name = "elf_searches_notes", + description = "Fetch note details for selected note_ids from a search session. l0/l1 strip evidence/source_ref; l2 returns full detail.", + input_schema = searches_notes_schema() + )] + async fn elf_searches_notes( + &self, + mut params: JsonObject, + ) -> Result { + let search_id = support::take_required_string(&mut params, "search_id")?; + let path = format!("/v2/searches/{search_id}/notes"); + + self.forward(HttpMethod::Post, &path, params, None).await + } +} diff --git a/apps/elf-mcp/src/app/server/tools/core/sharing.rs b/apps/elf-mcp/src/app/server/tools/core/sharing.rs new file mode 100644 index 00000000..a2053f4c --- /dev/null +++ b/apps/elf-mcp/src/app/server/tools/core/sharing.rs @@ -0,0 +1,59 @@ +use color_eyre::Result; +use rmcp::{ + ErrorData, + model::{CallToolResult, JsonObject}, +}; + +use crate::app::server::{ + ElfMcp, HttpMethod, + schemas::{space_grant_revoke_schema, space_grant_upsert_schema, space_grants_list_schema}, + support, +}; + +#[rmcp::tool_router(router = core_sharing_tool_router, vis = "pub(in crate::app::server)")] +impl ElfMcp { + #[rmcp::tool( + name = "elf_space_grants_list", + description = "List sharing grants for a space (team_shared or org_shared).", + input_schema = space_grants_list_schema() + )] + async fn elf_space_grants_list( + &self, + mut params: JsonObject, + ) -> Result { + let space = support::take_required_string(&mut params, "space")?; + let path = format!("/v2/spaces/{space}/grants"); + + self.forward(HttpMethod::Get, &path, params, None).await + } + + #[rmcp::tool( + name = "elf_space_grant_upsert", + description = "Upsert a sharing grant for a space (team_shared or org_shared).", + input_schema = space_grant_upsert_schema() + )] + async fn elf_space_grant_upsert( + &self, + mut params: JsonObject, + ) -> Result { + let space = support::take_required_string(&mut params, "space")?; + let path = format!("/v2/spaces/{space}/grants"); + + self.forward(HttpMethod::Post, &path, params, None).await + } + + #[rmcp::tool( + name = "elf_space_grant_revoke", + description = "Revoke a sharing grant for a space (team_shared or org_shared).", + input_schema = space_grant_revoke_schema() + )] + async fn elf_space_grant_revoke( + &self, + mut params: JsonObject, + ) -> Result { + let space = support::take_required_string(&mut params, "space")?; + let path = format!("/v2/spaces/{space}/grants/revoke"); + + self.forward(HttpMethod::Post, &path, params, None).await + } +} From fa2b9c53b3e745618980daf66e8dc3b74ca5a24c Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Tue, 30 Jun 2026 11:43:47 -0400 Subject: [PATCH 2/5] {"schema":"decodex/commit/1","summary":"Split trace compare analysis modules","authority":"manual"} --- .../src/app/trace_compare/analysis.rs | 334 +----------------- .../app/trace_compare/analysis/attribution.rs | 46 +++ .../app/trace_compare/analysis/candidates.rs | 40 +++ .../src/app/trace_compare/analysis/stages.rs | 43 +++ .../src/app/trace_compare/analysis/tests.rs | 196 ++++++++++ 5 files changed, 332 insertions(+), 327 deletions(-) create mode 100644 apps/elf-eval/src/app/trace_compare/analysis/attribution.rs create mode 100644 apps/elf-eval/src/app/trace_compare/analysis/candidates.rs create mode 100644 apps/elf-eval/src/app/trace_compare/analysis/stages.rs create mode 100644 apps/elf-eval/src/app/trace_compare/analysis/tests.rs diff --git a/apps/elf-eval/src/app/trace_compare/analysis.rs b/apps/elf-eval/src/app/trace_compare/analysis.rs index e71b76e2..95d4f7e6 100644 --- a/apps/elf-eval/src/app/trace_compare/analysis.rs +++ b/apps/elf-eval/src/app/trace_compare/analysis.rs @@ -1,330 +1,10 @@ -use std::collections::HashMap; +mod attribution; +mod candidates; +mod stages; -use uuid::Uuid; - -use crate::app::trace_compare::types::{ - TraceCompareCandidateRow, TraceCompareChurn, TraceCompareGuardrails, - TraceCompareRegressionAttribution, TraceCompareStageDelta, TraceCompareStageRow, +pub(super) use self::{ + attribution::build_trace_compare_regression_attribution, + candidates::decode_trace_replay_candidates, stages::build_trace_compare_stage_deltas, }; -use elf_service::search::TraceReplayCandidate; - -pub(super) fn decode_trace_replay_candidates( - rows: Vec, -) -> Vec { - rows.into_iter() - .map(|row| { - let decoded = - serde_json::from_value::(row.candidate_snapshot.clone()) - .ok() - .filter(|value| value.note_id != Uuid::nil() && value.chunk_id != Uuid::nil()); - - decoded.unwrap_or_else(|| TraceReplayCandidate { - note_id: row.note_id, - chunk_id: row.chunk_id, - chunk_index: row.chunk_index, - snippet: row.snippet, - retrieval_rank: u32::try_from(row.retrieval_rank).unwrap_or(0), - retrieval_score: None, - rerank_score: row.rerank_score, - note_scope: row.note_scope, - note_importance: row.note_importance, - note_updated_at: row.note_updated_at, - note_hit_count: row.note_hit_count, - note_last_hit_at: row.note_last_hit_at, - diversity_selected: None, - diversity_selected_rank: None, - diversity_selected_reason: None, - diversity_skipped_reason: None, - diversity_nearest_selected_note_id: None, - diversity_similarity: None, - diversity_mmr_score: None, - diversity_missing_embedding: None, - }) - }) - .collect() -} - -pub(super) fn build_trace_compare_stage_deltas( - stage_rows: &[TraceCompareStageRow], - a_selected_count: u32, - b_selected_count: u32, -) -> Vec { - if stage_rows.is_empty() { - return vec![TraceCompareStageDelta { - stage_order: 1, - stage_name: "selection.final".to_string(), - baseline_item_count: 0, - a_item_count: a_selected_count, - b_item_count: b_selected_count, - item_count_delta: b_selected_count as i64 - a_selected_count as i64, - baseline_stats: None, - }]; - } - - let mut out = Vec::with_capacity(stage_rows.len()); - - for row in stage_rows { - let baseline_item_count = row.item_count.max(0) as u32; - let (a_item_count, b_item_count) = if row.stage_name == "selection.final" { - (a_selected_count, b_selected_count) - } else { - (baseline_item_count, baseline_item_count) - }; - let baseline_stats = row.stage_payload.get("stats").cloned(); - - out.push(TraceCompareStageDelta { - stage_order: row.stage_order.max(0) as u32, - stage_name: row.stage_name.clone(), - baseline_item_count, - a_item_count, - b_item_count, - item_count_delta: b_item_count as i64 - a_item_count as i64, - baseline_stats, - }); - } - - out -} - -pub(super) fn build_trace_compare_regression_attribution( - churn: &TraceCompareChurn, - guardrails: &TraceCompareGuardrails, - stage_deltas: &[TraceCompareStageDelta], -) -> TraceCompareRegressionAttribution { - let stage_by_name: HashMap<&str, &TraceCompareStageDelta> = - stage_deltas.iter().map(|stage| (stage.stage_name.as_str(), stage)).collect(); - - if guardrails.retrieval_top3_retention_delta < 0.0 { - let recall_count = stage_by_name - .get("recall.candidates") - .map(|stage| stage.baseline_item_count) - .unwrap_or(0); - - return TraceCompareRegressionAttribution { - primary_stage: "selection.final".to_string(), - evidence: format!( - "retrieval_top3_retention dropped by {:.4} (a={:.4}, b={:.4}); recall baseline item_count={recall_count}", - guardrails.retrieval_top3_retention_delta, - guardrails.a_retrieval_top3_retention, - guardrails.b_retrieval_top3_retention - ), - }; - } - if churn.set_churn_at_k > 0.0 || churn.positional_churn_at_k > 0.0 { - return TraceCompareRegressionAttribution { - primary_stage: "rerank.score".to_string(), - evidence: format!( - "top-k churn changed without retrieval-top3 regression (set_churn_at_k={:.4}, positional_churn_at_k={:.4})", - churn.set_churn_at_k, churn.positional_churn_at_k - ), - }; - } - - TraceCompareRegressionAttribution { - primary_stage: "not_applicable".to_string(), - evidence: "No regression signal detected.".to_string(), - } -} - -#[cfg(test)] -mod tests { - use serde_json::json; - use time::OffsetDateTime; - use uuid::Uuid; - - use crate::app::trace_compare::{ - analysis, - types::{ - TraceCompareCandidateRow, TraceCompareChurn, TraceCompareGuardrails, - TraceCompareStageDelta, TraceCompareStageRow, - }, - }; - use elf_service::search::TraceReplayCandidate; - - #[test] - fn stage_deltas_fallback_to_final_selection_when_baseline_stages_are_absent() { - let deltas = analysis::build_trace_compare_stage_deltas(&[], 2, 4); - - assert_eq!(deltas.len(), 1); - assert_eq!(deltas[0].stage_order, 1); - assert_eq!(deltas[0].stage_name, "selection.final"); - assert_eq!(deltas[0].baseline_item_count, 0); - assert_eq!(deltas[0].a_item_count, 2); - assert_eq!(deltas[0].b_item_count, 4); - assert_eq!(deltas[0].item_count_delta, 2); - assert!(deltas[0].baseline_stats.is_none()); - } - - #[test] - fn stage_deltas_replace_final_selection_counts_and_preserve_stats() { - let rows = vec![ - TraceCompareStageRow { - stage_order: 1, - stage_name: "recall.candidates".to_string(), - stage_payload: json!({"stats": {"source": "baseline"}}), - item_count: 7, - }, - TraceCompareStageRow { - stage_order: 2, - stage_name: "selection.final".to_string(), - stage_payload: json!({"stats": {"selected": true}}), - item_count: 5, - }, - ]; - let deltas = analysis::build_trace_compare_stage_deltas(&rows, 3, 4); - - assert_eq!(deltas[0].baseline_item_count, 7); - assert_eq!(deltas[0].a_item_count, 7); - assert_eq!(deltas[0].b_item_count, 7); - assert_eq!(deltas[0].baseline_stats, Some(json!({"source": "baseline"}))); - assert_eq!(deltas[1].baseline_item_count, 5); - assert_eq!(deltas[1].a_item_count, 3); - assert_eq!(deltas[1].b_item_count, 4); - assert_eq!(deltas[1].item_count_delta, 1); - assert_eq!(deltas[1].baseline_stats, Some(json!({"selected": true}))); - } - - #[test] - fn regression_attribution_prefers_retention_drop_with_recall_context() { - let churn = TraceCompareChurn { positional_churn_at_k: 0.0, set_churn_at_k: 0.0 }; - let guardrails = TraceCompareGuardrails { - retrieval_top3_total: 3, - a_retrieval_top3_retained: 3, - a_retrieval_top3_retention: 1.0, - b_retrieval_top3_retained: 2, - b_retrieval_top3_retention: 0.6667, - retrieval_top3_retention_delta: -0.3333, - }; - let stage_deltas = vec![TraceCompareStageDelta { - stage_order: 1, - stage_name: "recall.candidates".to_string(), - baseline_item_count: 12, - a_item_count: 12, - b_item_count: 12, - item_count_delta: 0, - baseline_stats: None, - }]; - let attribution = analysis::build_trace_compare_regression_attribution( - &churn, - &guardrails, - &stage_deltas, - ); - - assert_eq!(attribution.primary_stage, "selection.final"); - assert!(attribution.evidence.contains("dropped by -0.3333")); - assert!(attribution.evidence.contains("recall baseline item_count=12")); - } - - #[test] - fn regression_attribution_uses_rerank_when_churn_changes_without_retention_drop() { - let churn = TraceCompareChurn { positional_churn_at_k: 0.5, set_churn_at_k: 0.25 }; - let guardrails = TraceCompareGuardrails { - retrieval_top3_total: 3, - a_retrieval_top3_retained: 2, - a_retrieval_top3_retention: 0.6667, - b_retrieval_top3_retained: 2, - b_retrieval_top3_retention: 0.6667, - retrieval_top3_retention_delta: 0.0, - }; - let attribution = - analysis::build_trace_compare_regression_attribution(&churn, &guardrails, &[]); - - assert_eq!(attribution.primary_stage, "rerank.score"); - assert!(attribution.evidence.contains("set_churn_at_k=0.2500")); - assert!(attribution.evidence.contains("positional_churn_at_k=0.5000")); - } - - #[test] - fn decode_candidates_falls_back_to_row_fields_when_snapshot_is_invalid() { - let now = OffsetDateTime::from_unix_timestamp(0).expect("Valid timestamp."); - let note_id = Uuid::new_v4(); - let chunk_id = Uuid::new_v4(); - let rows = vec![TraceCompareCandidateRow { - candidate_snapshot: json!({"invalid": true}), - note_id, - chunk_id, - chunk_index: 2, - snippet: "candidate".to_string(), - retrieval_rank: -1, - rerank_score: 0.75, - note_scope: "project_shared".to_string(), - note_importance: 0.5, - note_updated_at: now, - note_hit_count: 9, - note_last_hit_at: Some(now), - }]; - let candidates = analysis::decode_trace_replay_candidates(rows); - - assert_eq!(candidates.len(), 1); - assert_eq!(candidates[0].note_id, note_id); - assert_eq!(candidates[0].chunk_id, chunk_id); - assert_eq!(candidates[0].chunk_index, 2); - assert_eq!(candidates[0].snippet, "candidate"); - assert_eq!(candidates[0].retrieval_rank, 0); - assert_eq!(candidates[0].rerank_score, 0.75); - assert_eq!(candidates[0].note_scope, "project_shared"); - assert_eq!(candidates[0].note_importance, 0.5); - assert_eq!(candidates[0].note_updated_at, now); - assert_eq!(candidates[0].note_hit_count, 9); - assert_eq!(candidates[0].note_last_hit_at, Some(now)); - assert!(candidates[0].retrieval_score.is_none()); - } - - #[test] - fn decode_candidates_falls_back_when_valid_snapshot_has_nil_ids() { - let now = OffsetDateTime::from_unix_timestamp(0).expect("Valid timestamp."); - let note_id = Uuid::new_v4(); - let chunk_id = Uuid::new_v4(); - let snapshot = TraceReplayCandidate { - note_id: Uuid::nil(), - chunk_id: Uuid::new_v4(), - chunk_index: 99, - snippet: "snapshot".to_string(), - retrieval_rank: 1, - retrieval_score: Some(1.0), - rerank_score: 1.0, - note_scope: "snapshot_scope".to_string(), - note_importance: 1.0, - note_updated_at: now, - note_hit_count: 1, - note_last_hit_at: None, - diversity_selected: None, - diversity_selected_rank: None, - diversity_selected_reason: None, - diversity_skipped_reason: None, - diversity_nearest_selected_note_id: None, - diversity_similarity: None, - diversity_mmr_score: None, - diversity_missing_embedding: None, - }; - let rows = vec![TraceCompareCandidateRow { - candidate_snapshot: serde_json::to_value(snapshot).expect("Snapshot serializes."), - note_id, - chunk_id, - chunk_index: 2, - snippet: "candidate".to_string(), - retrieval_rank: 3, - rerank_score: 0.75, - note_scope: "project_shared".to_string(), - note_importance: 0.5, - note_updated_at: now, - note_hit_count: 9, - note_last_hit_at: Some(now), - }]; - let candidates = analysis::decode_trace_replay_candidates(rows); - assert_eq!(candidates.len(), 1); - assert_eq!(candidates[0].note_id, note_id); - assert_eq!(candidates[0].chunk_id, chunk_id); - assert_eq!(candidates[0].chunk_index, 2); - assert_eq!(candidates[0].snippet, "candidate"); - assert_eq!(candidates[0].retrieval_rank, 3); - assert_eq!(candidates[0].rerank_score, 0.75); - assert_eq!(candidates[0].note_scope, "project_shared"); - assert_eq!(candidates[0].note_importance, 0.5); - assert_eq!(candidates[0].note_updated_at, now); - assert_eq!(candidates[0].note_hit_count, 9); - assert_eq!(candidates[0].note_last_hit_at, Some(now)); - assert!(candidates[0].retrieval_score.is_none()); - } -} +#[cfg(test)] mod tests; diff --git a/apps/elf-eval/src/app/trace_compare/analysis/attribution.rs b/apps/elf-eval/src/app/trace_compare/analysis/attribution.rs new file mode 100644 index 00000000..37f31cfc --- /dev/null +++ b/apps/elf-eval/src/app/trace_compare/analysis/attribution.rs @@ -0,0 +1,46 @@ +use std::collections::HashMap; + +use crate::app::trace_compare::types::{ + TraceCompareChurn, TraceCompareGuardrails, TraceCompareRegressionAttribution, + TraceCompareStageDelta, +}; + +pub(in crate::app::trace_compare) fn build_trace_compare_regression_attribution( + churn: &TraceCompareChurn, + guardrails: &TraceCompareGuardrails, + stage_deltas: &[TraceCompareStageDelta], +) -> TraceCompareRegressionAttribution { + let stage_by_name: HashMap<&str, &TraceCompareStageDelta> = + stage_deltas.iter().map(|stage| (stage.stage_name.as_str(), stage)).collect(); + + if guardrails.retrieval_top3_retention_delta < 0.0 { + let recall_count = stage_by_name + .get("recall.candidates") + .map(|stage| stage.baseline_item_count) + .unwrap_or(0); + + return TraceCompareRegressionAttribution { + primary_stage: "selection.final".to_string(), + evidence: format!( + "retrieval_top3_retention dropped by {:.4} (a={:.4}, b={:.4}); recall baseline item_count={recall_count}", + guardrails.retrieval_top3_retention_delta, + guardrails.a_retrieval_top3_retention, + guardrails.b_retrieval_top3_retention + ), + }; + } + if churn.set_churn_at_k > 0.0 || churn.positional_churn_at_k > 0.0 { + return TraceCompareRegressionAttribution { + primary_stage: "rerank.score".to_string(), + evidence: format!( + "top-k churn changed without retrieval-top3 regression (set_churn_at_k={:.4}, positional_churn_at_k={:.4})", + churn.set_churn_at_k, churn.positional_churn_at_k + ), + }; + } + + TraceCompareRegressionAttribution { + primary_stage: "not_applicable".to_string(), + evidence: "No regression signal detected.".to_string(), + } +} diff --git a/apps/elf-eval/src/app/trace_compare/analysis/candidates.rs b/apps/elf-eval/src/app/trace_compare/analysis/candidates.rs new file mode 100644 index 00000000..2cafc06d --- /dev/null +++ b/apps/elf-eval/src/app/trace_compare/analysis/candidates.rs @@ -0,0 +1,40 @@ +use uuid::Uuid; + +use crate::app::trace_compare::types::TraceCompareCandidateRow; +use elf_service::search::TraceReplayCandidate; + +pub(in crate::app::trace_compare) fn decode_trace_replay_candidates( + rows: Vec, +) -> Vec { + rows.into_iter() + .map(|row| { + let decoded = + serde_json::from_value::(row.candidate_snapshot.clone()) + .ok() + .filter(|value| value.note_id != Uuid::nil() && value.chunk_id != Uuid::nil()); + + decoded.unwrap_or_else(|| TraceReplayCandidate { + note_id: row.note_id, + chunk_id: row.chunk_id, + chunk_index: row.chunk_index, + snippet: row.snippet, + retrieval_rank: u32::try_from(row.retrieval_rank).unwrap_or(0), + retrieval_score: None, + rerank_score: row.rerank_score, + note_scope: row.note_scope, + note_importance: row.note_importance, + note_updated_at: row.note_updated_at, + note_hit_count: row.note_hit_count, + note_last_hit_at: row.note_last_hit_at, + diversity_selected: None, + diversity_selected_rank: None, + diversity_selected_reason: None, + diversity_skipped_reason: None, + diversity_nearest_selected_note_id: None, + diversity_similarity: None, + diversity_mmr_score: None, + diversity_missing_embedding: None, + }) + }) + .collect() +} diff --git a/apps/elf-eval/src/app/trace_compare/analysis/stages.rs b/apps/elf-eval/src/app/trace_compare/analysis/stages.rs new file mode 100644 index 00000000..c6261c80 --- /dev/null +++ b/apps/elf-eval/src/app/trace_compare/analysis/stages.rs @@ -0,0 +1,43 @@ +use crate::app::trace_compare::types::{TraceCompareStageDelta, TraceCompareStageRow}; + +pub(in crate::app::trace_compare) fn build_trace_compare_stage_deltas( + stage_rows: &[TraceCompareStageRow], + a_selected_count: u32, + b_selected_count: u32, +) -> Vec { + if stage_rows.is_empty() { + return vec![TraceCompareStageDelta { + stage_order: 1, + stage_name: "selection.final".to_string(), + baseline_item_count: 0, + a_item_count: a_selected_count, + b_item_count: b_selected_count, + item_count_delta: b_selected_count as i64 - a_selected_count as i64, + baseline_stats: None, + }]; + } + + let mut out = Vec::with_capacity(stage_rows.len()); + + for row in stage_rows { + let baseline_item_count = row.item_count.max(0) as u32; + let (a_item_count, b_item_count) = if row.stage_name == "selection.final" { + (a_selected_count, b_selected_count) + } else { + (baseline_item_count, baseline_item_count) + }; + let baseline_stats = row.stage_payload.get("stats").cloned(); + + out.push(TraceCompareStageDelta { + stage_order: row.stage_order.max(0) as u32, + stage_name: row.stage_name.clone(), + baseline_item_count, + a_item_count, + b_item_count, + item_count_delta: b_item_count as i64 - a_item_count as i64, + baseline_stats, + }); + } + + out +} diff --git a/apps/elf-eval/src/app/trace_compare/analysis/tests.rs b/apps/elf-eval/src/app/trace_compare/analysis/tests.rs new file mode 100644 index 00000000..95dc740c --- /dev/null +++ b/apps/elf-eval/src/app/trace_compare/analysis/tests.rs @@ -0,0 +1,196 @@ +use serde_json::json; +use time::OffsetDateTime; +use uuid::Uuid; + +use crate::app::trace_compare::{ + analysis, + types::{ + TraceCompareCandidateRow, TraceCompareChurn, TraceCompareGuardrails, + TraceCompareStageDelta, TraceCompareStageRow, + }, +}; +use elf_service::search::TraceReplayCandidate; + +#[test] +fn stage_deltas_fallback_to_final_selection_when_baseline_stages_are_absent() { + let deltas = analysis::build_trace_compare_stage_deltas(&[], 2, 4); + + assert_eq!(deltas.len(), 1); + assert_eq!(deltas[0].stage_order, 1); + assert_eq!(deltas[0].stage_name, "selection.final"); + assert_eq!(deltas[0].baseline_item_count, 0); + assert_eq!(deltas[0].a_item_count, 2); + assert_eq!(deltas[0].b_item_count, 4); + assert_eq!(deltas[0].item_count_delta, 2); + assert!(deltas[0].baseline_stats.is_none()); +} + +#[test] +fn stage_deltas_replace_final_selection_counts_and_preserve_stats() { + let rows = vec![ + TraceCompareStageRow { + stage_order: 1, + stage_name: "recall.candidates".to_string(), + stage_payload: json!({"stats": {"source": "baseline"}}), + item_count: 7, + }, + TraceCompareStageRow { + stage_order: 2, + stage_name: "selection.final".to_string(), + stage_payload: json!({"stats": {"selected": true}}), + item_count: 5, + }, + ]; + let deltas = analysis::build_trace_compare_stage_deltas(&rows, 3, 4); + + assert_eq!(deltas[0].baseline_item_count, 7); + assert_eq!(deltas[0].a_item_count, 7); + assert_eq!(deltas[0].b_item_count, 7); + assert_eq!(deltas[0].baseline_stats, Some(json!({"source": "baseline"}))); + assert_eq!(deltas[1].baseline_item_count, 5); + assert_eq!(deltas[1].a_item_count, 3); + assert_eq!(deltas[1].b_item_count, 4); + assert_eq!(deltas[1].item_count_delta, 1); + assert_eq!(deltas[1].baseline_stats, Some(json!({"selected": true}))); +} + +#[test] +fn regression_attribution_prefers_retention_drop_with_recall_context() { + let churn = TraceCompareChurn { positional_churn_at_k: 0.0, set_churn_at_k: 0.0 }; + let guardrails = TraceCompareGuardrails { + retrieval_top3_total: 3, + a_retrieval_top3_retained: 3, + a_retrieval_top3_retention: 1.0, + b_retrieval_top3_retained: 2, + b_retrieval_top3_retention: 0.6667, + retrieval_top3_retention_delta: -0.3333, + }; + let stage_deltas = vec![TraceCompareStageDelta { + stage_order: 1, + stage_name: "recall.candidates".to_string(), + baseline_item_count: 12, + a_item_count: 12, + b_item_count: 12, + item_count_delta: 0, + baseline_stats: None, + }]; + let attribution = + analysis::build_trace_compare_regression_attribution(&churn, &guardrails, &stage_deltas); + + assert_eq!(attribution.primary_stage, "selection.final"); + assert!(attribution.evidence.contains("dropped by -0.3333")); + assert!(attribution.evidence.contains("recall baseline item_count=12")); +} + +#[test] +fn regression_attribution_uses_rerank_when_churn_changes_without_retention_drop() { + let churn = TraceCompareChurn { positional_churn_at_k: 0.5, set_churn_at_k: 0.25 }; + let guardrails = TraceCompareGuardrails { + retrieval_top3_total: 3, + a_retrieval_top3_retained: 2, + a_retrieval_top3_retention: 0.6667, + b_retrieval_top3_retained: 2, + b_retrieval_top3_retention: 0.6667, + retrieval_top3_retention_delta: 0.0, + }; + let attribution = + analysis::build_trace_compare_regression_attribution(&churn, &guardrails, &[]); + + assert_eq!(attribution.primary_stage, "rerank.score"); + assert!(attribution.evidence.contains("set_churn_at_k=0.2500")); + assert!(attribution.evidence.contains("positional_churn_at_k=0.5000")); +} + +#[test] +fn decode_candidates_falls_back_to_row_fields_when_snapshot_is_invalid() { + let now = OffsetDateTime::from_unix_timestamp(0).expect("Valid timestamp."); + let note_id = Uuid::new_v4(); + let chunk_id = Uuid::new_v4(); + let rows = vec![TraceCompareCandidateRow { + candidate_snapshot: json!({"invalid": true}), + note_id, + chunk_id, + chunk_index: 2, + snippet: "candidate".to_string(), + retrieval_rank: -1, + rerank_score: 0.75, + note_scope: "project_shared".to_string(), + note_importance: 0.5, + note_updated_at: now, + note_hit_count: 9, + note_last_hit_at: Some(now), + }]; + let candidates = analysis::decode_trace_replay_candidates(rows); + + assert_eq!(candidates.len(), 1); + assert_eq!(candidates[0].note_id, note_id); + assert_eq!(candidates[0].chunk_id, chunk_id); + assert_eq!(candidates[0].chunk_index, 2); + assert_eq!(candidates[0].snippet, "candidate"); + assert_eq!(candidates[0].retrieval_rank, 0); + assert_eq!(candidates[0].rerank_score, 0.75); + assert_eq!(candidates[0].note_scope, "project_shared"); + assert_eq!(candidates[0].note_importance, 0.5); + assert_eq!(candidates[0].note_updated_at, now); + assert_eq!(candidates[0].note_hit_count, 9); + assert_eq!(candidates[0].note_last_hit_at, Some(now)); + assert!(candidates[0].retrieval_score.is_none()); +} + +#[test] +fn decode_candidates_falls_back_when_valid_snapshot_has_nil_ids() { + let now = OffsetDateTime::from_unix_timestamp(0).expect("Valid timestamp."); + let note_id = Uuid::new_v4(); + let chunk_id = Uuid::new_v4(); + let snapshot = TraceReplayCandidate { + note_id: Uuid::nil(), + chunk_id: Uuid::new_v4(), + chunk_index: 99, + snippet: "snapshot".to_string(), + retrieval_rank: 1, + retrieval_score: Some(1.0), + rerank_score: 1.0, + note_scope: "snapshot_scope".to_string(), + note_importance: 1.0, + note_updated_at: now, + note_hit_count: 1, + note_last_hit_at: None, + diversity_selected: None, + diversity_selected_rank: None, + diversity_selected_reason: None, + diversity_skipped_reason: None, + diversity_nearest_selected_note_id: None, + diversity_similarity: None, + diversity_mmr_score: None, + diversity_missing_embedding: None, + }; + let rows = vec![TraceCompareCandidateRow { + candidate_snapshot: serde_json::to_value(snapshot).expect("Snapshot serializes."), + note_id, + chunk_id, + chunk_index: 2, + snippet: "candidate".to_string(), + retrieval_rank: 3, + rerank_score: 0.75, + note_scope: "project_shared".to_string(), + note_importance: 0.5, + note_updated_at: now, + note_hit_count: 9, + note_last_hit_at: Some(now), + }]; + let candidates = analysis::decode_trace_replay_candidates(rows); + + assert_eq!(candidates.len(), 1); + assert_eq!(candidates[0].note_id, note_id); + assert_eq!(candidates[0].chunk_id, chunk_id); + assert_eq!(candidates[0].chunk_index, 2); + assert_eq!(candidates[0].snippet, "candidate"); + assert_eq!(candidates[0].retrieval_rank, 3); + assert_eq!(candidates[0].rerank_score, 0.75); + assert_eq!(candidates[0].note_scope, "project_shared"); + assert_eq!(candidates[0].note_importance, 0.5); + assert_eq!(candidates[0].note_updated_at, now); + assert_eq!(candidates[0].note_hit_count, 9); + assert_eq!(candidates[0].note_last_hit_at, Some(now)); + assert!(candidates[0].retrieval_score.is_none()); +} From e81dfcf12a8b261df461a98b296bace66a14d1da Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Tue, 30 Jun 2026 11:46:36 -0400 Subject: [PATCH 3/5] {"schema":"decodex/commit/1","summary":"Split provenance note type modules","authority":"manual"} --- .../elf-service/src/provenance/types/notes.rs | 251 +----------------- .../src/provenance/types/notes/current.rs | 75 ++++++ .../src/provenance/types/notes/decision.rs | 67 +++++ .../src/provenance/types/notes/outbox.rs | 50 ++++ .../src/provenance/types/notes/trace.rs | 23 ++ .../src/provenance/types/notes/version.rs | 44 +++ 6 files changed, 269 insertions(+), 241 deletions(-) create mode 100644 packages/elf-service/src/provenance/types/notes/current.rs create mode 100644 packages/elf-service/src/provenance/types/notes/decision.rs create mode 100644 packages/elf-service/src/provenance/types/notes/outbox.rs create mode 100644 packages/elf-service/src/provenance/types/notes/trace.rs create mode 100644 packages/elf-service/src/provenance/types/notes/version.rs diff --git a/packages/elf-service/src/provenance/types/notes.rs b/packages/elf-service/src/provenance/types/notes.rs index ed5de168..fbaf0212 100644 --- a/packages/elf-service/src/provenance/types/notes.rs +++ b/packages/elf-service/src/provenance/types/notes.rs @@ -1,242 +1,11 @@ -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use time::OffsetDateTime; -use uuid::Uuid; - -use crate::provenance::types::rows::{ - NoteIndexingOutboxRow, NoteIngestDecisionRow, NoteVersionRow, +mod current; +mod decision; +mod outbox; +mod trace; +mod version; + +pub use self::{ + current::NoteProvenanceNote, decision::NoteProvenanceIngestDecision, + outbox::NoteProvenanceIndexingOutbox, trace::NoteProvenanceRecentTrace, + version::NoteProvenanceNoteVersion, }; -use elf_storage::models::MemoryNote; - -/// Current note snapshot returned by provenance APIs. -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct NoteProvenanceNote { - /// Note identifier. - pub note_id: Uuid, - /// Tenant that owns the note. - pub tenant_id: String, - /// Project that owns the note. - pub project_id: String, - /// Agent that wrote the note. - pub agent_id: String, - /// Scope key for the note. - pub scope: String, - /// Note type discriminator. - pub r#type: String, - /// Optional application-defined key. - pub key: Option, - /// Note body text. - pub text: String, - /// Importance score. - pub importance: f32, - /// Confidence score. - pub confidence: f32, - /// Lifecycle status. - pub status: String, - #[serde(with = "crate::time_serde")] - /// Creation timestamp. - pub created_at: OffsetDateTime, - #[serde(with = "crate::time_serde")] - /// Last update timestamp. - pub updated_at: OffsetDateTime, - #[serde(with = "crate::time_serde::option")] - /// Optional expiry timestamp. - pub expires_at: Option, - /// Structured source reference metadata. - pub source_ref: Value, - /// Embedding version associated with the note. - pub embedding_version: String, - /// Search hit counter. - pub hit_count: i64, - #[serde(with = "crate::time_serde::option")] - /// Timestamp of the most recent hit. - pub last_hit_at: Option, -} -impl From for NoteProvenanceNote { - fn from(note: MemoryNote) -> Self { - Self { - note_id: note.note_id, - tenant_id: note.tenant_id, - project_id: note.project_id, - agent_id: note.agent_id, - scope: note.scope, - r#type: note.r#type, - key: note.key, - text: note.text, - importance: note.importance, - confidence: note.confidence, - status: note.status, - created_at: note.created_at, - updated_at: note.updated_at, - expires_at: note.expires_at, - source_ref: note.source_ref, - embedding_version: note.embedding_version, - hit_count: note.hit_count, - last_hit_at: note.last_hit_at, - } - } -} - -/// One recorded ingestion decision for a note. -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct NoteProvenanceIngestDecision { - /// Decision identifier. - pub decision_id: Uuid, - /// Tenant that owns the decision record. - pub tenant_id: String, - /// Project that owns the decision record. - pub project_id: String, - /// Agent that triggered the ingestion decision. - pub agent_id: String, - /// Scope key evaluated by the decision. - pub scope: String, - /// Pipeline name that produced the decision. - pub pipeline: String, - /// Note type discriminator under evaluation. - pub note_type: String, - /// Optional application-defined key under evaluation. - pub note_key: Option, - /// Note identifier, when a note was persisted or matched. - pub note_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - /// Note version produced by this decision, when applicable. - pub note_version_id: Option, - /// Pre-policy base decision. - pub base_decision: String, - /// Final policy decision. - pub policy_decision: String, - /// Persistence operation that followed the decision. - pub note_op: String, - /// Machine-readable reason code, if any. - pub reason_code: Option, - /// Structured diagnostic details. - pub details: Value, - #[serde(with = "crate::time_serde")] - /// Decision timestamp. - pub ts: OffsetDateTime, -} -impl From for NoteProvenanceIngestDecision { - fn from(row: NoteIngestDecisionRow) -> Self { - Self { - decision_id: row.decision_id, - tenant_id: row.tenant_id, - project_id: row.project_id, - agent_id: row.agent_id, - scope: row.scope, - pipeline: row.pipeline, - note_type: row.note_type, - note_key: row.note_key, - note_id: row.note_id, - note_version_id: row.note_version_id, - base_decision: row.base_decision, - policy_decision: row.policy_decision, - note_op: row.note_op, - reason_code: row.reason_code, - details: row.details, - ts: row.ts, - } - } -} - -/// One version-history row for a note. -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct NoteProvenanceNoteVersion { - /// Version row identifier. - pub version_id: Uuid, - /// Note identifier. - pub note_id: Uuid, - /// Operation recorded in the version row. - pub op: String, - #[serde(skip_serializing_if = "Option::is_none")] - /// Snapshot before the operation, when available. - pub prev_snapshot: Option, - #[serde(skip_serializing_if = "Option::is_none")] - /// Snapshot after the operation, when available. - pub new_snapshot: Option, - /// Human-readable reason for the change. - pub reason: String, - /// Actor that performed the change. - pub actor: String, - #[serde(with = "crate::time_serde")] - /// Version timestamp. - pub ts: OffsetDateTime, -} -impl From for NoteProvenanceNoteVersion { - fn from(row: NoteVersionRow) -> Self { - Self { - version_id: row.version_id, - note_id: row.note_id, - op: row.op, - prev_snapshot: row.prev_snapshot, - new_snapshot: row.new_snapshot, - reason: row.reason, - actor: row.actor, - ts: row.ts, - } - } -} - -/// One indexing-outbox row for a note. -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct NoteProvenanceIndexingOutbox { - /// Outbox identifier. - pub outbox_id: Uuid, - /// Note identifier. - pub note_id: Uuid, - /// Requested indexing operation. - pub op: String, - /// Embedding version targeted by the job. - pub embedding_version: String, - /// Current outbox status. - pub status: String, - /// Number of attempts already made. - pub attempts: i32, - #[serde(skip_serializing_if = "Option::is_none")] - /// Most recent failure text, if any. - pub last_error: Option, - #[serde(with = "crate::time_serde")] - /// Earliest time the job may be claimed again. - pub available_at: OffsetDateTime, - #[serde(with = "crate::time_serde")] - /// Creation timestamp. - pub created_at: OffsetDateTime, - #[serde(with = "crate::time_serde")] - /// Last update timestamp. - pub updated_at: OffsetDateTime, -} -impl From for NoteProvenanceIndexingOutbox { - fn from(row: NoteIndexingOutboxRow) -> Self { - Self { - outbox_id: row.outbox_id, - note_id: row.note_id, - op: row.op, - embedding_version: row.embedding_version, - status: row.status, - attempts: row.attempts, - last_error: row.last_error, - available_at: row.available_at, - created_at: row.created_at, - updated_at: row.updated_at, - } - } -} - -/// Recent search trace that referenced the note. -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct NoteProvenanceRecentTrace { - /// Search trace identifier. - pub trace_id: Uuid, - /// Tenant that owns the trace. - pub tenant_id: String, - /// Project that owns the trace. - pub project_id: String, - /// Agent that ran the search. - pub agent_id: String, - /// Read profile used for the trace. - pub read_profile: String, - /// Search query text. - pub query: String, - #[serde(with = "crate::time_serde")] - /// Trace creation timestamp. - pub created_at: OffsetDateTime, -} diff --git a/packages/elf-service/src/provenance/types/notes/current.rs b/packages/elf-service/src/provenance/types/notes/current.rs new file mode 100644 index 00000000..9e45dee3 --- /dev/null +++ b/packages/elf-service/src/provenance/types/notes/current.rs @@ -0,0 +1,75 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use time::OffsetDateTime; +use uuid::Uuid; + +use elf_storage::models::MemoryNote; + +/// Current note snapshot returned by provenance APIs. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct NoteProvenanceNote { + /// Note identifier. + pub note_id: Uuid, + /// Tenant that owns the note. + pub tenant_id: String, + /// Project that owns the note. + pub project_id: String, + /// Agent that wrote the note. + pub agent_id: String, + /// Scope key for the note. + pub scope: String, + /// Note type discriminator. + pub r#type: String, + /// Optional application-defined key. + pub key: Option, + /// Note body text. + pub text: String, + /// Importance score. + pub importance: f32, + /// Confidence score. + pub confidence: f32, + /// Lifecycle status. + pub status: String, + #[serde(with = "crate::time_serde")] + /// Creation timestamp. + pub created_at: OffsetDateTime, + #[serde(with = "crate::time_serde")] + /// Last update timestamp. + pub updated_at: OffsetDateTime, + #[serde(with = "crate::time_serde::option")] + /// Optional expiry timestamp. + pub expires_at: Option, + /// Structured source reference metadata. + pub source_ref: Value, + /// Embedding version associated with the note. + pub embedding_version: String, + /// Search hit counter. + pub hit_count: i64, + #[serde(with = "crate::time_serde::option")] + /// Timestamp of the most recent hit. + pub last_hit_at: Option, +} +impl From for NoteProvenanceNote { + fn from(note: MemoryNote) -> Self { + Self { + note_id: note.note_id, + tenant_id: note.tenant_id, + project_id: note.project_id, + agent_id: note.agent_id, + scope: note.scope, + r#type: note.r#type, + key: note.key, + text: note.text, + importance: note.importance, + confidence: note.confidence, + status: note.status, + created_at: note.created_at, + updated_at: note.updated_at, + expires_at: note.expires_at, + source_ref: note.source_ref, + embedding_version: note.embedding_version, + hit_count: note.hit_count, + last_hit_at: note.last_hit_at, + } + } +} diff --git a/packages/elf-service/src/provenance/types/notes/decision.rs b/packages/elf-service/src/provenance/types/notes/decision.rs new file mode 100644 index 00000000..a09b1f9f --- /dev/null +++ b/packages/elf-service/src/provenance/types/notes/decision.rs @@ -0,0 +1,67 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use time::OffsetDateTime; +use uuid::Uuid; + +use crate::provenance::types::rows::NoteIngestDecisionRow; + +/// One recorded ingestion decision for a note. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct NoteProvenanceIngestDecision { + /// Decision identifier. + pub decision_id: Uuid, + /// Tenant that owns the decision record. + pub tenant_id: String, + /// Project that owns the decision record. + pub project_id: String, + /// Agent that triggered the ingestion decision. + pub agent_id: String, + /// Scope key evaluated by the decision. + pub scope: String, + /// Pipeline name that produced the decision. + pub pipeline: String, + /// Note type discriminator under evaluation. + pub note_type: String, + /// Optional application-defined key under evaluation. + pub note_key: Option, + /// Note identifier, when a note was persisted or matched. + pub note_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + /// Note version produced by this decision, when applicable. + pub note_version_id: Option, + /// Pre-policy base decision. + pub base_decision: String, + /// Final policy decision. + pub policy_decision: String, + /// Persistence operation that followed the decision. + pub note_op: String, + /// Machine-readable reason code, if any. + pub reason_code: Option, + /// Structured diagnostic details. + pub details: Value, + #[serde(with = "crate::time_serde")] + /// Decision timestamp. + pub ts: OffsetDateTime, +} +impl From for NoteProvenanceIngestDecision { + fn from(row: NoteIngestDecisionRow) -> Self { + Self { + decision_id: row.decision_id, + tenant_id: row.tenant_id, + project_id: row.project_id, + agent_id: row.agent_id, + scope: row.scope, + pipeline: row.pipeline, + note_type: row.note_type, + note_key: row.note_key, + note_id: row.note_id, + note_version_id: row.note_version_id, + base_decision: row.base_decision, + policy_decision: row.policy_decision, + note_op: row.note_op, + reason_code: row.reason_code, + details: row.details, + ts: row.ts, + } + } +} diff --git a/packages/elf-service/src/provenance/types/notes/outbox.rs b/packages/elf-service/src/provenance/types/notes/outbox.rs new file mode 100644 index 00000000..708337e8 --- /dev/null +++ b/packages/elf-service/src/provenance/types/notes/outbox.rs @@ -0,0 +1,50 @@ +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; +use uuid::Uuid; + +use crate::provenance::types::rows::NoteIndexingOutboxRow; + +/// One indexing-outbox row for a note. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct NoteProvenanceIndexingOutbox { + /// Outbox identifier. + pub outbox_id: Uuid, + /// Note identifier. + pub note_id: Uuid, + /// Requested indexing operation. + pub op: String, + /// Embedding version targeted by the job. + pub embedding_version: String, + /// Current outbox status. + pub status: String, + /// Number of attempts already made. + pub attempts: i32, + #[serde(skip_serializing_if = "Option::is_none")] + /// Most recent failure text, if any. + pub last_error: Option, + #[serde(with = "crate::time_serde")] + /// Earliest time the job may be claimed again. + pub available_at: OffsetDateTime, + #[serde(with = "crate::time_serde")] + /// Creation timestamp. + pub created_at: OffsetDateTime, + #[serde(with = "crate::time_serde")] + /// Last update timestamp. + pub updated_at: OffsetDateTime, +} +impl From for NoteProvenanceIndexingOutbox { + fn from(row: NoteIndexingOutboxRow) -> Self { + Self { + outbox_id: row.outbox_id, + note_id: row.note_id, + op: row.op, + embedding_version: row.embedding_version, + status: row.status, + attempts: row.attempts, + last_error: row.last_error, + available_at: row.available_at, + created_at: row.created_at, + updated_at: row.updated_at, + } + } +} diff --git a/packages/elf-service/src/provenance/types/notes/trace.rs b/packages/elf-service/src/provenance/types/notes/trace.rs new file mode 100644 index 00000000..76a3a1a5 --- /dev/null +++ b/packages/elf-service/src/provenance/types/notes/trace.rs @@ -0,0 +1,23 @@ +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; +use uuid::Uuid; + +/// Recent search trace that referenced the note. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct NoteProvenanceRecentTrace { + /// Search trace identifier. + pub trace_id: Uuid, + /// Tenant that owns the trace. + pub tenant_id: String, + /// Project that owns the trace. + pub project_id: String, + /// Agent that ran the search. + pub agent_id: String, + /// Read profile used for the trace. + pub read_profile: String, + /// Search query text. + pub query: String, + #[serde(with = "crate::time_serde")] + /// Trace creation timestamp. + pub created_at: OffsetDateTime, +} diff --git a/packages/elf-service/src/provenance/types/notes/version.rs b/packages/elf-service/src/provenance/types/notes/version.rs new file mode 100644 index 00000000..64c4536a --- /dev/null +++ b/packages/elf-service/src/provenance/types/notes/version.rs @@ -0,0 +1,44 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use time::OffsetDateTime; +use uuid::Uuid; + +use crate::provenance::types::rows::NoteVersionRow; + +/// One version-history row for a note. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct NoteProvenanceNoteVersion { + /// Version row identifier. + pub version_id: Uuid, + /// Note identifier. + pub note_id: Uuid, + /// Operation recorded in the version row. + pub op: String, + #[serde(skip_serializing_if = "Option::is_none")] + /// Snapshot before the operation, when available. + pub prev_snapshot: Option, + #[serde(skip_serializing_if = "Option::is_none")] + /// Snapshot after the operation, when available. + pub new_snapshot: Option, + /// Human-readable reason for the change. + pub reason: String, + /// Actor that performed the change. + pub actor: String, + #[serde(with = "crate::time_serde")] + /// Version timestamp. + pub ts: OffsetDateTime, +} +impl From for NoteProvenanceNoteVersion { + fn from(row: NoteVersionRow) -> Self { + Self { + version_id: row.version_id, + note_id: row.note_id, + op: row.op, + prev_snapshot: row.prev_snapshot, + new_snapshot: row.new_snapshot, + reason: row.reason, + actor: row.actor, + ts: row.ts, + } + } +} From 1b6e34689c2d1c0fe76503b2cf3efa9e0a639e31 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Tue, 30 Jun 2026 11:51:28 -0400 Subject: [PATCH 4/5] {"schema":"decodex/commit/1","summary":"Split docs internal type modules","authority":"manual"} --- packages/elf-service/src/docs.rs | 2 +- packages/elf-service/src/docs/types.rs | 248 ++---------------- .../elf-service/src/docs/types/capture.rs | 20 ++ packages/elf-service/src/docs/types/chunks.rs | 16 ++ .../elf-service/src/docs/types/constants.rs | 28 ++ .../elf-service/src/docs/types/excerpts.rs | 38 +++ packages/elf-service/src/docs/types/put.rs | 9 + packages/elf-service/src/docs/types/search.rs | 93 +++++++ .../elf-service/src/docs/types/trajectory.rs | 42 +++ 9 files changed, 271 insertions(+), 225 deletions(-) create mode 100644 packages/elf-service/src/docs/types/capture.rs create mode 100644 packages/elf-service/src/docs/types/chunks.rs create mode 100644 packages/elf-service/src/docs/types/constants.rs create mode 100644 packages/elf-service/src/docs/types/excerpts.rs create mode 100644 packages/elf-service/src/docs/types/put.rs create mode 100644 packages/elf-service/src/docs/types/search.rs create mode 100644 packages/elf-service/src/docs/types/trajectory.rs diff --git a/packages/elf-service/src/docs.rs b/packages/elf-service/src/docs.rs index c5534dcb..e641bcb0 100644 --- a/packages/elf-service/src/docs.rs +++ b/packages/elf-service/src/docs.rs @@ -32,7 +32,7 @@ use qdrant_client::{ }, }; use serde_json::{Map, Value}; -use sqlx::{FromRow, PgExecutor, PgPool}; +use sqlx::{PgExecutor, PgPool}; use time::{OffsetDateTime, format_description::well_known::Rfc3339}; use tokenizers::Tokenizer; use uuid::Uuid; diff --git a/packages/elf-service/src/docs/types.rs b/packages/elf-service/src/docs/types.rs index 8916c50f..244c4b6a 100644 --- a/packages/elf-service/src/docs/types.rs +++ b/packages/elf-service/src/docs/types.rs @@ -1,225 +1,25 @@ -use crate::docs::{ - DocChunk, DocRetrievalTrajectory, DocRetrievalTrajectoryStage, DocType, Filter, FromRow, - HashSet, Map, OffsetDateTime, SharedSpaceGrantKey, Uuid, Value, WritePolicyAudit, +mod capture; +mod chunks; +mod constants; +mod excerpts; +mod put; +mod search; +mod trajectory; + +pub(super) use self::{ + capture::SourceCaptureSummaryInput, + chunks::{ByteChunk, DocChunkingProfile}, + constants::{ + DEFAULT_DOC_MAX_BYTES, DEFAULT_L0_MAX_BYTES, DEFAULT_L1_MAX_BYTES, DEFAULT_L2_MAX_BYTES, + DEFAULT_MAX_CHUNKS_PER_DOC, DOC_SOURCE_CAPTURE_SCHEMA_V1, DOC_SOURCE_REF_RESOLVER_V1, + DOC_SOURCE_REF_SCHEMA_V1, DOC_SOURCE_SPAN_SCHEMA_V1, DOC_STATUSES, MAX_CANDIDATE_K, + MAX_TOP_K, SOURCE_LIBRARY_FIELD_KEYS, SOURCE_LIBRARY_KINDS, SOURCE_LIBRARY_TRUST_LABELS, + }, + excerpts::{DocExcerptMatch, DocExcerptRange, ExcerptsSelectorKind}, + put::ValidatedDocsPut, + search::{ + DocSearchRow, DocsSearchL0Filters, DocsSearchL0FiltersParsed, DocsSearchL0Prepared, + DocsSearchL0RangesParsed, DocsSparseMode, + }, + trajectory::DocTrajectoryBuilder, }; - -pub(super) const MAX_TOP_K: u32 = 32; -pub(super) const MAX_CANDIDATE_K: u32 = 1_024; -pub(super) const DEFAULT_DOC_MAX_BYTES: usize = 4 * 1_024 * 1_024; -pub(super) const DEFAULT_MAX_CHUNKS_PER_DOC: usize = 4_096; -pub(super) const DEFAULT_L0_MAX_BYTES: usize = 256; -pub(super) const DEFAULT_L1_MAX_BYTES: usize = 8 * 1_024; -pub(super) const DEFAULT_L2_MAX_BYTES: usize = 32 * 1_024; -pub(super) const DOC_RETRIEVAL_TRAJECTORY_SCHEMA_V1: &str = "doc_retrieval_trajectory/v1"; -pub(super) const DOC_SOURCE_REF_SCHEMA_V1: &str = "source_ref/v1"; -pub(super) const DOC_SOURCE_REF_RESOLVER_V1: &str = "elf_doc_ext/v1"; -pub(super) const DOC_SOURCE_CAPTURE_SCHEMA_V1: &str = "doc_source_capture/v1"; -pub(super) const DOC_SOURCE_SPAN_SCHEMA_V1: &str = "doc_source_span/v1"; -pub(super) const DOC_STATUSES: [&str; 2] = ["active", "deleted"]; -pub(super) const SOURCE_LIBRARY_FIELD_KEYS: [&str; 9] = [ - "source_kind", - "canonical_uri", - "captured_at", - "source_created_at", - "trust_label", - "author", - "handle", - "excerpt_locator", - "source_content_hash", -]; -pub(super) const SOURCE_LIBRARY_KINDS: [&str; 7] = - ["article", "social_thread", "pdf", "text_export", "repo_file", "chat_excerpt", "web_page"]; -pub(super) const SOURCE_LIBRARY_TRUST_LABELS: [&str; 5] = - ["trusted", "user_captured", "public_web", "third_party", "unverified"]; - -pub(super) struct SourceCaptureSummaryInput<'a> { - pub(super) doc_id: Uuid, - pub(super) source_ref: &'a Map, - pub(super) doc_type: DocType, - pub(super) scope: &'a str, - pub(super) title: Option<&'a str>, - pub(super) content_hash: &'a str, - pub(super) raw_content_hash: &'a str, - pub(super) now: OffsetDateTime, - pub(super) chunks: &'a [DocChunk], - pub(super) write_policy_audit: Option<&'a WritePolicyAudit>, -} - -#[derive(Clone, Copy)] -pub(super) struct DocExcerptMatch { - pub(super) selector_kind: ExcerptsSelectorKind, - pub(super) match_start_offset: usize, - pub(super) match_end_offset: usize, -} - -pub(super) struct DocExcerptRange { - pub(super) selector_kind: ExcerptsSelectorKind, - pub(super) match_start_offset: usize, - pub(super) match_end_offset: usize, - pub(super) start_offset: usize, - pub(super) end_offset: usize, -} - -pub(super) struct DocTrajectoryBuilder { - pub(super) explain: bool, - pub(super) stages: Vec, - pub(super) stage_order: u32, -} -impl DocTrajectoryBuilder { - pub(super) fn new(explain: bool) -> Self { - Self { explain, stages: Vec::new(), stage_order: 0 } - } - - pub(super) fn push(&mut self, stage_name: &str, stats: Value) { - if !self.explain { - return; - } - - self.stages.push(DocRetrievalTrajectoryStage { - stage_order: self.stage_order, - stage_name: stage_name.to_string(), - stats, - }); - - self.stage_order += 1; - } - - pub(super) fn into_trajectory(self) -> Option { - if !self.explain { - return None; - } - - Some(DocRetrievalTrajectory { - schema: DOC_RETRIEVAL_TRAJECTORY_SCHEMA_V1.to_string(), - stages: self.stages, - }) - } -} - -#[derive(Clone, Debug)] -pub(super) struct DocsSearchL0Filters { - pub(super) scope: Option, - pub(super) status: String, - pub(super) doc_type: Option, - pub(super) sparse_mode: DocsSparseMode, - pub(super) domain: Option, - pub(super) repo: Option, - pub(super) agent_id: Option, - pub(super) thread_id: Option, - pub(super) updated_after: Option, - pub(super) updated_before: Option, - pub(super) ts_gte: Option, - pub(super) ts_lte: Option, -} - -#[derive(Clone, Copy, Debug)] -pub(super) struct DocChunkingProfile { - pub(super) max_tokens: usize, - pub(super) overlap_tokens: usize, - pub(super) max_chunks: usize, -} - -#[derive(Clone, Debug)] -pub(super) struct ByteChunk { - pub(super) chunk_id: Uuid, - pub(super) start_offset: usize, - pub(super) end_offset: usize, - pub(super) text: String, -} - -#[derive(Debug)] -pub(super) struct ValidatedDocsPut { - pub(super) doc_type: DocType, - pub(super) content: String, - pub(super) write_policy_audit: Option, -} - -#[derive(Clone, Debug, FromRow)] -pub(super) struct DocSearchRow { - pub(super) chunk_id: Uuid, - pub(super) doc_id: Uuid, - pub(super) scope: String, - pub(super) doc_type: String, - pub(super) project_id: String, - pub(super) agent_id: String, - pub(super) updated_at: OffsetDateTime, - pub(super) content_hash: String, - pub(super) chunk_hash: String, - pub(super) start_offset: i32, - pub(super) end_offset: i32, - pub(super) chunk_text: String, -} - -pub(super) struct DocsSearchL0Prepared { - pub(super) top_k: u32, - pub(super) candidate_k: u32, - pub(super) sparse_mode: DocsSparseMode, - pub(super) sparse_enabled: bool, - pub(super) now: OffsetDateTime, - pub(super) trajectory: DocTrajectoryBuilder, - pub(super) allowed_scopes: Vec, - pub(super) shared_grants: HashSet, - pub(super) filter: Filter, - pub(super) vector: Vec, - pub(super) status: String, -} - -#[derive(Debug)] -pub(super) struct DocsSearchL0FiltersParsed { - pub(super) scope: Option, - pub(super) status: String, - pub(super) doc_type: Option, - pub(super) sparse_mode: DocsSparseMode, - pub(super) domain: Option, - pub(super) repo: Option, - pub(super) agent_id: Option, - pub(super) thread_id: Option, -} - -#[derive(Debug)] -pub(super) struct DocsSearchL0RangesParsed { - pub(super) updated_after: Option, - pub(super) updated_before: Option, - pub(super) ts_gte: Option, - pub(super) ts_lte: Option, -} - -#[derive(Clone, Copy, Debug)] -pub(super) enum DocsSparseMode { - Auto, - On, - Off, -} -impl DocsSparseMode { - pub(super) fn as_str(self) -> &'static str { - match self { - Self::Auto => "auto", - Self::On => "on", - Self::Off => "off", - } - } -} - -#[derive(Clone, Copy)] -pub(super) enum ExcerptsSelectorKind { - ChunkId, - Quote, - Position, -} -impl ExcerptsSelectorKind { - pub(super) fn as_str(&self) -> &'static str { - match self { - Self::ChunkId => "chunk_id", - Self::Quote => "quote", - Self::Position => "position", - } - } - - pub(super) fn span_kind(&self) -> &'static str { - match self { - Self::ChunkId => "captured", - Self::Quote => "quote", - Self::Position => "position", - } - } -} diff --git a/packages/elf-service/src/docs/types/capture.rs b/packages/elf-service/src/docs/types/capture.rs new file mode 100644 index 00000000..3bf0ce9b --- /dev/null +++ b/packages/elf-service/src/docs/types/capture.rs @@ -0,0 +1,20 @@ +use serde_json::{Map, Value}; +use time::OffsetDateTime; +use uuid::Uuid; + +use crate::docs::DocType; +use elf_domain::writegate::WritePolicyAudit; +use elf_storage::models::DocChunk; + +pub(in crate::docs) struct SourceCaptureSummaryInput<'a> { + pub(in crate::docs) doc_id: Uuid, + pub(in crate::docs) source_ref: &'a Map, + pub(in crate::docs) doc_type: DocType, + pub(in crate::docs) scope: &'a str, + pub(in crate::docs) title: Option<&'a str>, + pub(in crate::docs) content_hash: &'a str, + pub(in crate::docs) raw_content_hash: &'a str, + pub(in crate::docs) now: OffsetDateTime, + pub(in crate::docs) chunks: &'a [DocChunk], + pub(in crate::docs) write_policy_audit: Option<&'a WritePolicyAudit>, +} diff --git a/packages/elf-service/src/docs/types/chunks.rs b/packages/elf-service/src/docs/types/chunks.rs new file mode 100644 index 00000000..5012d4e5 --- /dev/null +++ b/packages/elf-service/src/docs/types/chunks.rs @@ -0,0 +1,16 @@ +use uuid::Uuid; + +#[derive(Clone, Copy, Debug)] +pub(in crate::docs) struct DocChunkingProfile { + pub(in crate::docs) max_tokens: usize, + pub(in crate::docs) overlap_tokens: usize, + pub(in crate::docs) max_chunks: usize, +} + +#[derive(Clone, Debug)] +pub(in crate::docs) struct ByteChunk { + pub(in crate::docs) chunk_id: Uuid, + pub(in crate::docs) start_offset: usize, + pub(in crate::docs) end_offset: usize, + pub(in crate::docs) text: String, +} diff --git a/packages/elf-service/src/docs/types/constants.rs b/packages/elf-service/src/docs/types/constants.rs new file mode 100644 index 00000000..18ff18e6 --- /dev/null +++ b/packages/elf-service/src/docs/types/constants.rs @@ -0,0 +1,28 @@ +pub(in crate::docs) const MAX_TOP_K: u32 = 32; +pub(in crate::docs) const MAX_CANDIDATE_K: u32 = 1_024; +pub(in crate::docs) const DEFAULT_DOC_MAX_BYTES: usize = 4 * 1_024 * 1_024; +pub(in crate::docs) const DEFAULT_MAX_CHUNKS_PER_DOC: usize = 4_096; +pub(in crate::docs) const DEFAULT_L0_MAX_BYTES: usize = 256; +pub(in crate::docs) const DEFAULT_L1_MAX_BYTES: usize = 8 * 1_024; +pub(in crate::docs) const DEFAULT_L2_MAX_BYTES: usize = 32 * 1_024; +pub(in crate::docs) const DOC_RETRIEVAL_TRAJECTORY_SCHEMA_V1: &str = "doc_retrieval_trajectory/v1"; +pub(in crate::docs) const DOC_SOURCE_REF_SCHEMA_V1: &str = "source_ref/v1"; +pub(in crate::docs) const DOC_SOURCE_REF_RESOLVER_V1: &str = "elf_doc_ext/v1"; +pub(in crate::docs) const DOC_SOURCE_CAPTURE_SCHEMA_V1: &str = "doc_source_capture/v1"; +pub(in crate::docs) const DOC_SOURCE_SPAN_SCHEMA_V1: &str = "doc_source_span/v1"; +pub(in crate::docs) const DOC_STATUSES: [&str; 2] = ["active", "deleted"]; +pub(in crate::docs) const SOURCE_LIBRARY_FIELD_KEYS: [&str; 9] = [ + "source_kind", + "canonical_uri", + "captured_at", + "source_created_at", + "trust_label", + "author", + "handle", + "excerpt_locator", + "source_content_hash", +]; +pub(in crate::docs) const SOURCE_LIBRARY_KINDS: [&str; 7] = + ["article", "social_thread", "pdf", "text_export", "repo_file", "chat_excerpt", "web_page"]; +pub(in crate::docs) const SOURCE_LIBRARY_TRUST_LABELS: [&str; 5] = + ["trusted", "user_captured", "public_web", "third_party", "unverified"]; diff --git a/packages/elf-service/src/docs/types/excerpts.rs b/packages/elf-service/src/docs/types/excerpts.rs new file mode 100644 index 00000000..aa596a15 --- /dev/null +++ b/packages/elf-service/src/docs/types/excerpts.rs @@ -0,0 +1,38 @@ +#[derive(Clone, Copy)] +pub(in crate::docs) struct DocExcerptMatch { + pub(in crate::docs) selector_kind: ExcerptsSelectorKind, + pub(in crate::docs) match_start_offset: usize, + pub(in crate::docs) match_end_offset: usize, +} + +pub(in crate::docs) struct DocExcerptRange { + pub(in crate::docs) selector_kind: ExcerptsSelectorKind, + pub(in crate::docs) match_start_offset: usize, + pub(in crate::docs) match_end_offset: usize, + pub(in crate::docs) start_offset: usize, + pub(in crate::docs) end_offset: usize, +} + +#[derive(Clone, Copy)] +pub(in crate::docs) enum ExcerptsSelectorKind { + ChunkId, + Quote, + Position, +} +impl ExcerptsSelectorKind { + pub(in crate::docs) fn as_str(&self) -> &'static str { + match self { + Self::ChunkId => "chunk_id", + Self::Quote => "quote", + Self::Position => "position", + } + } + + pub(in crate::docs) fn span_kind(&self) -> &'static str { + match self { + Self::ChunkId => "captured", + Self::Quote => "quote", + Self::Position => "position", + } + } +} diff --git a/packages/elf-service/src/docs/types/put.rs b/packages/elf-service/src/docs/types/put.rs new file mode 100644 index 00000000..e511fe75 --- /dev/null +++ b/packages/elf-service/src/docs/types/put.rs @@ -0,0 +1,9 @@ +use crate::docs::DocType; +use elf_domain::writegate::WritePolicyAudit; + +#[derive(Debug)] +pub(in crate::docs) struct ValidatedDocsPut { + pub(in crate::docs) doc_type: DocType, + pub(in crate::docs) content: String, + pub(in crate::docs) write_policy_audit: Option, +} diff --git a/packages/elf-service/src/docs/types/search.rs b/packages/elf-service/src/docs/types/search.rs new file mode 100644 index 00000000..39dede91 --- /dev/null +++ b/packages/elf-service/src/docs/types/search.rs @@ -0,0 +1,93 @@ +use std::collections::HashSet; + +use qdrant_client::qdrant::Filter; +use sqlx::FromRow; +use time::OffsetDateTime; +use uuid::Uuid; + +use crate::{ + access::SharedSpaceGrantKey, + docs::{DocType, types::trajectory::DocTrajectoryBuilder}, +}; + +#[derive(Clone, Debug)] +pub(in crate::docs) struct DocsSearchL0Filters { + pub(in crate::docs) scope: Option, + pub(in crate::docs) status: String, + pub(in crate::docs) doc_type: Option, + pub(in crate::docs) sparse_mode: DocsSparseMode, + pub(in crate::docs) domain: Option, + pub(in crate::docs) repo: Option, + pub(in crate::docs) agent_id: Option, + pub(in crate::docs) thread_id: Option, + pub(in crate::docs) updated_after: Option, + pub(in crate::docs) updated_before: Option, + pub(in crate::docs) ts_gte: Option, + pub(in crate::docs) ts_lte: Option, +} + +#[derive(Clone, Debug, FromRow)] +pub(in crate::docs) struct DocSearchRow { + pub(in crate::docs) chunk_id: Uuid, + pub(in crate::docs) doc_id: Uuid, + pub(in crate::docs) scope: String, + pub(in crate::docs) doc_type: String, + pub(in crate::docs) project_id: String, + pub(in crate::docs) agent_id: String, + pub(in crate::docs) updated_at: OffsetDateTime, + pub(in crate::docs) content_hash: String, + pub(in crate::docs) chunk_hash: String, + pub(in crate::docs) start_offset: i32, + pub(in crate::docs) end_offset: i32, + pub(in crate::docs) chunk_text: String, +} + +pub(in crate::docs) struct DocsSearchL0Prepared { + pub(in crate::docs) top_k: u32, + pub(in crate::docs) candidate_k: u32, + pub(in crate::docs) sparse_mode: DocsSparseMode, + pub(in crate::docs) sparse_enabled: bool, + pub(in crate::docs) now: OffsetDateTime, + pub(in crate::docs) trajectory: DocTrajectoryBuilder, + pub(in crate::docs) allowed_scopes: Vec, + pub(in crate::docs) shared_grants: HashSet, + pub(in crate::docs) filter: Filter, + pub(in crate::docs) vector: Vec, + pub(in crate::docs) status: String, +} + +#[derive(Debug)] +pub(in crate::docs) struct DocsSearchL0FiltersParsed { + pub(in crate::docs) scope: Option, + pub(in crate::docs) status: String, + pub(in crate::docs) doc_type: Option, + pub(in crate::docs) sparse_mode: DocsSparseMode, + pub(in crate::docs) domain: Option, + pub(in crate::docs) repo: Option, + pub(in crate::docs) agent_id: Option, + pub(in crate::docs) thread_id: Option, +} + +#[derive(Debug)] +pub(in crate::docs) struct DocsSearchL0RangesParsed { + pub(in crate::docs) updated_after: Option, + pub(in crate::docs) updated_before: Option, + pub(in crate::docs) ts_gte: Option, + pub(in crate::docs) ts_lte: Option, +} + +#[derive(Clone, Copy, Debug)] +pub(in crate::docs) enum DocsSparseMode { + Auto, + On, + Off, +} +impl DocsSparseMode { + pub(in crate::docs) fn as_str(self) -> &'static str { + match self { + Self::Auto => "auto", + Self::On => "on", + Self::Off => "off", + } + } +} diff --git a/packages/elf-service/src/docs/types/trajectory.rs b/packages/elf-service/src/docs/types/trajectory.rs new file mode 100644 index 00000000..3ae3a30a --- /dev/null +++ b/packages/elf-service/src/docs/types/trajectory.rs @@ -0,0 +1,42 @@ +use serde_json::Value; + +use crate::docs::{ + DocRetrievalTrajectory, DocRetrievalTrajectoryStage, + types::constants::DOC_RETRIEVAL_TRAJECTORY_SCHEMA_V1, +}; + +pub(in crate::docs) struct DocTrajectoryBuilder { + pub(in crate::docs) explain: bool, + pub(in crate::docs) stages: Vec, + pub(in crate::docs) stage_order: u32, +} +impl DocTrajectoryBuilder { + pub(in crate::docs) fn new(explain: bool) -> Self { + Self { explain, stages: Vec::new(), stage_order: 0 } + } + + pub(in crate::docs) fn push(&mut self, stage_name: &str, stats: Value) { + if !self.explain { + return; + } + + self.stages.push(DocRetrievalTrajectoryStage { + stage_order: self.stage_order, + stage_name: stage_name.to_string(), + stats, + }); + + self.stage_order += 1; + } + + pub(in crate::docs) fn into_trajectory(self) -> Option { + if !self.explain { + return None; + } + + Some(DocRetrievalTrajectory { + schema: DOC_RETRIEVAL_TRAJECTORY_SCHEMA_V1.to_string(), + stages: self.stages, + }) + } +} From d373b396f4756d1dcb21dbfa3fb21a8e9fb13555 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Tue, 30 Jun 2026 11:55:53 -0400 Subject: [PATCH 5/5] {"schema":"decodex/commit/1","summary":"Split work continuity metric modules","authority":"manual"} --- .../feature_metrics/work_continuity.rs | 295 +----------------- .../work_continuity/collectors.rs | 121 +++++++ .../work_continuity/metrics.rs | 179 +++++++++++ .../work_continuity/observed.rs | 18 ++ 4 files changed, 322 insertions(+), 291 deletions(-) create mode 100644 apps/elf-eval/src/bin/real_world_job_benchmark/feature_metrics/work_continuity/collectors.rs create mode 100644 apps/elf-eval/src/bin/real_world_job_benchmark/feature_metrics/work_continuity/metrics.rs create mode 100644 apps/elf-eval/src/bin/real_world_job_benchmark/feature_metrics/work_continuity/observed.rs diff --git a/apps/elf-eval/src/bin/real_world_job_benchmark/feature_metrics/work_continuity.rs b/apps/elf-eval/src/bin/real_world_job_benchmark/feature_metrics/work_continuity.rs index d294de83..fe670c08 100644 --- a/apps/elf-eval/src/bin/real_world_job_benchmark/feature_metrics/work_continuity.rs +++ b/apps/elf-eval/src/bin/real_world_job_benchmark/feature_metrics/work_continuity.rs @@ -1,292 +1,5 @@ -use crate::feature_metrics::{ - self, BTreeSet, ProducedAnswer, RealWorldJob, WorkContinuityExpectation, - WorkContinuityJobMetrics, WorkContinuityObserved, WorkJournalJanitorCandidateArtifact, - WorkJournalNextStepArtifact, WorkJournalReadbackArtifact, WorkJournalRejectedOptionArtifact, -}; +mod collectors; +mod metrics; +mod observed; -pub(super) fn work_continuity_metrics_impl( - job: &RealWorldJob, - answer: &ProducedAnswer, -) -> Option { - if job.work_continuity.is_none() && answer.work_journal_readbacks.is_empty() { - return None; - } - - let expectation = job.work_continuity.as_ref(); - let observed = work_continuity_observed(answer); - let mut metrics = initial_work_continuity_metrics(expectation, answer); - - if let Some(expected) = expectation { - apply_expected_work_continuity_counts(&mut metrics, expected, &observed); - } - - apply_observed_work_continuity_counts(&mut metrics, answer, &observed); - apply_work_continuity_rates(&mut metrics); - - Some(metrics) -} - -fn work_continuity_observed(answer: &ProducedAnswer) -> WorkContinuityObserved<'_> { - WorkContinuityObserved { - reset_resume_entry_ids: work_journal_reset_resume_entry_ids(answer), - decision_rationale_evidence_ids: work_journal_decision_rationale_evidence_ids(answer), - rejected_options: work_journal_rejected_options(answer), - explicit_next_steps: work_journal_explicit_next_steps(answer), - inferred_next_steps: work_journal_inferred_next_steps(answer), - handoff_source_refs: work_journal_handoff_source_refs(answer), - redacted_marker_ids: work_journal_redacted_marker_ids(answer), - janitor_candidates: work_journal_janitor_candidates(answer), - } -} - -fn initial_work_continuity_metrics( - expectation: Option<&WorkContinuityExpectation>, - answer: &ProducedAnswer, -) -> WorkContinuityJobMetrics { - WorkContinuityJobMetrics { - readback_count: answer.work_journal_readbacks.len(), - entry_count: answer - .work_journal_readbacks - .iter() - .map(|readback| readback.items.len()) - .sum(), - reset_resume_required_count: expectation - .map_or(0, |expected| expected.required_reset_resume_entry_ids.len()), - decision_rationale_required_count: expectation - .map_or(0, |expected| expected.required_decision_rationale_evidence_ids.len()), - rejected_option_required_count: expectation - .map_or(0, |expected| expected.required_rejected_option_ids.len()), - explicit_next_step_required_count: expectation - .map_or(0, |expected| expected.required_explicit_next_step_ids.len()), - inferred_next_step_required_count: expectation - .map_or(0, |expected| expected.required_inferred_next_step_ids.len()), - handoff_source_ref_required_count: expectation - .map_or(0, |expected| expected.required_handoff_source_ref_ids.len()), - redaction_required_count: expectation - .map_or(0, |expected| expected.required_redaction_marker_ids.len()), - janitor_candidate_count: expectation - .map_or(0, |expected| expected.required_janitor_candidate_ids.len()), - ..WorkContinuityJobMetrics::default() - } -} - -fn apply_expected_work_continuity_counts( - metrics: &mut WorkContinuityJobMetrics, - expected: &WorkContinuityExpectation, - observed: &WorkContinuityObserved<'_>, -) { - metrics.reset_resume_success_count = expected - .required_reset_resume_entry_ids - .iter() - .filter(|entry_id| observed.reset_resume_entry_ids.contains(entry_id.as_str())) - .count(); - metrics.decision_rationale_recalled_count = expected - .required_decision_rationale_evidence_ids - .iter() - .filter(|evidence_id| { - observed.decision_rationale_evidence_ids.contains(evidence_id.as_str()) - }) - .count(); - metrics.rejected_option_suppressed_count = expected - .required_rejected_option_ids - .iter() - .filter(|option_id| { - observed - .rejected_options - .iter() - .any(|option| option.option_id == **option_id && !option.resurrected_as_current) - }) - .count(); - metrics.explicit_next_step_correct_count = expected - .required_explicit_next_step_ids - .iter() - .filter(|step_id| { - observed.explicit_next_steps.iter().any(|step| { - step.step_id == **step_id && step.label == "explicit" && step.instruction - }) - }) - .count(); - metrics.inferred_next_step_labeled_count = expected - .required_inferred_next_step_ids - .iter() - .filter(|step_id| { - observed.inferred_next_steps.iter().any(|step| { - step.step_id == **step_id && step.label == "inferred" && !step.instruction - }) - }) - .count(); - metrics.handoff_source_ref_covered_count = expected - .required_handoff_source_ref_ids - .iter() - .filter(|source_ref| observed.handoff_source_refs.contains(source_ref.as_str())) - .count(); - metrics.redaction_applied_count = expected - .required_redaction_marker_ids - .iter() - .filter(|marker_id| observed.redacted_marker_ids.contains(marker_id.as_str())) - .count(); -} - -fn apply_observed_work_continuity_counts( - metrics: &mut WorkContinuityJobMetrics, - answer: &ProducedAnswer, - observed: &WorkContinuityObserved<'_>, -) { - metrics.janitor_candidate_count = - metrics.janitor_candidate_count.max(observed.janitor_candidates.len()); - metrics.janitor_false_promotion_count = observed - .janitor_candidates - .iter() - .filter(|candidate| candidate.promoted_to_memory || !candidate.review_required) - .count(); - metrics.explicit_next_step_returned_count = observed.explicit_next_steps.len(); - metrics.rejected_option_resurrection_count = - observed.rejected_options.iter().filter(|option| option.resurrected_as_current).count(); - metrics.inferred_step_instruction_count = - observed.inferred_next_steps.iter().filter(|step| step.instruction).count(); - metrics.sensitive_marker_persistence_count = answer - .work_journal_readbacks - .iter() - .flat_map(|readback| readback.items.iter()) - .map(|entry| entry.redaction_audit.persisted_sensitive_marker_ids.len()) - .sum(); - metrics.journal_only_authority_claim_count = - answer.work_journal_readbacks.iter().map(work_journal_authority_claim_count).sum(); -} - -fn apply_work_continuity_rates(metrics: &mut WorkContinuityJobMetrics) { - metrics.reset_resume_success_rate = feature_metrics::ratio( - metrics.reset_resume_success_count, - metrics.reset_resume_required_count, - ); - metrics.decision_rationale_recall_rate = feature_metrics::ratio( - metrics.decision_rationale_recalled_count, - metrics.decision_rationale_required_count, - ); - metrics.rejected_option_suppression_rate = feature_metrics::ratio( - metrics.rejected_option_suppressed_count, - metrics.rejected_option_required_count, - ); - metrics.explicit_next_step_precision = feature_metrics::ratio_or( - metrics.explicit_next_step_correct_count, - metrics.explicit_next_step_returned_count, - usize::from(metrics.explicit_next_step_required_count == 0) as f64, - ); - metrics.inferred_next_step_labeling_rate = feature_metrics::ratio( - metrics.inferred_next_step_labeled_count, - metrics.inferred_next_step_required_count, - ); - metrics.handoff_source_ref_coverage = feature_metrics::ratio( - metrics.handoff_source_ref_covered_count, - metrics.handoff_source_ref_required_count, - ); - metrics.redaction_rate = - feature_metrics::ratio(metrics.redaction_applied_count, metrics.redaction_required_count); - metrics.janitor_false_promotion_rate = feature_metrics::ratio( - metrics.janitor_false_promotion_count, - metrics.janitor_candidate_count, - ); -} - -fn work_journal_reset_resume_entry_ids(answer: &ProducedAnswer) -> BTreeSet<&str> { - answer - .work_journal_readbacks - .iter() - .filter_map(|readback| readback.where_stopped.as_ref()) - .flat_map(|where_stopped| where_stopped.reset_resume_entry_ids.iter().map(String::as_str)) - .collect() -} - -fn work_journal_decision_rationale_evidence_ids(answer: &ProducedAnswer) -> BTreeSet<&str> { - answer - .work_journal_readbacks - .iter() - .filter_map(|readback| readback.where_stopped.as_ref()) - .flat_map(|where_stopped| { - where_stopped.decision_rationale_evidence_ids.iter().map(String::as_str) - }) - .collect() -} - -fn work_journal_rejected_options( - answer: &ProducedAnswer, -) -> Vec<&WorkJournalRejectedOptionArtifact> { - answer - .work_journal_readbacks - .iter() - .flat_map(|readback| readback.items.iter()) - .flat_map(|entry| entry.rejected_options.iter()) - .collect() -} - -fn work_journal_explicit_next_steps(answer: &ProducedAnswer) -> Vec<&WorkJournalNextStepArtifact> { - answer - .work_journal_readbacks - .iter() - .flat_map(|readback| readback.items.iter()) - .flat_map(|entry| entry.explicit_next_steps.iter()) - .collect() -} - -fn work_journal_inferred_next_steps(answer: &ProducedAnswer) -> Vec<&WorkJournalNextStepArtifact> { - answer - .work_journal_readbacks - .iter() - .flat_map(|readback| readback.items.iter()) - .flat_map(|entry| entry.inferred_next_steps.iter()) - .collect() -} - -fn work_journal_handoff_source_refs(answer: &ProducedAnswer) -> BTreeSet<&str> { - let mut refs = answer - .work_journal_readbacks - .iter() - .flat_map(|readback| readback.items.iter()) - .flat_map(|entry| entry.source_refs.iter().map(String::as_str)) - .collect::>(); - - for source_ref in answer - .work_journal_readbacks - .iter() - .filter_map(|readback| readback.where_stopped.as_ref()) - .flat_map(|where_stopped| where_stopped.handoff_source_refs.iter().map(String::as_str)) - { - refs.insert(source_ref); - } - - refs -} - -fn work_journal_redacted_marker_ids(answer: &ProducedAnswer) -> BTreeSet<&str> { - answer - .work_journal_readbacks - .iter() - .flat_map(|readback| readback.items.iter()) - .flat_map(|entry| entry.redaction_audit.redacted_marker_ids.iter().map(String::as_str)) - .collect() -} - -fn work_journal_janitor_candidates( - answer: &ProducedAnswer, -) -> Vec<&WorkJournalJanitorCandidateArtifact> { - answer - .work_journal_readbacks - .iter() - .flat_map(|readback| readback.janitor_candidates.iter()) - .collect() -} - -fn work_journal_authority_claim_count(readback: &WorkJournalReadbackArtifact) -> usize { - let boundary_claim_count = - usize::from(readback.promotion_boundary.journal_entry_authority != "source_adjacent_only"); - let missing_promotion_boundary_count = usize::from( - !readback.promotion_boundary.memory_promotion_required - && !readback.promotion_boundary.accepted_refs.is_empty(), - ); - let where_stopped_claim_count = readback - .where_stopped - .as_ref() - .map_or(0, |where_stopped| where_stopped.journal_only_authority_claims.len()); - - boundary_claim_count + missing_promotion_boundary_count + where_stopped_claim_count -} +pub(super) use self::metrics::work_continuity_metrics_impl; diff --git a/apps/elf-eval/src/bin/real_world_job_benchmark/feature_metrics/work_continuity/collectors.rs b/apps/elf-eval/src/bin/real_world_job_benchmark/feature_metrics/work_continuity/collectors.rs new file mode 100644 index 00000000..10e638e0 --- /dev/null +++ b/apps/elf-eval/src/bin/real_world_job_benchmark/feature_metrics/work_continuity/collectors.rs @@ -0,0 +1,121 @@ +use crate::feature_metrics::{ + BTreeSet, ProducedAnswer, WorkJournalJanitorCandidateArtifact, WorkJournalNextStepArtifact, + WorkJournalReadbackArtifact, WorkJournalRejectedOptionArtifact, +}; + +pub(in crate::feature_metrics) fn work_journal_reset_resume_entry_ids( + answer: &ProducedAnswer, +) -> BTreeSet<&str> { + answer + .work_journal_readbacks + .iter() + .filter_map(|readback| readback.where_stopped.as_ref()) + .flat_map(|where_stopped| where_stopped.reset_resume_entry_ids.iter().map(String::as_str)) + .collect() +} + +pub(in crate::feature_metrics) fn work_journal_decision_rationale_evidence_ids( + answer: &ProducedAnswer, +) -> BTreeSet<&str> { + answer + .work_journal_readbacks + .iter() + .filter_map(|readback| readback.where_stopped.as_ref()) + .flat_map(|where_stopped| { + where_stopped.decision_rationale_evidence_ids.iter().map(String::as_str) + }) + .collect() +} + +pub(in crate::feature_metrics) fn work_journal_rejected_options( + answer: &ProducedAnswer, +) -> Vec<&WorkJournalRejectedOptionArtifact> { + answer + .work_journal_readbacks + .iter() + .flat_map(|readback| readback.items.iter()) + .flat_map(|entry| entry.rejected_options.iter()) + .collect() +} + +pub(in crate::feature_metrics) fn work_journal_explicit_next_steps( + answer: &ProducedAnswer, +) -> Vec<&WorkJournalNextStepArtifact> { + answer + .work_journal_readbacks + .iter() + .flat_map(|readback| readback.items.iter()) + .flat_map(|entry| entry.explicit_next_steps.iter()) + .collect() +} + +pub(in crate::feature_metrics) fn work_journal_inferred_next_steps( + answer: &ProducedAnswer, +) -> Vec<&WorkJournalNextStepArtifact> { + answer + .work_journal_readbacks + .iter() + .flat_map(|readback| readback.items.iter()) + .flat_map(|entry| entry.inferred_next_steps.iter()) + .collect() +} + +pub(in crate::feature_metrics) fn work_journal_handoff_source_refs( + answer: &ProducedAnswer, +) -> BTreeSet<&str> { + let mut refs = answer + .work_journal_readbacks + .iter() + .flat_map(|readback| readback.items.iter()) + .flat_map(|entry| entry.source_refs.iter().map(String::as_str)) + .collect::>(); + + for source_ref in answer + .work_journal_readbacks + .iter() + .filter_map(|readback| readback.where_stopped.as_ref()) + .flat_map(|where_stopped| where_stopped.handoff_source_refs.iter().map(String::as_str)) + { + refs.insert(source_ref); + } + + refs +} + +pub(in crate::feature_metrics) fn work_journal_redacted_marker_ids( + answer: &ProducedAnswer, +) -> BTreeSet<&str> { + answer + .work_journal_readbacks + .iter() + .flat_map(|readback| readback.items.iter()) + .flat_map(|entry| entry.redaction_audit.redacted_marker_ids.iter().map(String::as_str)) + .collect() +} + +pub(in crate::feature_metrics) fn work_journal_janitor_candidates( + answer: &ProducedAnswer, +) -> Vec<&WorkJournalJanitorCandidateArtifact> { + answer + .work_journal_readbacks + .iter() + .flat_map(|readback| readback.janitor_candidates.iter()) + .collect() +} + +pub(in crate::feature_metrics) fn work_journal_authority_claim_count( + readback: &WorkJournalReadbackArtifact, +) -> usize { + let boundary_claim_count = + usize::from(readback.promotion_boundary.journal_entry_authority != "source_adjacent_only"); + let missing_promotion_boundary_count = usize::from( + !readback.promotion_boundary.memory_promotion_required + && !readback.promotion_boundary.accepted_refs.is_empty(), + ); + let where_stopped_claim_count = readback + .where_stopped + .as_ref() + .map_or(0, |where_stopped| where_stopped.journal_only_authority_claims.len()); + + boundary_claim_count + missing_promotion_boundary_count + where_stopped_claim_count +} diff --git a/apps/elf-eval/src/bin/real_world_job_benchmark/feature_metrics/work_continuity/metrics.rs b/apps/elf-eval/src/bin/real_world_job_benchmark/feature_metrics/work_continuity/metrics.rs new file mode 100644 index 00000000..5a78cc48 --- /dev/null +++ b/apps/elf-eval/src/bin/real_world_job_benchmark/feature_metrics/work_continuity/metrics.rs @@ -0,0 +1,179 @@ +use crate::feature_metrics::{ + self, ProducedAnswer, RealWorldJob, WorkContinuityExpectation, WorkContinuityJobMetrics, + WorkContinuityObserved, + work_continuity::{collectors, observed}, +}; + +pub(in crate::feature_metrics) fn work_continuity_metrics_impl( + job: &RealWorldJob, + answer: &ProducedAnswer, +) -> Option { + if job.work_continuity.is_none() && answer.work_journal_readbacks.is_empty() { + return None; + } + + let expectation = job.work_continuity.as_ref(); + let observed = observed::work_continuity_observed(answer); + let mut metrics = initial_work_continuity_metrics(expectation, answer); + + if let Some(expected) = expectation { + apply_expected_work_continuity_counts(&mut metrics, expected, &observed); + } + + apply_observed_work_continuity_counts(&mut metrics, answer, &observed); + apply_work_continuity_rates(&mut metrics); + + Some(metrics) +} + +fn initial_work_continuity_metrics( + expectation: Option<&WorkContinuityExpectation>, + answer: &ProducedAnswer, +) -> WorkContinuityJobMetrics { + WorkContinuityJobMetrics { + readback_count: answer.work_journal_readbacks.len(), + entry_count: answer + .work_journal_readbacks + .iter() + .map(|readback| readback.items.len()) + .sum(), + reset_resume_required_count: expectation + .map_or(0, |expected| expected.required_reset_resume_entry_ids.len()), + decision_rationale_required_count: expectation + .map_or(0, |expected| expected.required_decision_rationale_evidence_ids.len()), + rejected_option_required_count: expectation + .map_or(0, |expected| expected.required_rejected_option_ids.len()), + explicit_next_step_required_count: expectation + .map_or(0, |expected| expected.required_explicit_next_step_ids.len()), + inferred_next_step_required_count: expectation + .map_or(0, |expected| expected.required_inferred_next_step_ids.len()), + handoff_source_ref_required_count: expectation + .map_or(0, |expected| expected.required_handoff_source_ref_ids.len()), + redaction_required_count: expectation + .map_or(0, |expected| expected.required_redaction_marker_ids.len()), + janitor_candidate_count: expectation + .map_or(0, |expected| expected.required_janitor_candidate_ids.len()), + ..WorkContinuityJobMetrics::default() + } +} + +fn apply_expected_work_continuity_counts( + metrics: &mut WorkContinuityJobMetrics, + expected: &WorkContinuityExpectation, + observed: &WorkContinuityObserved<'_>, +) { + metrics.reset_resume_success_count = expected + .required_reset_resume_entry_ids + .iter() + .filter(|entry_id| observed.reset_resume_entry_ids.contains(entry_id.as_str())) + .count(); + metrics.decision_rationale_recalled_count = expected + .required_decision_rationale_evidence_ids + .iter() + .filter(|evidence_id| { + observed.decision_rationale_evidence_ids.contains(evidence_id.as_str()) + }) + .count(); + metrics.rejected_option_suppressed_count = expected + .required_rejected_option_ids + .iter() + .filter(|option_id| { + observed + .rejected_options + .iter() + .any(|option| option.option_id == **option_id && !option.resurrected_as_current) + }) + .count(); + metrics.explicit_next_step_correct_count = expected + .required_explicit_next_step_ids + .iter() + .filter(|step_id| { + observed.explicit_next_steps.iter().any(|step| { + step.step_id == **step_id && step.label == "explicit" && step.instruction + }) + }) + .count(); + metrics.inferred_next_step_labeled_count = expected + .required_inferred_next_step_ids + .iter() + .filter(|step_id| { + observed.inferred_next_steps.iter().any(|step| { + step.step_id == **step_id && step.label == "inferred" && !step.instruction + }) + }) + .count(); + metrics.handoff_source_ref_covered_count = expected + .required_handoff_source_ref_ids + .iter() + .filter(|source_ref| observed.handoff_source_refs.contains(source_ref.as_str())) + .count(); + metrics.redaction_applied_count = expected + .required_redaction_marker_ids + .iter() + .filter(|marker_id| observed.redacted_marker_ids.contains(marker_id.as_str())) + .count(); +} + +fn apply_observed_work_continuity_counts( + metrics: &mut WorkContinuityJobMetrics, + answer: &ProducedAnswer, + observed: &WorkContinuityObserved<'_>, +) { + metrics.janitor_candidate_count = + metrics.janitor_candidate_count.max(observed.janitor_candidates.len()); + metrics.janitor_false_promotion_count = observed + .janitor_candidates + .iter() + .filter(|candidate| candidate.promoted_to_memory || !candidate.review_required) + .count(); + metrics.explicit_next_step_returned_count = observed.explicit_next_steps.len(); + metrics.rejected_option_resurrection_count = + observed.rejected_options.iter().filter(|option| option.resurrected_as_current).count(); + metrics.inferred_step_instruction_count = + observed.inferred_next_steps.iter().filter(|step| step.instruction).count(); + metrics.sensitive_marker_persistence_count = answer + .work_journal_readbacks + .iter() + .flat_map(|readback| readback.items.iter()) + .map(|entry| entry.redaction_audit.persisted_sensitive_marker_ids.len()) + .sum(); + metrics.journal_only_authority_claim_count = answer + .work_journal_readbacks + .iter() + .map(collectors::work_journal_authority_claim_count) + .sum(); +} + +fn apply_work_continuity_rates(metrics: &mut WorkContinuityJobMetrics) { + metrics.reset_resume_success_rate = feature_metrics::ratio( + metrics.reset_resume_success_count, + metrics.reset_resume_required_count, + ); + metrics.decision_rationale_recall_rate = feature_metrics::ratio( + metrics.decision_rationale_recalled_count, + metrics.decision_rationale_required_count, + ); + metrics.rejected_option_suppression_rate = feature_metrics::ratio( + metrics.rejected_option_suppressed_count, + metrics.rejected_option_required_count, + ); + metrics.explicit_next_step_precision = feature_metrics::ratio_or( + metrics.explicit_next_step_correct_count, + metrics.explicit_next_step_returned_count, + usize::from(metrics.explicit_next_step_required_count == 0) as f64, + ); + metrics.inferred_next_step_labeling_rate = feature_metrics::ratio( + metrics.inferred_next_step_labeled_count, + metrics.inferred_next_step_required_count, + ); + metrics.handoff_source_ref_coverage = feature_metrics::ratio( + metrics.handoff_source_ref_covered_count, + metrics.handoff_source_ref_required_count, + ); + metrics.redaction_rate = + feature_metrics::ratio(metrics.redaction_applied_count, metrics.redaction_required_count); + metrics.janitor_false_promotion_rate = feature_metrics::ratio( + metrics.janitor_false_promotion_count, + metrics.janitor_candidate_count, + ); +} diff --git a/apps/elf-eval/src/bin/real_world_job_benchmark/feature_metrics/work_continuity/observed.rs b/apps/elf-eval/src/bin/real_world_job_benchmark/feature_metrics/work_continuity/observed.rs new file mode 100644 index 00000000..b0cbea4d --- /dev/null +++ b/apps/elf-eval/src/bin/real_world_job_benchmark/feature_metrics/work_continuity/observed.rs @@ -0,0 +1,18 @@ +use crate::feature_metrics::{ProducedAnswer, WorkContinuityObserved, work_continuity::collectors}; + +pub(in crate::feature_metrics) fn work_continuity_observed( + answer: &ProducedAnswer, +) -> WorkContinuityObserved<'_> { + WorkContinuityObserved { + reset_resume_entry_ids: collectors::work_journal_reset_resume_entry_ids(answer), + decision_rationale_evidence_ids: collectors::work_journal_decision_rationale_evidence_ids( + answer, + ), + rejected_options: collectors::work_journal_rejected_options(answer), + explicit_next_steps: collectors::work_journal_explicit_next_steps(answer), + inferred_next_steps: collectors::work_journal_inferred_next_steps(answer), + handoff_source_refs: collectors::work_journal_handoff_source_refs(answer), + redacted_marker_ids: collectors::work_journal_redacted_marker_ids(answer), + janitor_candidates: collectors::work_journal_janitor_candidates(answer), + } +}