From b627ccdcff52e4ac8f21e90a4e3ec37e0a155f2b Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Tue, 30 Jun 2026 10:54:45 -0400 Subject: [PATCH 1/5] {"schema":"decodex/commit/1","summary":"Split admin and dreaming policy modules","authority":"manual"} --- .../src/admin_graph_predicates/service.rs | 251 +----------------- .../admin_graph_predicates/service/aliases.rs | 94 +++++++ .../admin_graph_predicates/service/auth.rs | 22 ++ .../admin_graph_predicates/service/list.rs | 36 +++ .../admin_graph_predicates/service/patch.rs | 125 +++++++++ .../src/dreaming_review_queue/policy.rs | 249 +---------------- .../dreaming_review_queue/policy/actions.rs | 58 ++++ .../src/dreaming_review_queue/policy/refs.rs | 64 +++++ .../dreaming_review_queue/policy/summary.rs | 43 +++ .../dreaming_review_queue/policy/variants.rs | 81 ++++++ 10 files changed, 538 insertions(+), 485 deletions(-) create mode 100644 packages/elf-service/src/admin_graph_predicates/service/aliases.rs create mode 100644 packages/elf-service/src/admin_graph_predicates/service/auth.rs create mode 100644 packages/elf-service/src/admin_graph_predicates/service/list.rs create mode 100644 packages/elf-service/src/admin_graph_predicates/service/patch.rs create mode 100644 packages/elf-service/src/dreaming_review_queue/policy/actions.rs create mode 100644 packages/elf-service/src/dreaming_review_queue/policy/refs.rs create mode 100644 packages/elf-service/src/dreaming_review_queue/policy/summary.rs create mode 100644 packages/elf-service/src/dreaming_review_queue/policy/variants.rs diff --git a/packages/elf-service/src/admin_graph_predicates/service.rs b/packages/elf-service/src/admin_graph_predicates/service.rs index 3662e474..48d9fbcc 100644 --- a/packages/elf-service/src/admin_graph_predicates/service.rs +++ b/packages/elf-service/src/admin_graph_predicates/service.rs @@ -1,247 +1,4 @@ -use crate::{ - ElfService, Error, Result, - admin_graph_predicates::{ - helpers::{ - self, AdminGraphPredicateScope, PredicateAccess, map_storage_error, to_alias_response, - }, - types::{ - AdminGraphPredicateAliasAddRequest, AdminGraphPredicateAliasesListRequest, - AdminGraphPredicateAliasesResponse, AdminGraphPredicatePatchRequest, - AdminGraphPredicateResponse, AdminGraphPredicatesListRequest, - AdminGraphPredicatesListResponse, - }, - }, -}; -use elf_config::SecurityAuthRole; -use elf_storage::graph; - -impl ElfService { - fn is_super_admin_token_id(&self, token_id: Option<&str>) -> bool { - if self.cfg.security.auth_mode.trim() != "static_keys" { - return false; - } - - let Some(token_id) = token_id.map(str::trim).filter(|value| !value.is_empty()) else { - return false; - }; - - self.cfg - .security - .auth_keys - .iter() - .any(|key| key.token_id == token_id && matches!(key.role, SecurityAuthRole::SuperAdmin)) - } - - /// Lists graph predicates visible to the caller's admin context. - pub async fn admin_graph_predicates_list( - &self, - req: AdminGraphPredicatesListRequest, - ) -> Result { - let raw = req.scope.as_deref().unwrap_or("all"); - let scope = AdminGraphPredicateScope::parse(raw).ok_or_else(|| Error::InvalidRequest { - message: "scope must be one of tenant_project|project|global|all".to_string(), - })?; - let scope_keys = helpers::graph_predicate_scope_keys( - req.tenant_id.as_str(), - req.project_id.as_str(), - scope, - ); - let mut conn = self.db.pool.acquire().await?; - let predicates = graph::list_predicates_by_scope_keys(&mut conn, &scope_keys) - .await - .map_err(map_storage_error)?; - let predicates = predicates - .into_iter() - .map(crate::admin_graph_predicates::helpers::to_predicate_response) - .collect(); - - Ok(AdminGraphPredicatesListResponse { predicates }) - } - - /// Updates a mutable graph predicate field inside the allowed admin scope. - pub async fn admin_graph_predicate_patch( - &self, - req: AdminGraphPredicatePatchRequest, - ) -> Result { - if req.status.is_none() && req.cardinality.is_none() { - return Err(Error::InvalidRequest { - message: "At least one of status or cardinality is required.".to_string(), - }); - } - - let status = req.status.as_deref().map(str::trim); - - if status.is_some_and(str::is_empty) { - return Err(Error::InvalidRequest { message: "status must be non-empty.".to_string() }); - } - - let cardinality = req.cardinality.as_deref().map(str::trim); - - if cardinality.is_some_and(str::is_empty) { - return Err(Error::InvalidRequest { - message: "cardinality must be non-empty.".to_string(), - }); - } - - let allow_global_mutation = self.is_super_admin_token_id(req.token_id.as_deref()); - let mut conn = self.db.pool.acquire().await?; - let existing = helpers::load_predicate_in_context( - &mut conn, - req.tenant_id.as_str(), - req.project_id.as_str(), - req.predicate_id, - PredicateAccess::Mutate, - allow_global_mutation, - ) - .await?; - let old_status = existing.status.clone(); - let old_cardinality = existing.cardinality.clone(); - - if old_status == "deprecated" { - return Err(Error::Conflict { - message: "graph predicate is deprecated and cannot be modified.".to_string(), - }); - } - - let new_status = match status { - None => None, - Some(raw) => { - let raw = raw.to_string(); - - if !matches!(raw.as_str(), "pending" | "active" | "deprecated") { - return Err(Error::InvalidRequest { - message: "status must be one of pending|active|deprecated.".to_string(), - }); - } - if raw != old_status - && !helpers::predicate_status_transition_allowed( - old_status.as_str(), - raw.as_str(), - ) { - return Err(Error::Conflict { - message: format!( - "Invalid graph predicate status transition; from={old_status} to={raw}.", - ), - }); - } - - Some(raw) - }, - }; - let new_cardinality = match cardinality { - None => None, - Some(raw) => { - let raw = raw.to_string(); - - if !matches!(raw.as_str(), "single" | "multi") { - return Err(Error::InvalidRequest { - message: "cardinality must be one of single|multi.".to_string(), - }); - } - - Some(raw) - }, - }; - let updated = graph::update_predicate_guarded( - &mut conn, - req.predicate_id, - old_status.as_str(), - old_cardinality.as_str(), - new_status.as_deref(), - new_cardinality.as_deref(), - ) - .await - .map_err(map_storage_error)?; - - tracing::info!( - actor_agent_id = %req.agent_id, - predicate_id = %req.predicate_id, - old_status = %old_status, - new_status = %updated.status, - old_cardinality = %old_cardinality, - new_cardinality = %updated.cardinality, - "Admin graph predicate patched." - ); - - Ok(helpers::to_predicate_response(updated)) - } - - /// Adds an alias to a mutable graph predicate. - pub async fn admin_graph_predicate_alias_add( - &self, - req: AdminGraphPredicateAliasAddRequest, - ) -> Result { - let alias = req.alias.trim(); - - if alias.is_empty() { - return Err(Error::InvalidRequest { message: "alias must be non-empty.".to_string() }); - } - - let allow_global_mutation = self.is_super_admin_token_id(req.token_id.as_deref()); - let mut conn = self.db.pool.acquire().await?; - let predicate = helpers::load_predicate_in_context( - &mut conn, - req.tenant_id.as_str(), - req.project_id.as_str(), - req.predicate_id, - PredicateAccess::Mutate, - allow_global_mutation, - ) - .await?; - - if predicate.status == "deprecated" { - return Err(Error::Conflict { - message: "graph predicate is deprecated and cannot be modified.".to_string(), - }); - } - - graph::add_predicate_alias(&mut conn, req.predicate_id, alias) - .await - .map_err(map_storage_error)?; - - tracing::info!( - actor_agent_id = %req.agent_id, - predicate_id = %req.predicate_id, - alias = %alias, - "Admin graph predicate alias added." - ); - - let mut aliases = graph::list_predicate_aliases(&mut conn, req.predicate_id) - .await - .map_err(map_storage_error)?; - - helpers::stable_sort_aliases(&mut aliases); - - let aliases = aliases.into_iter().map(to_alias_response).collect(); - - Ok(AdminGraphPredicateAliasesResponse { predicate_id: req.predicate_id, aliases }) - } - - /// Lists aliases for a graph predicate visible in admin scope. - pub async fn admin_graph_predicate_aliases_list( - &self, - req: AdminGraphPredicateAliasesListRequest, - ) -> Result { - let mut conn = self.db.pool.acquire().await?; - - helpers::load_predicate_in_context( - &mut conn, - req.tenant_id.as_str(), - req.project_id.as_str(), - req.predicate_id, - PredicateAccess::Read, - false, - ) - .await?; - - let mut aliases = graph::list_predicate_aliases(&mut conn, req.predicate_id) - .await - .map_err(map_storage_error)?; - - helpers::stable_sort_aliases(&mut aliases); - - let aliases = aliases.into_iter().map(to_alias_response).collect(); - - Ok(AdminGraphPredicateAliasesResponse { predicate_id: req.predicate_id, aliases }) - } -} +mod aliases; +mod auth; +mod list; +mod patch; diff --git a/packages/elf-service/src/admin_graph_predicates/service/aliases.rs b/packages/elf-service/src/admin_graph_predicates/service/aliases.rs new file mode 100644 index 00000000..aa5c9dcb --- /dev/null +++ b/packages/elf-service/src/admin_graph_predicates/service/aliases.rs @@ -0,0 +1,94 @@ +use sqlx::PgConnection; +use uuid::Uuid; + +use crate::{ + ElfService, Error, Result, + admin_graph_predicates::{ + helpers::{self, PredicateAccess, map_storage_error, to_alias_response}, + service::auth, + types::{ + AdminGraphPredicateAliasAddRequest, AdminGraphPredicateAliasesListRequest, + AdminGraphPredicateAliasesResponse, + }, + }, +}; +use elf_storage::graph; + +impl ElfService { + /// Adds an alias to a mutable graph predicate. + pub async fn admin_graph_predicate_alias_add( + &self, + req: AdminGraphPredicateAliasAddRequest, + ) -> Result { + let alias = req.alias.trim(); + + if alias.is_empty() { + return Err(Error::InvalidRequest { message: "alias must be non-empty.".to_string() }); + } + + let allow_global_mutation = auth::is_super_admin_token_id(self, req.token_id.as_deref()); + let mut conn = self.db.pool.acquire().await?; + let predicate = helpers::load_predicate_in_context( + &mut conn, + req.tenant_id.as_str(), + req.project_id.as_str(), + req.predicate_id, + PredicateAccess::Mutate, + allow_global_mutation, + ) + .await?; + + if predicate.status == "deprecated" { + return Err(Error::Conflict { + message: "graph predicate is deprecated and cannot be modified.".to_string(), + }); + } + + graph::add_predicate_alias(&mut conn, req.predicate_id, alias) + .await + .map_err(map_storage_error)?; + + tracing::info!( + actor_agent_id = %req.agent_id, + predicate_id = %req.predicate_id, + alias = %alias, + "Admin graph predicate alias added." + ); + + list_aliases(&mut conn, req.predicate_id).await + } + + /// Lists aliases for a graph predicate visible in admin scope. + pub async fn admin_graph_predicate_aliases_list( + &self, + req: AdminGraphPredicateAliasesListRequest, + ) -> Result { + let mut conn = self.db.pool.acquire().await?; + + helpers::load_predicate_in_context( + &mut conn, + req.tenant_id.as_str(), + req.project_id.as_str(), + req.predicate_id, + PredicateAccess::Read, + false, + ) + .await?; + + list_aliases(&mut conn, req.predicate_id).await + } +} + +async fn list_aliases( + conn: &mut PgConnection, + predicate_id: Uuid, +) -> Result { + let mut aliases = + graph::list_predicate_aliases(conn, predicate_id).await.map_err(map_storage_error)?; + + helpers::stable_sort_aliases(&mut aliases); + + let aliases = aliases.into_iter().map(to_alias_response).collect(); + + Ok(AdminGraphPredicateAliasesResponse { predicate_id, aliases }) +} diff --git a/packages/elf-service/src/admin_graph_predicates/service/auth.rs b/packages/elf-service/src/admin_graph_predicates/service/auth.rs new file mode 100644 index 00000000..8099bbf1 --- /dev/null +++ b/packages/elf-service/src/admin_graph_predicates/service/auth.rs @@ -0,0 +1,22 @@ +use crate::ElfService; +use elf_config::SecurityAuthRole; + +pub(in crate::admin_graph_predicates) fn is_super_admin_token_id( + service: &ElfService, + token_id: Option<&str>, +) -> bool { + if service.cfg.security.auth_mode.trim() != "static_keys" { + return false; + } + + let Some(token_id) = token_id.map(str::trim).filter(|value| !value.is_empty()) else { + return false; + }; + + service + .cfg + .security + .auth_keys + .iter() + .any(|key| key.token_id == token_id && matches!(key.role, SecurityAuthRole::SuperAdmin)) +} diff --git a/packages/elf-service/src/admin_graph_predicates/service/list.rs b/packages/elf-service/src/admin_graph_predicates/service/list.rs new file mode 100644 index 00000000..5b4cd1df --- /dev/null +++ b/packages/elf-service/src/admin_graph_predicates/service/list.rs @@ -0,0 +1,36 @@ +use crate::{ + ElfService, Error, Result, + admin_graph_predicates::{ + helpers::{self, AdminGraphPredicateScope, map_storage_error}, + types::{AdminGraphPredicatesListRequest, AdminGraphPredicatesListResponse}, + }, +}; +use elf_storage::graph; + +impl ElfService { + /// Lists graph predicates visible to the caller's admin context. + pub async fn admin_graph_predicates_list( + &self, + req: AdminGraphPredicatesListRequest, + ) -> Result { + let raw = req.scope.as_deref().unwrap_or("all"); + let scope = AdminGraphPredicateScope::parse(raw).ok_or_else(|| Error::InvalidRequest { + message: "scope must be one of tenant_project|project|global|all".to_string(), + })?; + let scope_keys = helpers::graph_predicate_scope_keys( + req.tenant_id.as_str(), + req.project_id.as_str(), + scope, + ); + let mut conn = self.db.pool.acquire().await?; + let predicates = graph::list_predicates_by_scope_keys(&mut conn, &scope_keys) + .await + .map_err(map_storage_error)?; + let predicates = predicates + .into_iter() + .map(crate::admin_graph_predicates::helpers::to_predicate_response) + .collect(); + + Ok(AdminGraphPredicatesListResponse { predicates }) + } +} diff --git a/packages/elf-service/src/admin_graph_predicates/service/patch.rs b/packages/elf-service/src/admin_graph_predicates/service/patch.rs new file mode 100644 index 00000000..2a0d9268 --- /dev/null +++ b/packages/elf-service/src/admin_graph_predicates/service/patch.rs @@ -0,0 +1,125 @@ +use crate::{ + ElfService, Error, Result, + admin_graph_predicates::{ + helpers::{self, PredicateAccess, map_storage_error}, + service::auth, + types::{AdminGraphPredicatePatchRequest, AdminGraphPredicateResponse}, + }, +}; +use elf_storage::graph; + +impl ElfService { + /// Updates a mutable graph predicate field inside the allowed admin scope. + pub async fn admin_graph_predicate_patch( + &self, + req: AdminGraphPredicatePatchRequest, + ) -> Result { + if req.status.is_none() && req.cardinality.is_none() { + return Err(Error::InvalidRequest { + message: "At least one of status or cardinality is required.".to_string(), + }); + } + + let status = req.status.as_deref().map(str::trim); + + if status.is_some_and(str::is_empty) { + return Err(Error::InvalidRequest { message: "status must be non-empty.".to_string() }); + } + + let cardinality = req.cardinality.as_deref().map(str::trim); + + if cardinality.is_some_and(str::is_empty) { + return Err(Error::InvalidRequest { + message: "cardinality must be non-empty.".to_string(), + }); + } + + let allow_global_mutation = auth::is_super_admin_token_id(self, req.token_id.as_deref()); + let mut conn = self.db.pool.acquire().await?; + let existing = helpers::load_predicate_in_context( + &mut conn, + req.tenant_id.as_str(), + req.project_id.as_str(), + req.predicate_id, + PredicateAccess::Mutate, + allow_global_mutation, + ) + .await?; + let old_status = existing.status.clone(); + let old_cardinality = existing.cardinality.clone(); + + if old_status == "deprecated" { + return Err(Error::Conflict { + message: "graph predicate is deprecated and cannot be modified.".to_string(), + }); + } + + let new_status = resolve_new_status(status, old_status.as_str())?; + let new_cardinality = resolve_new_cardinality(cardinality)?; + let updated = graph::update_predicate_guarded( + &mut conn, + req.predicate_id, + old_status.as_str(), + old_cardinality.as_str(), + new_status.as_deref(), + new_cardinality.as_deref(), + ) + .await + .map_err(map_storage_error)?; + + tracing::info!( + actor_agent_id = %req.agent_id, + predicate_id = %req.predicate_id, + old_status = %old_status, + new_status = %updated.status, + old_cardinality = %old_cardinality, + new_cardinality = %updated.cardinality, + "Admin graph predicate patched." + ); + + Ok(helpers::to_predicate_response(updated)) + } +} + +fn resolve_new_status(status: Option<&str>, old_status: &str) -> Result> { + match status { + None => Ok(None), + Some(raw) => { + let raw = raw.to_string(); + + if !matches!(raw.as_str(), "pending" | "active" | "deprecated") { + return Err(Error::InvalidRequest { + message: "status must be one of pending|active|deprecated.".to_string(), + }); + } + if raw != old_status + && !helpers::predicate_status_transition_allowed(old_status, raw.as_str()) + { + return Err(Error::Conflict { + message: format!( + "Invalid graph predicate status transition; from={old_status} to={raw}.", + ), + }); + } + + Ok(Some(raw)) + }, + } +} + +fn resolve_new_cardinality(cardinality: Option<&str>) -> Result> { + match cardinality { + None => Ok(None), + Some(raw) => { + let raw = raw.to_string(); + + if !matches!(raw.as_str(), "single" | "multi") { + return Err(Error::InvalidRequest { + message: "cardinality must be one of single|multi.".to_string(), + }); + } + + Ok(Some(raw)) + }, + } +} diff --git a/packages/elf-service/src/dreaming_review_queue/policy.rs b/packages/elf-service/src/dreaming_review_queue/policy.rs index 0194af88..7d80e3f0 100644 --- a/packages/elf-service/src/dreaming_review_queue/policy.rs +++ b/packages/elf-service/src/dreaming_review_queue/policy.rs @@ -1,243 +1,16 @@ -use std::collections::BTreeSet; - -use serde_json::Value; - -use crate::dreaming_review_queue::types::{DreamingReviewQueueItem, DreamingReviewQueueSummary}; +mod actions; +mod refs; +mod summary; +mod variants; + +pub(in crate::dreaming_review_queue) use self::{ + actions::{available_review_actions, bounded_queue_limit, policy_reason}, + refs::{affected_refs, contains_forbidden_source_mutation_key, non_empty_json_array}, + summary::summarize_items, + variants::{high_impact_variant, low_risk_derived_organization, queue_variant_for}, +}; /// Schema identifier for Dreaming review queue responses. pub const ELF_DREAMING_REVIEW_QUEUE_SCHEMA_V1: &str = "elf.dreaming_review_queue/v1"; pub(super) const HIGH_CONFIDENCE_AUTO_APPLY_FLOOR: f32 = 0.9; - -const DEFAULT_QUEUE_LIMIT: u32 = 50; -const MAX_QUEUE_LIMIT: u32 = 200; -const FORBIDDEN_SOURCE_MUTATION_KEYS: [&str; 8] = [ - "delete_source", - "delete_sources", - "overwrite_source", - "source_delete", - "source_mutation", - "source_mutations", - "source_note_updates", - "update_source", -]; - -pub(super) fn summarize_items(items: &[DreamingReviewQueueItem]) -> DreamingReviewQueueSummary { - let mut summary = DreamingReviewQueueSummary { - item_count: items.len(), - ..DreamingReviewQueueSummary::default() - }; - let mut variants = BTreeSet::new(); - - for item in items { - match item.review_state.as_str() { - "proposed" => summary.proposed_count += 1, - "approved" => summary.approved_count += 1, - "applied" => summary.applied_count += 1, - "rejected" => summary.discarded_count += 1, - "archived" => summary.deferred_count += 1, - _ => {}, - } - - if item.policy.high_impact { - summary.high_impact_count += 1; - } - if item.policy.source_mutation_requested { - summary.source_mutation_requested_count += 1; - } - if item.policy.auto_apply_candidate { - summary.auto_apply_candidate_count += 1; - } - if item.policy.auto_apply_allowed { - summary.auto_apply_allowed_count += 1; - } - - variants.insert(item.queue_variant.as_str()); - } - - summary.variant_count = variants.len(); - - summary -} - -pub(super) fn queue_variant_for( - proposal_kind: &str, - apply_intent: &str, - proposed_payload: &Value, -) -> String { - for pointer in [ - "/queue_variant", - "/dreaming_variant", - "/proposal_variant", - "/variant", - "/artifact_kind", - "/metadata/queue_variant", - "/metadata/dreaming_variant", - "/metadata/artifact_kind", - ] { - if let Some(raw) = proposed_payload.pointer(pointer).and_then(Value::as_str) - && let Some(variant) = normalize_variant(raw) - { - return variant; - } - } - - if let Some(variant) = normalize_variant(proposal_kind) { - return variant; - } - - match apply_intent { - "create_derived_knowledge_page" | "update_derived_knowledge_page" => - "page_rebuild".to_string(), - "create_derived_graph_view" => "graph_fact".to_string(), - "create_derived_note" | "update_derived_note" => "memory_promotion".to_string(), - _ => "other".to_string(), - } -} - -pub(super) fn affected_refs(target_ref: &Value, proposed_payload: &Value) -> Vec { - let mut refs = Vec::new(); - - push_non_empty_object(&mut refs, target_ref); - - for pointer in [ - "/affected_refs", - "/affected_pages", - "/affected_memories", - "/affected_facts", - "/affected_notes", - ] { - match proposed_payload.pointer(pointer) { - Some(Value::Array(values)) => refs.extend(values.iter().cloned()), - Some(value) if non_empty_json_object(value) => refs.push(value.clone()), - _ => {}, - } - } - - refs -} - -pub(super) fn non_empty_json_array(value: &Value) -> bool { - value.as_array().is_some_and(|array| !array.is_empty()) -} - -pub(super) fn contains_forbidden_source_mutation_key(value: &Value) -> bool { - match value { - Value::Object(map) => map.iter().any(|(key, nested)| { - FORBIDDEN_SOURCE_MUTATION_KEYS.contains(&key.as_str()) - || contains_forbidden_source_mutation_key(nested) - }), - Value::Array(items) => items.iter().any(contains_forbidden_source_mutation_key), - _ => false, - } -} - -pub(super) fn low_risk_derived_organization(queue_variant: &str) -> bool { - matches!(queue_variant, "tag" | "duplicate_merge") -} - -pub(super) fn high_impact_variant(queue_variant: &str) -> bool { - matches!(queue_variant, "memory_promotion" | "graph_fact" | "correction") -} - -pub(super) fn available_review_actions( - review_state: &str, - manual_apply_allowed: bool, -) -> Vec { - let actions = match review_state { - "proposed" => &["approve", "defer", "discard"][..], - "approved" if manual_apply_allowed => &["apply", "defer", "discard"][..], - "approved" => &["defer", "discard"][..], - _ => &[][..], - }; - - actions.iter().map(|action| (*action).to_string()).collect() -} - -pub(super) fn policy_reason( - source_mutation_requested: bool, - high_impact: bool, - has_unsupported_claims: bool, - has_review_markers: bool, - auto_apply_candidate: bool, - auto_apply_allowed: bool, - manual_apply_allowed: bool, -) -> String { - if source_mutation_requested { - return "source mutation is requested, so the proposal cannot be applied by the queue" - .to_string(); - } - if has_unsupported_claims || has_review_markers { - return "lint or review markers require explicit reviewer inspection".to_string(); - } - if auto_apply_allowed { - return "approved low-risk derived organization proposal satisfies auto-apply policy" - .to_string(); - } - if manual_apply_allowed { - return "approved review-gated proposal may be manually applied to a derived target" - .to_string(); - } - if high_impact { - return "high-impact memory, graph, or correction proposal requires approval before apply" - .to_string(); - } - - if auto_apply_candidate { - return "low-risk derived organization proposal is a candidate after reviewer approval" - .to_string(); - } - - "proposal remains reviewable derived output".to_string() -} - -pub(super) fn bounded_queue_limit(limit: Option) -> i64 { - i64::from(limit.unwrap_or(DEFAULT_QUEUE_LIMIT).clamp(1, MAX_QUEUE_LIMIT)) -} - -fn normalize_variant(raw: &str) -> Option { - let token = raw.trim().to_ascii_lowercase().replace(['-', ' '], "_"); - - if token.is_empty() { - return None; - } - if token.contains("duplicate") || token.contains("dedupe") { - return Some("duplicate_merge".to_string()); - } - if token.contains("tag") || token.contains("taxonomy") { - return Some("tag".to_string()); - } - if token.contains("knowledge_page") || token.contains("page_rebuild") { - return Some("page_rebuild".to_string()); - } - if token.contains("graph_fact") || token.contains("graph_view") { - return Some("graph_fact".to_string()); - } - if token.contains("proactive_brief") || token.contains("daily_brief") { - return Some("proactive_brief".to_string()); - } - if token.contains("scheduled_memory") || token.contains("weekly_summary") { - return Some("scheduled_memory".to_string()); - } - if token.contains("memory_summary") || token.contains("summary") { - return Some("memory_summary".to_string()); - } - if token.contains("memory_promotion") || token.contains("derived_note") { - return Some("memory_promotion".to_string()); - } - if token.contains("correction") || token.contains("repair") { - return Some("correction".to_string()); - } - - Some(token) -} - -fn push_non_empty_object(refs: &mut Vec, value: &Value) { - if non_empty_json_object(value) { - refs.push(value.clone()); - } -} - -fn non_empty_json_object(value: &Value) -> bool { - value.as_object().is_some_and(|object| !object.is_empty()) -} diff --git a/packages/elf-service/src/dreaming_review_queue/policy/actions.rs b/packages/elf-service/src/dreaming_review_queue/policy/actions.rs new file mode 100644 index 00000000..9499f986 --- /dev/null +++ b/packages/elf-service/src/dreaming_review_queue/policy/actions.rs @@ -0,0 +1,58 @@ +const DEFAULT_QUEUE_LIMIT: u32 = 50; +const MAX_QUEUE_LIMIT: u32 = 200; + +pub(in crate::dreaming_review_queue) fn available_review_actions( + review_state: &str, + manual_apply_allowed: bool, +) -> Vec { + let actions = match review_state { + "proposed" => &["approve", "defer", "discard"][..], + "approved" if manual_apply_allowed => &["apply", "defer", "discard"][..], + "approved" => &["defer", "discard"][..], + _ => &[][..], + }; + + actions.iter().map(|action| (*action).to_string()).collect() +} + +#[allow(clippy::too_many_arguments)] +pub(in crate::dreaming_review_queue) fn policy_reason( + source_mutation_requested: bool, + high_impact: bool, + has_unsupported_claims: bool, + has_review_markers: bool, + auto_apply_candidate: bool, + auto_apply_allowed: bool, + manual_apply_allowed: bool, +) -> String { + if source_mutation_requested { + return "source mutation is requested, so the proposal cannot be applied by the queue" + .to_string(); + } + if has_unsupported_claims || has_review_markers { + return "lint or review markers require explicit reviewer inspection".to_string(); + } + if auto_apply_allowed { + return "approved low-risk derived organization proposal satisfies auto-apply policy" + .to_string(); + } + if manual_apply_allowed { + return "approved review-gated proposal may be manually applied to a derived target" + .to_string(); + } + if high_impact { + return "high-impact memory, graph, or correction proposal requires approval before apply" + .to_string(); + } + + if auto_apply_candidate { + return "low-risk derived organization proposal is a candidate after reviewer approval" + .to_string(); + } + + "proposal remains reviewable derived output".to_string() +} + +pub(in crate::dreaming_review_queue) fn bounded_queue_limit(limit: Option) -> i64 { + i64::from(limit.unwrap_or(DEFAULT_QUEUE_LIMIT).clamp(1, MAX_QUEUE_LIMIT)) +} diff --git a/packages/elf-service/src/dreaming_review_queue/policy/refs.rs b/packages/elf-service/src/dreaming_review_queue/policy/refs.rs new file mode 100644 index 00000000..9da0fc27 --- /dev/null +++ b/packages/elf-service/src/dreaming_review_queue/policy/refs.rs @@ -0,0 +1,64 @@ +use serde_json::Value; + +const FORBIDDEN_SOURCE_MUTATION_KEYS: [&str; 8] = [ + "delete_source", + "delete_sources", + "overwrite_source", + "source_delete", + "source_mutation", + "source_mutations", + "source_note_updates", + "update_source", +]; + +pub(in crate::dreaming_review_queue) fn affected_refs( + target_ref: &Value, + proposed_payload: &Value, +) -> Vec { + let mut refs = Vec::new(); + + push_non_empty_object(&mut refs, target_ref); + + for pointer in [ + "/affected_refs", + "/affected_pages", + "/affected_memories", + "/affected_facts", + "/affected_notes", + ] { + match proposed_payload.pointer(pointer) { + Some(Value::Array(values)) => refs.extend(values.iter().cloned()), + Some(value) if non_empty_json_object(value) => refs.push(value.clone()), + _ => {}, + } + } + + refs +} + +pub(in crate::dreaming_review_queue) fn non_empty_json_array(value: &Value) -> bool { + value.as_array().is_some_and(|array| !array.is_empty()) +} + +pub(in crate::dreaming_review_queue) fn contains_forbidden_source_mutation_key( + value: &Value, +) -> bool { + match value { + Value::Object(map) => map.iter().any(|(key, nested)| { + FORBIDDEN_SOURCE_MUTATION_KEYS.contains(&key.as_str()) + || contains_forbidden_source_mutation_key(nested) + }), + Value::Array(items) => items.iter().any(contains_forbidden_source_mutation_key), + _ => false, + } +} + +fn push_non_empty_object(refs: &mut Vec, value: &Value) { + if non_empty_json_object(value) { + refs.push(value.clone()); + } +} + +fn non_empty_json_object(value: &Value) -> bool { + value.as_object().is_some_and(|object| !object.is_empty()) +} diff --git a/packages/elf-service/src/dreaming_review_queue/policy/summary.rs b/packages/elf-service/src/dreaming_review_queue/policy/summary.rs new file mode 100644 index 00000000..3fd37c73 --- /dev/null +++ b/packages/elf-service/src/dreaming_review_queue/policy/summary.rs @@ -0,0 +1,43 @@ +use std::collections::BTreeSet; + +use crate::dreaming_review_queue::types::{DreamingReviewQueueItem, DreamingReviewQueueSummary}; + +pub(in crate::dreaming_review_queue) fn summarize_items( + items: &[DreamingReviewQueueItem], +) -> DreamingReviewQueueSummary { + let mut summary = DreamingReviewQueueSummary { + item_count: items.len(), + ..DreamingReviewQueueSummary::default() + }; + let mut variants = BTreeSet::new(); + + for item in items { + match item.review_state.as_str() { + "proposed" => summary.proposed_count += 1, + "approved" => summary.approved_count += 1, + "applied" => summary.applied_count += 1, + "rejected" => summary.discarded_count += 1, + "archived" => summary.deferred_count += 1, + _ => {}, + } + + if item.policy.high_impact { + summary.high_impact_count += 1; + } + if item.policy.source_mutation_requested { + summary.source_mutation_requested_count += 1; + } + if item.policy.auto_apply_candidate { + summary.auto_apply_candidate_count += 1; + } + if item.policy.auto_apply_allowed { + summary.auto_apply_allowed_count += 1; + } + + variants.insert(item.queue_variant.as_str()); + } + + summary.variant_count = variants.len(); + + summary +} diff --git a/packages/elf-service/src/dreaming_review_queue/policy/variants.rs b/packages/elf-service/src/dreaming_review_queue/policy/variants.rs new file mode 100644 index 00000000..5c193f88 --- /dev/null +++ b/packages/elf-service/src/dreaming_review_queue/policy/variants.rs @@ -0,0 +1,81 @@ +use serde_json::Value; + +pub(in crate::dreaming_review_queue) fn queue_variant_for( + proposal_kind: &str, + apply_intent: &str, + proposed_payload: &Value, +) -> String { + for pointer in [ + "/queue_variant", + "/dreaming_variant", + "/proposal_variant", + "/variant", + "/artifact_kind", + "/metadata/queue_variant", + "/metadata/dreaming_variant", + "/metadata/artifact_kind", + ] { + if let Some(raw) = proposed_payload.pointer(pointer).and_then(Value::as_str) + && let Some(variant) = normalize_variant(raw) + { + return variant; + } + } + + if let Some(variant) = normalize_variant(proposal_kind) { + return variant; + } + + match apply_intent { + "create_derived_knowledge_page" | "update_derived_knowledge_page" => + "page_rebuild".to_string(), + "create_derived_graph_view" => "graph_fact".to_string(), + "create_derived_note" | "update_derived_note" => "memory_promotion".to_string(), + _ => "other".to_string(), + } +} + +pub(in crate::dreaming_review_queue) fn low_risk_derived_organization(queue_variant: &str) -> bool { + matches!(queue_variant, "tag" | "duplicate_merge") +} + +pub(in crate::dreaming_review_queue) fn high_impact_variant(queue_variant: &str) -> bool { + matches!(queue_variant, "memory_promotion" | "graph_fact" | "correction") +} + +fn normalize_variant(raw: &str) -> Option { + let token = raw.trim().to_ascii_lowercase().replace(['-', ' '], "_"); + + if token.is_empty() { + return None; + } + if token.contains("duplicate") || token.contains("dedupe") { + return Some("duplicate_merge".to_string()); + } + if token.contains("tag") || token.contains("taxonomy") { + return Some("tag".to_string()); + } + if token.contains("knowledge_page") || token.contains("page_rebuild") { + return Some("page_rebuild".to_string()); + } + if token.contains("graph_fact") || token.contains("graph_view") { + return Some("graph_fact".to_string()); + } + if token.contains("proactive_brief") || token.contains("daily_brief") { + return Some("proactive_brief".to_string()); + } + if token.contains("scheduled_memory") || token.contains("weekly_summary") { + return Some("scheduled_memory".to_string()); + } + if token.contains("memory_summary") || token.contains("summary") { + return Some("memory_summary".to_string()); + } + if token.contains("memory_promotion") || token.contains("derived_note") { + return Some("memory_promotion".to_string()); + } + if token.contains("correction") || token.contains("repair") { + return Some("correction".to_string()); + } + + Some(token) +} From e18dabc68772f26c5e034a716e31fc44220eba11 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Tue, 30 Jun 2026 11:01:09 -0400 Subject: [PATCH 2/5] {"schema":"decodex/commit/1","summary":"Split entity memory build modules","authority":"manual"} --- .../elf-service/src/entity_memory/build.rs | 248 +----------------- .../src/entity_memory/build/core_blocks.rs | 60 +++++ .../src/entity_memory/build/lifecycle.rs | 25 ++ .../src/entity_memory/build/note_items.rs | 88 +++++++ .../src/entity_memory/build/sort.rs | 30 +++ .../src/entity_memory/build/summary.rs | 27 ++ .../src/entity_memory/build/visibility.rs | 29 ++ .../elf-service/src/entity_memory/tests.rs | 26 +- 8 files changed, 284 insertions(+), 249 deletions(-) create mode 100644 packages/elf-service/src/entity_memory/build/core_blocks.rs create mode 100644 packages/elf-service/src/entity_memory/build/lifecycle.rs create mode 100644 packages/elf-service/src/entity_memory/build/note_items.rs create mode 100644 packages/elf-service/src/entity_memory/build/sort.rs create mode 100644 packages/elf-service/src/entity_memory/build/summary.rs create mode 100644 packages/elf-service/src/entity_memory/build/visibility.rs diff --git a/packages/elf-service/src/entity_memory/build.rs b/packages/elf-service/src/entity_memory/build.rs index 1a56c858..4bc6f127 100644 --- a/packages/elf-service/src/entity_memory/build.rs +++ b/packages/elf-service/src/entity_memory/build.rs @@ -1,242 +1,12 @@ -use std::collections::HashSet; +pub(in crate::entity_memory) mod core_blocks; +pub(in crate::entity_memory) mod lifecycle; -use time::OffsetDateTime; +mod note_items; +mod sort; +mod summary; +mod visibility; -use crate::{ - access, - entity_memory::{ - TOP_OF_MIND_IMPORTANCE_THRESHOLD, - storage::{EntityCoreBlockRow, EntityNoteRow}, - types::{EntityMemoryItem, EntityMemoryRelation, EntityMemorySummary}, - }, - graph, +pub(in crate::entity_memory) use self::{ + core_blocks::build_core_block_items, note_items::build_note_items, + sort::sort_entity_memory_items, summary::summarize_items, }; - -pub(super) fn build_note_items( - rows: Vec, - requester_agent_id: &str, - allowed_scopes: &[String], - shared_grants: &HashSet, - as_of: OffsetDateTime, -) -> Vec { - let mut items = Vec::new(); - - for row in rows { - if !row_read_allowed( - row.agent_id.as_str(), - row.scope.as_str(), - requester_agent_id, - allowed_scopes, - shared_grants, - ) || !row_read_allowed( - row.fact_agent_id.as_str(), - row.fact_scope.as_str(), - requester_agent_id, - allowed_scopes, - shared_grants, - ) { - continue; - } - - let lifecycle = note_lifecycle(row.status.as_str(), row.expires_at, as_of); - let read_bucket = note_read_bucket(lifecycle.as_str(), row.importance); - let relation = relation_from_note_row(&row, as_of); - - if let Some(item) = items.iter_mut().find(|item: &&mut EntityMemoryItem| { - item.source == "archival_note" && item.note_id == Some(row.note_id) - }) { - item.relations.push(relation); - - continue; - } - - items.push(EntityMemoryItem { - source: "archival_note".to_string(), - lifecycle, - read_bucket, - scope: row.scope, - agent_id: row.agent_id, - note_id: Some(row.note_id), - block_id: None, - attachment_id: None, - note_type: Some(row.r#type), - key: row.key, - title: None, - text: row.text, - importance: Some(row.importance), - confidence: Some(row.confidence), - source_ref: row.source_ref, - updated_at: row.updated_at, - expires_at: row.expires_at, - relations: vec![relation], - }); - } - - items -} - -pub(super) fn build_core_block_items( - rows: Vec, - requester_agent_id: &str, - allowed_scopes: &[String], - shared_grants: &HashSet, - surfaces: &[String], -) -> Vec { - rows.into_iter() - .filter(|row| { - row_read_allowed( - row.agent_id.as_str(), - row.scope.as_str(), - requester_agent_id, - allowed_scopes, - shared_grants, - ) && core_block_mentions_entity(row, surfaces) - }) - .map(|row| EntityMemoryItem { - source: "core_block".to_string(), - lifecycle: "current".to_string(), - read_bucket: "top_of_mind".to_string(), - scope: row.scope, - agent_id: row.agent_id, - note_id: None, - block_id: Some(row.block_id), - attachment_id: Some(row.attachment_id), - note_type: None, - key: Some(row.key), - title: Some(row.title), - text: row.content, - importance: None, - confidence: None, - source_ref: row.source_ref, - updated_at: row.updated_at, - expires_at: None, - relations: Vec::new(), - }) - .collect() -} - -pub(super) fn note_lifecycle( - status: &str, - expires_at: Option, - as_of: OffsetDateTime, -) -> String { - match status { - "active" if expires_at.is_some_and(|expires_at| expires_at <= as_of) => "stale".to_string(), - "active" => "current".to_string(), - "deprecated" => "superseded".to_string(), - "deleted" => "tombstoned".to_string(), - other => other.to_string(), - } -} - -pub(super) fn note_read_bucket(lifecycle: &str, importance: f32) -> String { - if lifecycle == "current" && importance >= TOP_OF_MIND_IMPORTANCE_THRESHOLD { - "top_of_mind".to_string() - } else { - "background".to_string() - } -} - -pub(super) fn core_block_mentions_entity(row: &EntityCoreBlockRow, surfaces: &[String]) -> bool { - let haystack = - format!("{} {} {} {}", row.key, row.title, row.content, row.source_ref).to_lowercase(); - - surfaces - .iter() - .map(|surface| surface.trim().to_lowercase()) - .filter(|surface| !surface.is_empty()) - .any(|surface| haystack.contains(surface.as_str())) -} - -pub(super) fn summarize_items(items: &[EntityMemoryItem]) -> EntityMemorySummary { - let mut summary = EntityMemorySummary::default(); - - for item in items { - match item.lifecycle.as_str() { - "current" => summary.current_count += 1, - "stale" => summary.stale_count += 1, - "superseded" => summary.superseded_count += 1, - "tombstoned" => summary.tombstoned_count += 1, - _ => {}, - } - match item.read_bucket.as_str() { - "top_of_mind" => summary.top_of_mind_count += 1, - "background" => summary.background_count += 1, - _ => {}, - } - match item.source.as_str() { - "core_block" => summary.core_block_count += 1, - "archival_note" => summary.archival_note_count += 1, - _ => {}, - } - } - - summary -} - -pub(super) fn sort_entity_memory_items(items: &mut [EntityMemoryItem]) { - items.sort_by(|left, right| { - read_bucket_rank(right.read_bucket.as_str()) - .cmp(&read_bucket_rank(left.read_bucket.as_str())) - .then_with(|| { - lifecycle_rank(right.lifecycle.as_str()) - .cmp(&lifecycle_rank(left.lifecycle.as_str())) - }) - .then_with(|| right.updated_at.cmp(&left.updated_at)) - .then_with(|| left.source.cmp(&right.source)) - }); -} - -fn row_read_allowed( - owner_agent_id: &str, - scope: &str, - requester_agent_id: &str, - allowed_scopes: &[String], - shared_grants: &HashSet, -) -> bool { - if !allowed_scopes.iter().any(|allowed| allowed == scope) { - return false; - } - if scope == "agent_private" { - return owner_agent_id == requester_agent_id; - } - if !matches!(scope, "project_shared" | "org_shared") { - return false; - } - if owner_agent_id == requester_agent_id { - return true; - } - - shared_grants.contains(&access::SharedSpaceGrantKey { - scope: scope.to_string(), - space_owner_agent_id: owner_agent_id.to_string(), - }) -} - -fn relation_from_note_row(row: &EntityNoteRow, as_of: OffsetDateTime) -> EntityMemoryRelation { - EntityMemoryRelation { - fact_id: row.fact_id, - predicate: row.predicate.clone(), - scope: row.fact_scope.clone(), - actor: row.fact_agent_id.clone(), - valid_from: row.valid_from, - valid_to: row.valid_to, - temporal_status: graph::relation_temporal_status(row.valid_from, row.valid_to, as_of), - } -} - -fn read_bucket_rank(bucket: &str) -> u8 { - match bucket { - "top_of_mind" => 1, - _ => 0, - } -} - -fn lifecycle_rank(lifecycle: &str) -> u8 { - match lifecycle { - "current" => 3, - "stale" => 2, - "superseded" => 1, - _ => 0, - } -} diff --git a/packages/elf-service/src/entity_memory/build/core_blocks.rs b/packages/elf-service/src/entity_memory/build/core_blocks.rs new file mode 100644 index 00000000..e08a8aa8 --- /dev/null +++ b/packages/elf-service/src/entity_memory/build/core_blocks.rs @@ -0,0 +1,60 @@ +use std::collections::HashSet; + +use crate::{ + access, + entity_memory::{build::visibility, storage::EntityCoreBlockRow, types::EntityMemoryItem}, +}; + +pub(in crate::entity_memory) fn build_core_block_items( + rows: Vec, + requester_agent_id: &str, + allowed_scopes: &[String], + shared_grants: &HashSet, + surfaces: &[String], +) -> Vec { + rows.into_iter() + .filter(|row| { + visibility::row_read_allowed( + row.agent_id.as_str(), + row.scope.as_str(), + requester_agent_id, + allowed_scopes, + shared_grants, + ) && core_block_mentions_entity(row, surfaces) + }) + .map(|row| EntityMemoryItem { + source: "core_block".to_string(), + lifecycle: "current".to_string(), + read_bucket: "top_of_mind".to_string(), + scope: row.scope, + agent_id: row.agent_id, + note_id: None, + block_id: Some(row.block_id), + attachment_id: Some(row.attachment_id), + note_type: None, + key: Some(row.key), + title: Some(row.title), + text: row.content, + importance: None, + confidence: None, + source_ref: row.source_ref, + updated_at: row.updated_at, + expires_at: None, + relations: Vec::new(), + }) + .collect() +} + +pub(in crate::entity_memory) fn core_block_mentions_entity( + row: &EntityCoreBlockRow, + surfaces: &[String], +) -> bool { + let haystack = + format!("{} {} {} {}", row.key, row.title, row.content, row.source_ref).to_lowercase(); + + surfaces + .iter() + .map(|surface| surface.trim().to_lowercase()) + .filter(|surface| !surface.is_empty()) + .any(|surface| haystack.contains(surface.as_str())) +} diff --git a/packages/elf-service/src/entity_memory/build/lifecycle.rs b/packages/elf-service/src/entity_memory/build/lifecycle.rs new file mode 100644 index 00000000..83feaf83 --- /dev/null +++ b/packages/elf-service/src/entity_memory/build/lifecycle.rs @@ -0,0 +1,25 @@ +use time::OffsetDateTime; + +use crate::entity_memory::TOP_OF_MIND_IMPORTANCE_THRESHOLD; + +pub(in crate::entity_memory) fn note_lifecycle( + status: &str, + expires_at: Option, + as_of: OffsetDateTime, +) -> String { + match status { + "active" if expires_at.is_some_and(|expires_at| expires_at <= as_of) => "stale".to_string(), + "active" => "current".to_string(), + "deprecated" => "superseded".to_string(), + "deleted" => "tombstoned".to_string(), + other => other.to_string(), + } +} + +pub(in crate::entity_memory) fn note_read_bucket(lifecycle: &str, importance: f32) -> String { + if lifecycle == "current" && importance >= TOP_OF_MIND_IMPORTANCE_THRESHOLD { + "top_of_mind".to_string() + } else { + "background".to_string() + } +} diff --git a/packages/elf-service/src/entity_memory/build/note_items.rs b/packages/elf-service/src/entity_memory/build/note_items.rs new file mode 100644 index 00000000..bc079f90 --- /dev/null +++ b/packages/elf-service/src/entity_memory/build/note_items.rs @@ -0,0 +1,88 @@ +use std::collections::HashSet; + +use time::OffsetDateTime; + +use crate::{ + access, + entity_memory::{ + build::{lifecycle, visibility}, + storage::EntityNoteRow, + types::{EntityMemoryItem, EntityMemoryRelation}, + }, + graph, +}; + +pub(in crate::entity_memory) fn build_note_items( + rows: Vec, + requester_agent_id: &str, + allowed_scopes: &[String], + shared_grants: &HashSet, + as_of: OffsetDateTime, +) -> Vec { + let mut items = Vec::new(); + + for row in rows { + if !visibility::row_read_allowed( + row.agent_id.as_str(), + row.scope.as_str(), + requester_agent_id, + allowed_scopes, + shared_grants, + ) || !visibility::row_read_allowed( + row.fact_agent_id.as_str(), + row.fact_scope.as_str(), + requester_agent_id, + allowed_scopes, + shared_grants, + ) { + continue; + } + + let lifecycle = lifecycle::note_lifecycle(row.status.as_str(), row.expires_at, as_of); + let read_bucket = lifecycle::note_read_bucket(lifecycle.as_str(), row.importance); + let relation = relation_from_note_row(&row, as_of); + + if let Some(item) = items.iter_mut().find(|item: &&mut EntityMemoryItem| { + item.source == "archival_note" && item.note_id == Some(row.note_id) + }) { + item.relations.push(relation); + + continue; + } + + items.push(EntityMemoryItem { + source: "archival_note".to_string(), + lifecycle, + read_bucket, + scope: row.scope, + agent_id: row.agent_id, + note_id: Some(row.note_id), + block_id: None, + attachment_id: None, + note_type: Some(row.r#type), + key: row.key, + title: None, + text: row.text, + importance: Some(row.importance), + confidence: Some(row.confidence), + source_ref: row.source_ref, + updated_at: row.updated_at, + expires_at: row.expires_at, + relations: vec![relation], + }); + } + + items +} + +fn relation_from_note_row(row: &EntityNoteRow, as_of: OffsetDateTime) -> EntityMemoryRelation { + EntityMemoryRelation { + fact_id: row.fact_id, + predicate: row.predicate.clone(), + scope: row.fact_scope.clone(), + actor: row.fact_agent_id.clone(), + valid_from: row.valid_from, + valid_to: row.valid_to, + temporal_status: graph::relation_temporal_status(row.valid_from, row.valid_to, as_of), + } +} diff --git a/packages/elf-service/src/entity_memory/build/sort.rs b/packages/elf-service/src/entity_memory/build/sort.rs new file mode 100644 index 00000000..a7dc1cc9 --- /dev/null +++ b/packages/elf-service/src/entity_memory/build/sort.rs @@ -0,0 +1,30 @@ +use crate::entity_memory::types::EntityMemoryItem; + +pub(in crate::entity_memory) fn sort_entity_memory_items(items: &mut [EntityMemoryItem]) { + items.sort_by(|left, right| { + read_bucket_rank(right.read_bucket.as_str()) + .cmp(&read_bucket_rank(left.read_bucket.as_str())) + .then_with(|| { + lifecycle_rank(right.lifecycle.as_str()) + .cmp(&lifecycle_rank(left.lifecycle.as_str())) + }) + .then_with(|| right.updated_at.cmp(&left.updated_at)) + .then_with(|| left.source.cmp(&right.source)) + }); +} + +fn read_bucket_rank(bucket: &str) -> u8 { + match bucket { + "top_of_mind" => 1, + _ => 0, + } +} + +fn lifecycle_rank(lifecycle: &str) -> u8 { + match lifecycle { + "current" => 3, + "stale" => 2, + "superseded" => 1, + _ => 0, + } +} diff --git a/packages/elf-service/src/entity_memory/build/summary.rs b/packages/elf-service/src/entity_memory/build/summary.rs new file mode 100644 index 00000000..620e1899 --- /dev/null +++ b/packages/elf-service/src/entity_memory/build/summary.rs @@ -0,0 +1,27 @@ +use crate::entity_memory::types::{EntityMemoryItem, EntityMemorySummary}; + +pub(in crate::entity_memory) fn summarize_items(items: &[EntityMemoryItem]) -> EntityMemorySummary { + let mut summary = EntityMemorySummary::default(); + + for item in items { + match item.lifecycle.as_str() { + "current" => summary.current_count += 1, + "stale" => summary.stale_count += 1, + "superseded" => summary.superseded_count += 1, + "tombstoned" => summary.tombstoned_count += 1, + _ => {}, + } + match item.read_bucket.as_str() { + "top_of_mind" => summary.top_of_mind_count += 1, + "background" => summary.background_count += 1, + _ => {}, + } + match item.source.as_str() { + "core_block" => summary.core_block_count += 1, + "archival_note" => summary.archival_note_count += 1, + _ => {}, + } + } + + summary +} diff --git a/packages/elf-service/src/entity_memory/build/visibility.rs b/packages/elf-service/src/entity_memory/build/visibility.rs new file mode 100644 index 00000000..46c06abe --- /dev/null +++ b/packages/elf-service/src/entity_memory/build/visibility.rs @@ -0,0 +1,29 @@ +use std::collections::HashSet; + +use crate::access; + +pub(in crate::entity_memory) fn row_read_allowed( + owner_agent_id: &str, + scope: &str, + requester_agent_id: &str, + allowed_scopes: &[String], + shared_grants: &HashSet, +) -> bool { + if !allowed_scopes.iter().any(|allowed| allowed == scope) { + return false; + } + if scope == "agent_private" { + return owner_agent_id == requester_agent_id; + } + if !matches!(scope, "project_shared" | "org_shared") { + return false; + } + if owner_agent_id == requester_agent_id { + return true; + } + + shared_grants.contains(&access::SharedSpaceGrantKey { + scope: scope.to_string(), + space_owner_agent_id: owner_agent_id.to_string(), + }) +} diff --git a/packages/elf-service/src/entity_memory/tests.rs b/packages/elf-service/src/entity_memory/tests.rs index 860033c0..bd2c1b7a 100644 --- a/packages/elf-service/src/entity_memory/tests.rs +++ b/packages/elf-service/src/entity_memory/tests.rs @@ -3,7 +3,10 @@ use uuid::Uuid; use crate::{ EntityMemoryItem, - entity_memory::{build, storage::EntityCoreBlockRow}, + entity_memory::{ + build::{self, core_blocks, lifecycle}, + storage::EntityCoreBlockRow, + }, }; #[test] @@ -11,17 +14,17 @@ fn entity_memory_note_lifecycle_classifies_current_stale_superseded_and_tombston let as_of = OffsetDateTime::from_unix_timestamp(100).expect("valid timestamp"); let expired = OffsetDateTime::from_unix_timestamp(90).expect("valid timestamp"); - assert_eq!(build::note_lifecycle("active", None, as_of), "current"); - assert_eq!(build::note_lifecycle("active", Some(expired), as_of), "stale"); - assert_eq!(build::note_lifecycle("deprecated", None, as_of), "superseded"); - assert_eq!(build::note_lifecycle("deleted", None, as_of), "tombstoned"); + assert_eq!(lifecycle::note_lifecycle("active", None, as_of), "current"); + assert_eq!(lifecycle::note_lifecycle("active", Some(expired), as_of), "stale"); + assert_eq!(lifecycle::note_lifecycle("deprecated", None, as_of), "superseded"); + assert_eq!(lifecycle::note_lifecycle("deleted", None, as_of), "tombstoned"); } #[test] fn entity_memory_read_bucket_keeps_only_current_high_importance_top_of_mind() { - assert_eq!(build::note_read_bucket("current", 0.8), "top_of_mind"); - assert_eq!(build::note_read_bucket("current", 0.79), "background"); - assert_eq!(build::note_read_bucket("stale", 0.99), "background"); + assert_eq!(lifecycle::note_read_bucket("current", 0.8), "top_of_mind"); + assert_eq!(lifecycle::note_read_bucket("current", 0.79), "background"); + assert_eq!(lifecycle::note_read_bucket("stale", 0.99), "background"); } #[test] @@ -38,8 +41,11 @@ fn entity_memory_core_block_mentions_canonical_or_alias_surface() { updated_at: OffsetDateTime::from_unix_timestamp(100).expect("valid timestamp"), }; - assert!(build::core_block_mentions_entity(&row, &["Alice".to_string(), "Alicia".to_string()])); - assert!(!build::core_block_mentions_entity(&row, &["Bob".to_string()])); + assert!(core_blocks::core_block_mentions_entity( + &row, + &["Alice".to_string(), "Alicia".to_string()] + )); + assert!(!core_blocks::core_block_mentions_entity(&row, &["Bob".to_string()])); } #[test] From b22ad6ca401fb399970242f136d6e16d033b9234 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Tue, 30 Jun 2026 11:05:38 -0400 Subject: [PATCH 3/5] {"schema":"decodex/commit/1","summary":"Split consolidation proposal types","authority":"manual"} --- .../src/consolidation/types/proposals.rs | 254 +----------------- .../consolidation/types/proposals/input.rs | 57 ++++ .../consolidation/types/proposals/requests.rs | 47 ++++ .../types/proposals/responses.rs | 143 ++++++++++ 4 files changed, 261 insertions(+), 240 deletions(-) create mode 100644 packages/elf-service/src/consolidation/types/proposals/input.rs create mode 100644 packages/elf-service/src/consolidation/types/proposals/requests.rs create mode 100644 packages/elf-service/src/consolidation/types/proposals/responses.rs diff --git a/packages/elf-service/src/consolidation/types/proposals.rs b/packages/elf-service/src/consolidation/types/proposals.rs index 49eddd5d..5dc2aba7 100644 --- a/packages/elf-service/src/consolidation/types/proposals.rs +++ b/packages/elf-service/src/consolidation/types/proposals.rs @@ -1,241 +1,15 @@ -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use time::OffsetDateTime; -use uuid::Uuid; - -use crate::consolidation::types::empty_object; -use elf_domain::consolidation::{ - ConsolidationApplyIntent, ConsolidationInputRef, ConsolidationLineage, ConsolidationMarkers, - ConsolidationProposalContract, ConsolidationProposalDiff, ConsolidationReviewAction, - ConsolidationReviewState, ConsolidationUnsupportedClaimFlag, +mod input; +mod requests; +mod responses; + +pub use self::{ + input::ConsolidationProposalInput, + requests::{ + ConsolidationProposalGetRequest, ConsolidationProposalReviewRequest, + ConsolidationProposalsListRequest, + }, + responses::{ + ConsolidationProposalResponse, ConsolidationProposalReviewEventResponse, + ConsolidationProposalsListResponse, + }, }; -use elf_storage::models::{ConsolidationProposal, ConsolidationProposalReviewEvent}; - -/// Fixture proposal input for a consolidation run. -#[derive(Clone, Debug, Deserialize)] -pub struct ConsolidationProposalInput { - /// Proposal kind, such as `derived_note` or `knowledge_page`. - pub proposal_kind: String, - /// Derived-output apply intent. - pub apply_intent: ConsolidationApplyIntent, - /// Source references directly supporting the proposal. - pub source_refs: Vec, - #[serde(default = "empty_object")] - /// Aggregate source snapshot metadata for reviewer inspection. - pub source_snapshot: Value, - /// Proposal lineage. - pub lineage: ConsolidationLineage, - /// Fixture confidence in the proposal. - pub confidence: f32, - #[serde(default)] - /// Unsupported claims reviewers must inspect before accepting the proposal. - pub unsupported_claim_flags: Vec, - #[serde(default)] - /// Review markers for contradiction and staleness checks. - pub markers: ConsolidationMarkers, - /// Reviewable derived-output diff. - pub diff: ConsolidationProposalDiff, - #[serde(default = "empty_object")] - /// Derived target reference, when the target already exists. - pub target_ref: Value, - #[serde(default = "empty_object")] - /// Proposed derived output payload. - pub proposed_payload: Value, -} -impl ConsolidationProposalInput { - pub(in crate::consolidation) fn into_contract(self) -> ConsolidationProposalContract { - ConsolidationProposalContract { - proposal_kind: self.proposal_kind, - apply_intent: self.apply_intent, - source_refs: self.source_refs, - source_snapshot: self.source_snapshot, - lineage: self.lineage, - confidence: self.confidence, - unsupported_claim_flags: self.unsupported_claim_flags, - markers: self.markers, - diff: self.diff, - target_ref: self.target_ref, - proposed_payload: self.proposed_payload, - } - } -} - -/// Request to get one consolidation proposal. -#[derive(Clone, Debug, Deserialize)] -pub struct ConsolidationProposalGetRequest { - /// Tenant that owns the proposal. - pub tenant_id: String, - /// Project that owns the proposal. - pub project_id: String, - /// Proposal identifier. - pub proposal_id: Uuid, -} - -/// Request to list consolidation proposals. -#[derive(Clone, Debug, Deserialize)] -pub struct ConsolidationProposalsListRequest { - /// Tenant that owns the proposals. - pub tenant_id: String, - /// Project that owns the proposals. - pub project_id: String, - /// Optional run filter. - pub run_id: Option, - /// Optional review-state filter. - pub review_state: Option, - /// Maximum number of proposals to return. - pub limit: Option, -} - -/// Response returned by consolidation proposal listing. -#[derive(Clone, Debug, Serialize)] -pub struct ConsolidationProposalsListResponse { - /// Returned proposals. - pub proposals: Vec, -} - -/// Request to apply one proposal review action. -#[derive(Clone, Debug, Deserialize)] -pub struct ConsolidationProposalReviewRequest { - /// Tenant that owns the proposal. - pub tenant_id: String, - /// Project that owns the proposal. - pub project_id: String, - /// Agent performing the review action. - pub reviewer_agent_id: String, - /// Proposal identifier. - pub proposal_id: Uuid, - /// Requested review action. - pub review_action: ConsolidationReviewAction, - /// Optional reviewer comment. - pub review_comment: Option, -} - -/// Public consolidation proposal review audit DTO. -#[derive(Clone, Debug, Serialize)] -pub struct ConsolidationProposalReviewEventResponse { - /// Review event identifier. - pub review_id: Uuid, - /// Reviewed proposal identifier. - pub proposal_id: Uuid, - /// Parent consolidation run identifier. - pub run_id: Uuid, - /// Tenant that owns the proposal. - pub tenant_id: String, - /// Project that owns the proposal. - pub project_id: String, - /// Agent that performed the review action. - pub reviewer_agent_id: String, - /// Review action requested by the reviewer. - pub action: String, - /// Review state before the transition. - pub from_review_state: String, - /// Review state after the transition. - pub to_review_state: String, - /// Optional reviewer comment. - pub review_comment: Option, - /// Creation timestamp. - pub created_at: OffsetDateTime, -} -impl From for ConsolidationProposalReviewEventResponse { - fn from(event: ConsolidationProposalReviewEvent) -> Self { - Self { - review_id: event.review_id, - proposal_id: event.proposal_id, - run_id: event.run_id, - tenant_id: event.tenant_id, - project_id: event.project_id, - reviewer_agent_id: event.reviewer_agent_id, - action: event.action, - from_review_state: event.from_review_state, - to_review_state: event.to_review_state, - review_comment: event.review_comment, - created_at: event.created_at, - } - } -} - -/// Public consolidation proposal DTO. -#[derive(Clone, Debug, Serialize)] -pub struct ConsolidationProposalResponse { - /// Consolidation proposal identifier. - pub proposal_id: Uuid, - /// Parent consolidation run identifier. - pub run_id: Uuid, - /// Tenant that owns the proposal. - pub tenant_id: String, - /// Project that owns the proposal. - pub project_id: String, - /// Agent that registered the proposal. - pub agent_id: String, - /// Versioned consolidation contract schema. - pub contract_schema: String, - /// Proposal kind, such as derived_note or knowledge_page. - pub proposal_kind: String, - /// Derived-output apply intent. - pub apply_intent: String, - /// Current review state. - pub review_state: String, - /// Serialized source references. - pub source_refs: Value, - /// Aggregate source snapshot metadata. - pub source_snapshot: Value, - /// Serialized proposal lineage. - pub lineage: Value, - /// Serialized reviewable diff. - pub diff: Value, - /// Proposal confidence score. - pub confidence: f32, - /// Serialized unsupported-claim flags. - pub unsupported_claim_flags: Value, - /// Serialized contradiction markers. - pub contradiction_markers: Value, - /// Serialized staleness markers. - pub staleness_markers: Value, - /// Serialized derived target reference. - pub target_ref: Value, - /// Serialized proposed derived output payload. - pub proposed_payload: Value, - /// Agent that last reviewed the proposal. - pub reviewer_agent_id: Option, - /// Optional reviewer comment. - pub review_comment: Option, - /// Timestamp of the last review transition. - pub reviewed_at: Option, - /// Creation timestamp. - pub created_at: OffsetDateTime, - /// Last update timestamp. - pub updated_at: OffsetDateTime, - /// Append-only review events for detail readback. - pub review_events: Vec, -} -impl From for ConsolidationProposalResponse { - fn from(proposal: ConsolidationProposal) -> Self { - Self { - proposal_id: proposal.proposal_id, - run_id: proposal.run_id, - tenant_id: proposal.tenant_id, - project_id: proposal.project_id, - agent_id: proposal.agent_id, - contract_schema: proposal.contract_schema, - proposal_kind: proposal.proposal_kind, - apply_intent: proposal.apply_intent, - review_state: proposal.review_state, - source_refs: proposal.source_refs, - source_snapshot: proposal.source_snapshot, - lineage: proposal.lineage, - diff: proposal.diff, - confidence: proposal.confidence, - unsupported_claim_flags: proposal.unsupported_claim_flags, - contradiction_markers: proposal.contradiction_markers, - staleness_markers: proposal.staleness_markers, - target_ref: proposal.target_ref, - proposed_payload: proposal.proposed_payload, - reviewer_agent_id: proposal.reviewer_agent_id, - review_comment: proposal.review_comment, - reviewed_at: proposal.reviewed_at, - created_at: proposal.created_at, - updated_at: proposal.updated_at, - review_events: Vec::new(), - } - } -} diff --git a/packages/elf-service/src/consolidation/types/proposals/input.rs b/packages/elf-service/src/consolidation/types/proposals/input.rs new file mode 100644 index 00000000..6aeddcfa --- /dev/null +++ b/packages/elf-service/src/consolidation/types/proposals/input.rs @@ -0,0 +1,57 @@ +use serde::Deserialize; +use serde_json::Value; + +use crate::consolidation::types::empty_object; +use elf_domain::consolidation::{ + ConsolidationApplyIntent, ConsolidationInputRef, ConsolidationLineage, ConsolidationMarkers, + ConsolidationProposalContract, ConsolidationProposalDiff, ConsolidationUnsupportedClaimFlag, +}; + +/// Fixture proposal input for a consolidation run. +#[derive(Clone, Debug, Deserialize)] +pub struct ConsolidationProposalInput { + /// Proposal kind, such as `derived_note` or `knowledge_page`. + pub proposal_kind: String, + /// Derived-output apply intent. + pub apply_intent: ConsolidationApplyIntent, + /// Source references directly supporting the proposal. + pub source_refs: Vec, + #[serde(default = "empty_object")] + /// Aggregate source snapshot metadata for reviewer inspection. + pub source_snapshot: Value, + /// Proposal lineage. + pub lineage: ConsolidationLineage, + /// Fixture confidence in the proposal. + pub confidence: f32, + #[serde(default)] + /// Unsupported claims reviewers must inspect before accepting the proposal. + pub unsupported_claim_flags: Vec, + #[serde(default)] + /// Review markers for contradiction and staleness checks. + pub markers: ConsolidationMarkers, + /// Reviewable derived-output diff. + pub diff: ConsolidationProposalDiff, + #[serde(default = "empty_object")] + /// Derived target reference, when the target already exists. + pub target_ref: Value, + #[serde(default = "empty_object")] + /// Proposed derived output payload. + pub proposed_payload: Value, +} +impl ConsolidationProposalInput { + pub(in crate::consolidation) fn into_contract(self) -> ConsolidationProposalContract { + ConsolidationProposalContract { + proposal_kind: self.proposal_kind, + apply_intent: self.apply_intent, + source_refs: self.source_refs, + source_snapshot: self.source_snapshot, + lineage: self.lineage, + confidence: self.confidence, + unsupported_claim_flags: self.unsupported_claim_flags, + markers: self.markers, + diff: self.diff, + target_ref: self.target_ref, + proposed_payload: self.proposed_payload, + } + } +} diff --git a/packages/elf-service/src/consolidation/types/proposals/requests.rs b/packages/elf-service/src/consolidation/types/proposals/requests.rs new file mode 100644 index 00000000..86c2c344 --- /dev/null +++ b/packages/elf-service/src/consolidation/types/proposals/requests.rs @@ -0,0 +1,47 @@ +use serde::Deserialize; +use uuid::Uuid; + +use elf_domain::consolidation::{ConsolidationReviewAction, ConsolidationReviewState}; + +/// Request to get one consolidation proposal. +#[derive(Clone, Debug, Deserialize)] +pub struct ConsolidationProposalGetRequest { + /// Tenant that owns the proposal. + pub tenant_id: String, + /// Project that owns the proposal. + pub project_id: String, + /// Proposal identifier. + pub proposal_id: Uuid, +} + +/// Request to list consolidation proposals. +#[derive(Clone, Debug, Deserialize)] +pub struct ConsolidationProposalsListRequest { + /// Tenant that owns the proposals. + pub tenant_id: String, + /// Project that owns the proposals. + pub project_id: String, + /// Optional run filter. + pub run_id: Option, + /// Optional review-state filter. + pub review_state: Option, + /// Maximum number of proposals to return. + pub limit: Option, +} + +/// Request to apply one proposal review action. +#[derive(Clone, Debug, Deserialize)] +pub struct ConsolidationProposalReviewRequest { + /// Tenant that owns the proposal. + pub tenant_id: String, + /// Project that owns the proposal. + pub project_id: String, + /// Agent performing the review action. + pub reviewer_agent_id: String, + /// Proposal identifier. + pub proposal_id: Uuid, + /// Requested review action. + pub review_action: ConsolidationReviewAction, + /// Optional reviewer comment. + pub review_comment: Option, +} diff --git a/packages/elf-service/src/consolidation/types/proposals/responses.rs b/packages/elf-service/src/consolidation/types/proposals/responses.rs new file mode 100644 index 00000000..6719648d --- /dev/null +++ b/packages/elf-service/src/consolidation/types/proposals/responses.rs @@ -0,0 +1,143 @@ +use serde::Serialize; +use serde_json::Value; +use time::OffsetDateTime; +use uuid::Uuid; + +use elf_storage::models::{ConsolidationProposal, ConsolidationProposalReviewEvent}; + +/// Response returned by consolidation proposal listing. +#[derive(Clone, Debug, Serialize)] +pub struct ConsolidationProposalsListResponse { + /// Returned proposals. + pub proposals: Vec, +} + +/// Public consolidation proposal review audit DTO. +#[derive(Clone, Debug, Serialize)] +pub struct ConsolidationProposalReviewEventResponse { + /// Review event identifier. + pub review_id: Uuid, + /// Reviewed proposal identifier. + pub proposal_id: Uuid, + /// Parent consolidation run identifier. + pub run_id: Uuid, + /// Tenant that owns the proposal. + pub tenant_id: String, + /// Project that owns the proposal. + pub project_id: String, + /// Agent that performed the review action. + pub reviewer_agent_id: String, + /// Review action requested by the reviewer. + pub action: String, + /// Review state before the transition. + pub from_review_state: String, + /// Review state after the transition. + pub to_review_state: String, + /// Optional reviewer comment. + pub review_comment: Option, + /// Creation timestamp. + pub created_at: OffsetDateTime, +} +impl From for ConsolidationProposalReviewEventResponse { + fn from(event: ConsolidationProposalReviewEvent) -> Self { + Self { + review_id: event.review_id, + proposal_id: event.proposal_id, + run_id: event.run_id, + tenant_id: event.tenant_id, + project_id: event.project_id, + reviewer_agent_id: event.reviewer_agent_id, + action: event.action, + from_review_state: event.from_review_state, + to_review_state: event.to_review_state, + review_comment: event.review_comment, + created_at: event.created_at, + } + } +} + +/// Public consolidation proposal DTO. +#[derive(Clone, Debug, Serialize)] +pub struct ConsolidationProposalResponse { + /// Consolidation proposal identifier. + pub proposal_id: Uuid, + /// Parent consolidation run identifier. + pub run_id: Uuid, + /// Tenant that owns the proposal. + pub tenant_id: String, + /// Project that owns the proposal. + pub project_id: String, + /// Agent that registered the proposal. + pub agent_id: String, + /// Versioned consolidation contract schema. + pub contract_schema: String, + /// Proposal kind, such as derived_note or knowledge_page. + pub proposal_kind: String, + /// Derived-output apply intent. + pub apply_intent: String, + /// Current review state. + pub review_state: String, + /// Serialized source references. + pub source_refs: Value, + /// Aggregate source snapshot metadata. + pub source_snapshot: Value, + /// Serialized proposal lineage. + pub lineage: Value, + /// Serialized reviewable diff. + pub diff: Value, + /// Proposal confidence score. + pub confidence: f32, + /// Serialized unsupported-claim flags. + pub unsupported_claim_flags: Value, + /// Serialized contradiction markers. + pub contradiction_markers: Value, + /// Serialized staleness markers. + pub staleness_markers: Value, + /// Serialized derived target reference. + pub target_ref: Value, + /// Serialized proposed derived output payload. + pub proposed_payload: Value, + /// Agent that last reviewed the proposal. + pub reviewer_agent_id: Option, + /// Optional reviewer comment. + pub review_comment: Option, + /// Timestamp of the last review transition. + pub reviewed_at: Option, + /// Creation timestamp. + pub created_at: OffsetDateTime, + /// Last update timestamp. + pub updated_at: OffsetDateTime, + /// Append-only review events for detail readback. + pub review_events: Vec, +} +impl From for ConsolidationProposalResponse { + fn from(proposal: ConsolidationProposal) -> Self { + Self { + proposal_id: proposal.proposal_id, + run_id: proposal.run_id, + tenant_id: proposal.tenant_id, + project_id: proposal.project_id, + agent_id: proposal.agent_id, + contract_schema: proposal.contract_schema, + proposal_kind: proposal.proposal_kind, + apply_intent: proposal.apply_intent, + review_state: proposal.review_state, + source_refs: proposal.source_refs, + source_snapshot: proposal.source_snapshot, + lineage: proposal.lineage, + diff: proposal.diff, + confidence: proposal.confidence, + unsupported_claim_flags: proposal.unsupported_claim_flags, + contradiction_markers: proposal.contradiction_markers, + staleness_markers: proposal.staleness_markers, + target_ref: proposal.target_ref, + proposed_payload: proposal.proposed_payload, + reviewer_agent_id: proposal.reviewer_agent_id, + review_comment: proposal.review_comment, + reviewed_at: proposal.reviewed_at, + created_at: proposal.created_at, + updated_at: proposal.updated_at, + review_events: Vec::new(), + } + } +} From c2821ec13e1f7fd34078166ac1e9d357a273c98e Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Tue, 30 Jun 2026 11:12:20 -0400 Subject: [PATCH 4/5] {"schema":"decodex/commit/1","summary":"Split work journal types","authority":"manual"} --- .../elf-service/src/work_journal/service.rs | 9 +- .../elf-service/src/work_journal/tests.rs | 2 +- .../elf-service/src/work_journal/types.rs | 257 ++---------------- .../src/work_journal/types/constants.rs | 10 + .../src/work_journal/types/family.rs | 48 ++++ .../src/work_journal/types/requests.rs | 84 ++++++ .../src/work_journal/types/responses.rs | 91 +++++++ .../src/work_journal/types/validated.rs | 18 ++ .../src/work_journal/validation.rs | 7 +- 9 files changed, 281 insertions(+), 245 deletions(-) create mode 100644 packages/elf-service/src/work_journal/types/constants.rs create mode 100644 packages/elf-service/src/work_journal/types/family.rs create mode 100644 packages/elf-service/src/work_journal/types/requests.rs create mode 100644 packages/elf-service/src/work_journal/types/responses.rs create mode 100644 packages/elf-service/src/work_journal/types/validated.rs diff --git a/packages/elf-service/src/work_journal/service.rs b/packages/elf-service/src/work_journal/service.rs index 0162f2d3..845e3815 100644 --- a/packages/elf-service/src/work_journal/service.rs +++ b/packages/elf-service/src/work_journal/service.rs @@ -9,10 +9,13 @@ use crate::{ search, work_journal::{ types::{ - DEFAULT_SESSION_READBACK_LIMIT, ELF_WORK_JOURNAL_SCHEMA_V1, MAX_SESSION_READBACK_LIMIT, - MAX_STORAGE_SCAN_ROWS, WorkJournalEntryCreateRequest, WorkJournalEntryCreateResponse, - WorkJournalEntryFamily, WorkJournalEntryGetRequest, WorkJournalEntryResponse, + WorkJournalEntryCreateRequest, WorkJournalEntryCreateResponse, WorkJournalEntryFamily, + WorkJournalEntryGetRequest, WorkJournalEntryResponse, WorkJournalSessionReadbackRequest, WorkJournalSessionReadbackResponse, + constants::{ + DEFAULT_SESSION_READBACK_LIMIT, ELF_WORK_JOURNAL_SCHEMA_V1, + MAX_SESSION_READBACK_LIMIT, MAX_STORAGE_SCAN_ROWS, + }, }, validation::{self}, }, diff --git a/packages/elf-service/src/work_journal/tests.rs b/packages/elf-service/src/work_journal/tests.rs index 99304e3e..40971099 100644 --- a/packages/elf-service/src/work_journal/tests.rs +++ b/packages/elf-service/src/work_journal/tests.rs @@ -6,7 +6,7 @@ use uuid::Uuid; use crate::{ access::SharedSpaceGrantKey, - work_journal::{types::WORK_JOURNAL_PROMOTION_BOUNDARY_SCHEMA_V1, validation}, + work_journal::{types::constants::WORK_JOURNAL_PROMOTION_BOUNDARY_SCHEMA_V1, validation}, }; use elf_storage::models::WorkJournalEntry; diff --git a/packages/elf-service/src/work_journal/types.rs b/packages/elf-service/src/work_journal/types.rs index 62c215fe..c3745488 100644 --- a/packages/elf-service/src/work_journal/types.rs +++ b/packages/elf-service/src/work_journal/types.rs @@ -1,239 +1,18 @@ -use serde::{Deserialize, Serialize}; -use serde_json::{Map, Value}; -use time::OffsetDateTime; -use uuid::Uuid; - -use crate::{Error, Result}; -use elf_domain::writegate::{WritePolicy, WritePolicyAudit}; - -/// Schema identifier for Work Journal readback. -pub const ELF_WORK_JOURNAL_SCHEMA_V1: &str = "elf.work_journal/v1"; - -pub(super) const WORK_JOURNAL_PROMOTION_BOUNDARY_SCHEMA_V1: &str = - "elf.work_journal.promotion_boundary/v1"; -pub(super) const DEFAULT_SESSION_READBACK_LIMIT: u32 = 20; -pub(super) const MAX_SESSION_READBACK_LIMIT: u32 = 100; -pub(super) const MAX_STORAGE_SCAN_ROWS: i64 = 500; -pub(super) const MAX_BODY_CHARS: usize = 16_384; -pub(super) const MAX_SIDE_LIST_ITEMS: usize = 64; - -/// Work Journal entry family. -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum WorkJournalEntryFamily { - /// Session log captured alongside source work. - SessionLog, - /// Handoff brief for another agent or future session. - HandoffBrief, - /// Janitor or cleanup report. - JanitorReport, - /// Explicit next step stated in the source. - ExplicitNextStep, - /// Inferred next step retained as a non-authoritative hint. - InferredNextStep, - /// Option that was considered and rejected. - RejectedOption, -} -impl WorkJournalEntryFamily { - /// Returns the canonical API/storage string. - pub fn as_str(self) -> &'static str { - match self { - Self::SessionLog => "session_log", - Self::HandoffBrief => "handoff_brief", - Self::JanitorReport => "janitor_report", - Self::ExplicitNextStep => "explicit_next_step", - Self::InferredNextStep => "inferred_next_step", - Self::RejectedOption => "rejected_option", - } - } - - pub(super) fn parse(raw: &str) -> Result { - match raw { - "session_log" => Ok(Self::SessionLog), - "handoff_brief" => Ok(Self::HandoffBrief), - "janitor_report" => Ok(Self::JanitorReport), - "explicit_next_step" => Ok(Self::ExplicitNextStep), - "inferred_next_step" => Ok(Self::InferredNextStep), - "rejected_option" => Ok(Self::RejectedOption), - _ => Err(Error::InvalidRequest { - message: "family must be one of: session_log, handoff_brief, janitor_report, explicit_next_step, inferred_next_step, rejected_option.".to_string(), - }), - } - } -} - -/// Request payload for source-adjacent Work Journal capture. -#[derive(Clone, Debug, Deserialize)] -pub struct WorkJournalEntryCreateRequest { - /// Tenant that owns the entry. - pub tenant_id: String, - /// Project that owns the entry. - pub project_id: String, - /// Agent capturing the entry. - pub agent_id: String, - /// Optional caller-supplied stable identifier. - pub entry_id: Option, - /// Visibility scope for readback. - pub scope: String, - /// Stable session identifier for grouping entries. - pub session_id: String, - /// Entry family. - pub family: WorkJournalEntryFamily, - /// Optional display title. - pub title: Option, - /// Journal body. This is source-adjacent, not authoritative memory. - pub body: String, - /// Source refs that support the journal entry. - pub source_refs: Vec, - /// Redaction/exclusion policy applied before persistence. - pub write_policy: Option, - #[serde(default)] - /// Explicit next steps stated by the captured source. - pub explicit_next_steps: Vec, - #[serde(default)] - /// Inferred next steps retained as non-authoritative hints. - pub inferred_next_steps: Vec, - #[serde(default)] - /// Options considered and rejected during the captured work. - pub rejected_options: Vec, - #[serde(default = "empty_object")] - /// Promotion boundary metadata. - pub promotion_boundary: Value, -} - -/// Response payload after Work Journal capture. -#[derive(Clone, Debug, Serialize)] -pub struct WorkJournalEntryCreateResponse { - /// Stored Work Journal entry. - pub entry: WorkJournalEntryResponse, -} - -/// Request payload for one Work Journal entry lookup. -#[derive(Clone, Debug, Deserialize)] -pub struct WorkJournalEntryGetRequest { - /// Tenant that owns the entry. - pub tenant_id: String, - /// Project used for read-profile and shared-grant checks. - pub project_id: String, - /// Agent requesting the read. - pub agent_id: String, - /// Read profile that determines visible scopes. - pub read_profile: String, - /// Entry identifier. - pub entry_id: Uuid, -} - -/// Request payload for session-level Work Journal readback. -#[derive(Clone, Debug, Deserialize)] -pub struct WorkJournalSessionReadbackRequest { - /// Tenant that owns the session. - pub tenant_id: String, - /// Project used for read-profile and shared-grant checks. - pub project_id: String, - /// Agent requesting the read. - pub agent_id: String, - /// Read profile that determines visible scopes. - pub read_profile: String, - /// Stable session identifier to read. - pub session_id: String, - #[serde(default)] - /// Optional family filter. - pub families: Vec, - /// Maximum number of returned entries. - pub limit: Option, -} - -/// Session-level Work Journal readback. -#[derive(Clone, Debug, Serialize)] -pub struct WorkJournalSessionReadbackResponse { - /// Readback schema identifier. - pub schema: String, - /// Stable session identifier. - pub session_id: String, - /// Newest-first journal entries. - pub items: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - /// Compact "where did we stop" projection from the returned entries. - pub where_stopped: Option, -} - -/// One source-adjacent Work Journal entry returned by readback. -#[derive(Clone, Debug, Serialize)] -pub struct WorkJournalEntryResponse { - /// Readback schema identifier. - pub schema: String, - /// Journal entry identifier. - pub entry_id: Uuid, - /// Tenant that owns the entry. - pub tenant_id: String, - /// Project that owns the entry. - pub project_id: String, - /// Agent that captured the entry. - pub agent_id: String, - /// Visibility scope for readback. - pub scope: String, - /// Stable session identifier. - pub session_id: String, - /// Entry family. - pub family: WorkJournalEntryFamily, - /// Lifecycle status. - pub status: String, - #[serde(skip_serializing_if = "Option::is_none")] - /// Optional display title. - pub title: Option, - /// Redacted durable journal body. - pub body: String, - /// Source refs supporting the entry. - pub source_refs: Vec, - /// Explicit next steps stated by the captured source. - pub explicit_next_steps: Vec, - /// Inferred next steps retained as non-authoritative hints. - pub inferred_next_steps: Vec, - /// Rejected options captured by the journal. - pub rejected_options: Vec, - /// Promotion boundary metadata. - pub promotion_boundary: Value, - /// Redaction audit for the durable journal body. - pub redaction_audit: WritePolicyAudit, - /// Creation timestamp. - pub created_at: OffsetDateTime, - /// Last update timestamp. - pub updated_at: OffsetDateTime, -} - -/// Compact "where did we stop" projection for one journal session. -#[derive(Clone, Debug, Serialize)] -pub struct WorkJournalWhereStopped { - /// Latest returned entry identifier. - pub latest_entry_id: Uuid, - /// Latest returned entry family. - pub latest_family: WorkJournalEntryFamily, - /// Source refs associated with the latest returned entry. - pub source_refs: Vec, - /// Most recent explicit next steps in returned entries. - pub explicit_next_steps: Vec, - /// Most recent inferred next steps in returned entries. - pub inferred_next_steps: Vec, - /// Most recent rejected options in returned entries. - pub rejected_options: Vec, - /// Promotion boundary for the latest returned entry. - pub promotion_boundary: Value, -} - -pub(super) struct ValidatedWorkJournalCreate { - pub(super) entry_id: Uuid, - pub(super) scope: String, - pub(super) session_id: String, - pub(super) title: Option, - pub(super) body: String, - pub(super) source_refs: Value, - pub(super) explicit_next_steps: Value, - pub(super) inferred_next_steps: Value, - pub(super) rejected_options: Value, - pub(super) promotion_boundary: Value, - pub(super) redaction_audit: WritePolicyAudit, -} - -fn empty_object() -> Value { - Value::Object(Map::new()) -} +pub(in crate::work_journal) mod constants; +pub(in crate::work_journal) mod family; +pub(in crate::work_journal) mod requests; +pub(in crate::work_journal) mod responses; +pub(in crate::work_journal) mod validated; + +pub use self::{ + constants::ELF_WORK_JOURNAL_SCHEMA_V1, + family::WorkJournalEntryFamily, + requests::{ + WorkJournalEntryCreateRequest, WorkJournalEntryGetRequest, + WorkJournalSessionReadbackRequest, + }, + responses::{ + WorkJournalEntryCreateResponse, WorkJournalEntryResponse, + WorkJournalSessionReadbackResponse, WorkJournalWhereStopped, + }, +}; diff --git a/packages/elf-service/src/work_journal/types/constants.rs b/packages/elf-service/src/work_journal/types/constants.rs new file mode 100644 index 00000000..5820f39a --- /dev/null +++ b/packages/elf-service/src/work_journal/types/constants.rs @@ -0,0 +1,10 @@ +/// Schema identifier for Work Journal readback. +pub const ELF_WORK_JOURNAL_SCHEMA_V1: &str = "elf.work_journal/v1"; + +pub(in crate::work_journal) const WORK_JOURNAL_PROMOTION_BOUNDARY_SCHEMA_V1: &str = + "elf.work_journal.promotion_boundary/v1"; +pub(in crate::work_journal) const DEFAULT_SESSION_READBACK_LIMIT: u32 = 20; +pub(in crate::work_journal) const MAX_SESSION_READBACK_LIMIT: u32 = 100; +pub(in crate::work_journal) const MAX_STORAGE_SCAN_ROWS: i64 = 500; +pub(in crate::work_journal) const MAX_BODY_CHARS: usize = 16_384; +pub(in crate::work_journal) const MAX_SIDE_LIST_ITEMS: usize = 64; diff --git a/packages/elf-service/src/work_journal/types/family.rs b/packages/elf-service/src/work_journal/types/family.rs new file mode 100644 index 00000000..fd3a822a --- /dev/null +++ b/packages/elf-service/src/work_journal/types/family.rs @@ -0,0 +1,48 @@ +use serde::{Deserialize, Serialize}; + +use crate::{Error, Result}; + +/// Work Journal entry family. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum WorkJournalEntryFamily { + /// Session log captured alongside source work. + SessionLog, + /// Handoff brief for another agent or future session. + HandoffBrief, + /// Janitor or cleanup report. + JanitorReport, + /// Explicit next step stated in the source. + ExplicitNextStep, + /// Inferred next step retained as a non-authoritative hint. + InferredNextStep, + /// Option that was considered and rejected. + RejectedOption, +} +impl WorkJournalEntryFamily { + /// Returns the canonical API/storage string. + pub fn as_str(self) -> &'static str { + match self { + Self::SessionLog => "session_log", + Self::HandoffBrief => "handoff_brief", + Self::JanitorReport => "janitor_report", + Self::ExplicitNextStep => "explicit_next_step", + Self::InferredNextStep => "inferred_next_step", + Self::RejectedOption => "rejected_option", + } + } + + pub(in crate::work_journal) fn parse(raw: &str) -> Result { + match raw { + "session_log" => Ok(Self::SessionLog), + "handoff_brief" => Ok(Self::HandoffBrief), + "janitor_report" => Ok(Self::JanitorReport), + "explicit_next_step" => Ok(Self::ExplicitNextStep), + "inferred_next_step" => Ok(Self::InferredNextStep), + "rejected_option" => Ok(Self::RejectedOption), + _ => Err(Error::InvalidRequest { + message: "family must be one of: session_log, handoff_brief, janitor_report, explicit_next_step, inferred_next_step, rejected_option.".to_string(), + }), + } + } +} diff --git a/packages/elf-service/src/work_journal/types/requests.rs b/packages/elf-service/src/work_journal/types/requests.rs new file mode 100644 index 00000000..736dc58d --- /dev/null +++ b/packages/elf-service/src/work_journal/types/requests.rs @@ -0,0 +1,84 @@ +use serde::Deserialize; +use serde_json::{Map, Value}; +use uuid::Uuid; + +use crate::work_journal::types::WorkJournalEntryFamily; +use elf_domain::writegate::WritePolicy; + +/// Request payload for source-adjacent Work Journal capture. +#[derive(Clone, Debug, Deserialize)] +pub struct WorkJournalEntryCreateRequest { + /// Tenant that owns the entry. + pub tenant_id: String, + /// Project that owns the entry. + pub project_id: String, + /// Agent capturing the entry. + pub agent_id: String, + /// Optional caller-supplied stable identifier. + pub entry_id: Option, + /// Visibility scope for readback. + pub scope: String, + /// Stable session identifier for grouping entries. + pub session_id: String, + /// Entry family. + pub family: WorkJournalEntryFamily, + /// Optional display title. + pub title: Option, + /// Journal body. This is source-adjacent, not authoritative memory. + pub body: String, + /// Source refs that support the journal entry. + pub source_refs: Vec, + /// Redaction/exclusion policy applied before persistence. + pub write_policy: Option, + #[serde(default)] + /// Explicit next steps stated by the captured source. + pub explicit_next_steps: Vec, + #[serde(default)] + /// Inferred next steps retained as non-authoritative hints. + pub inferred_next_steps: Vec, + #[serde(default)] + /// Options considered and rejected during the captured work. + pub rejected_options: Vec, + #[serde(default = "empty_object")] + /// Promotion boundary metadata. + pub promotion_boundary: Value, +} + +/// Request payload for one Work Journal entry lookup. +#[derive(Clone, Debug, Deserialize)] +pub struct WorkJournalEntryGetRequest { + /// Tenant that owns the entry. + pub tenant_id: String, + /// Project used for read-profile and shared-grant checks. + pub project_id: String, + /// Agent requesting the read. + pub agent_id: String, + /// Read profile that determines visible scopes. + pub read_profile: String, + /// Entry identifier. + pub entry_id: Uuid, +} + +/// Request payload for session-level Work Journal readback. +#[derive(Clone, Debug, Deserialize)] +pub struct WorkJournalSessionReadbackRequest { + /// Tenant that owns the session. + pub tenant_id: String, + /// Project used for read-profile and shared-grant checks. + pub project_id: String, + /// Agent requesting the read. + pub agent_id: String, + /// Read profile that determines visible scopes. + pub read_profile: String, + /// Stable session identifier to read. + pub session_id: String, + #[serde(default)] + /// Optional family filter. + pub families: Vec, + /// Maximum number of returned entries. + pub limit: Option, +} + +fn empty_object() -> Value { + Value::Object(Map::new()) +} diff --git a/packages/elf-service/src/work_journal/types/responses.rs b/packages/elf-service/src/work_journal/types/responses.rs new file mode 100644 index 00000000..0a376bf6 --- /dev/null +++ b/packages/elf-service/src/work_journal/types/responses.rs @@ -0,0 +1,91 @@ +use serde::Serialize; +use serde_json::Value; +use time::OffsetDateTime; +use uuid::Uuid; + +use crate::work_journal::types::WorkJournalEntryFamily; +use elf_domain::writegate::WritePolicyAudit; + +/// Response payload after Work Journal capture. +#[derive(Clone, Debug, Serialize)] +pub struct WorkJournalEntryCreateResponse { + /// Stored Work Journal entry. + pub entry: WorkJournalEntryResponse, +} + +/// Session-level Work Journal readback. +#[derive(Clone, Debug, Serialize)] +pub struct WorkJournalSessionReadbackResponse { + /// Readback schema identifier. + pub schema: String, + /// Stable session identifier. + pub session_id: String, + /// Newest-first journal entries. + pub items: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + /// Compact "where did we stop" projection from the returned entries. + pub where_stopped: Option, +} + +/// One source-adjacent Work Journal entry returned by readback. +#[derive(Clone, Debug, Serialize)] +pub struct WorkJournalEntryResponse { + /// Readback schema identifier. + pub schema: String, + /// Journal entry identifier. + pub entry_id: Uuid, + /// Tenant that owns the entry. + pub tenant_id: String, + /// Project that owns the entry. + pub project_id: String, + /// Agent that captured the entry. + pub agent_id: String, + /// Visibility scope for readback. + pub scope: String, + /// Stable session identifier. + pub session_id: String, + /// Entry family. + pub family: WorkJournalEntryFamily, + /// Lifecycle status. + pub status: String, + #[serde(skip_serializing_if = "Option::is_none")] + /// Optional display title. + pub title: Option, + /// Redacted durable journal body. + pub body: String, + /// Source refs supporting the entry. + pub source_refs: Vec, + /// Explicit next steps stated by the captured source. + pub explicit_next_steps: Vec, + /// Inferred next steps retained as non-authoritative hints. + pub inferred_next_steps: Vec, + /// Rejected options captured by the journal. + pub rejected_options: Vec, + /// Promotion boundary metadata. + pub promotion_boundary: Value, + /// Redaction audit for the durable journal body. + pub redaction_audit: WritePolicyAudit, + /// Creation timestamp. + pub created_at: OffsetDateTime, + /// Last update timestamp. + pub updated_at: OffsetDateTime, +} + +/// Compact "where did we stop" projection for one journal session. +#[derive(Clone, Debug, Serialize)] +pub struct WorkJournalWhereStopped { + /// Latest returned entry identifier. + pub latest_entry_id: Uuid, + /// Latest returned entry family. + pub latest_family: WorkJournalEntryFamily, + /// Source refs associated with the latest returned entry. + pub source_refs: Vec, + /// Most recent explicit next steps in returned entries. + pub explicit_next_steps: Vec, + /// Most recent inferred next steps in returned entries. + pub inferred_next_steps: Vec, + /// Most recent rejected options in returned entries. + pub rejected_options: Vec, + /// Promotion boundary for the latest returned entry. + pub promotion_boundary: Value, +} diff --git a/packages/elf-service/src/work_journal/types/validated.rs b/packages/elf-service/src/work_journal/types/validated.rs new file mode 100644 index 00000000..0cbc21cb --- /dev/null +++ b/packages/elf-service/src/work_journal/types/validated.rs @@ -0,0 +1,18 @@ +use serde_json::Value; +use uuid::Uuid; + +use elf_domain::writegate::WritePolicyAudit; + +pub(in crate::work_journal) struct ValidatedWorkJournalCreate { + pub(in crate::work_journal) entry_id: Uuid, + pub(in crate::work_journal) scope: String, + pub(in crate::work_journal) session_id: String, + pub(in crate::work_journal) title: Option, + pub(in crate::work_journal) body: String, + pub(in crate::work_journal) source_refs: Value, + pub(in crate::work_journal) explicit_next_steps: Value, + pub(in crate::work_journal) inferred_next_steps: Value, + pub(in crate::work_journal) rejected_options: Value, + pub(in crate::work_journal) promotion_boundary: Value, + pub(in crate::work_journal) redaction_audit: WritePolicyAudit, +} diff --git a/packages/elf-service/src/work_journal/validation.rs b/packages/elf-service/src/work_journal/validation.rs index d4ad4311..62ef9868 100644 --- a/packages/elf-service/src/work_journal/validation.rs +++ b/packages/elf-service/src/work_journal/validation.rs @@ -32,10 +32,13 @@ use crate::{ ElfService, Error, Result, access::{ORG_PROJECT_ID, SharedSpaceGrantKey}, work_journal::types::{ - ELF_WORK_JOURNAL_SCHEMA_V1, MAX_BODY_CHARS, MAX_SIDE_LIST_ITEMS, - ValidatedWorkJournalCreate, WORK_JOURNAL_PROMOTION_BOUNDARY_SCHEMA_V1, WorkJournalEntryCreateRequest, WorkJournalEntryFamily, WorkJournalEntryResponse, WorkJournalWhereStopped, + constants::{ + ELF_WORK_JOURNAL_SCHEMA_V1, MAX_BODY_CHARS, MAX_SIDE_LIST_ITEMS, + WORK_JOURNAL_PROMOTION_BOUNDARY_SCHEMA_V1, + }, + validated::ValidatedWorkJournalCreate, }, }; use elf_config::Config; From 68c9f49f93b9443cac68f7405d23d5076721d0ff Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Tue, 30 Jun 2026 11:17:26 -0400 Subject: [PATCH 5/5] {"schema":"decodex/commit/1","summary":"Split CLI argument modules","authority":"manual"} --- apps/elf-cli/src/args.rs | 345 ++------------------------- apps/elf-cli/src/args/commands.rs | 34 +++ apps/elf-cli/src/args/common.rs | 55 +++++ apps/elf-cli/src/args/constants.rs | 6 + apps/elf-cli/src/args/diagnostics.rs | 82 +++++++ apps/elf-cli/src/args/memory.rs | 72 ++++++ apps/elf-cli/src/args/search.rs | 91 +++++++ 7 files changed, 358 insertions(+), 327 deletions(-) create mode 100644 apps/elf-cli/src/args/commands.rs create mode 100644 apps/elf-cli/src/args/common.rs create mode 100644 apps/elf-cli/src/args/constants.rs create mode 100644 apps/elf-cli/src/args/diagnostics.rs create mode 100644 apps/elf-cli/src/args/memory.rs create mode 100644 apps/elf-cli/src/args/search.rs diff --git a/apps/elf-cli/src/args.rs b/apps/elf-cli/src/args.rs index 981b3a24..c63645cc 100644 --- a/apps/elf-cli/src/args.rs +++ b/apps/elf-cli/src/args.rs @@ -1,328 +1,19 @@ -mod benchmark; - -pub(crate) use self::benchmark::{ - BenchmarkArgs, BenchmarkCommand, BenchmarkReportArgs, BenchmarkRunArgs, +pub(in crate::args) mod benchmark; +pub(in crate::args) mod commands; +pub(in crate::args) mod common; +pub(in crate::args) mod constants; +pub(in crate::args) mod diagnostics; +pub(in crate::args) mod memory; +pub(in crate::args) mod search; + +pub(crate) use self::{ + benchmark::{BenchmarkArgs, BenchmarkCommand, BenchmarkReportArgs, BenchmarkRunArgs}, + commands::{Cli, Commands}, + common::{AdminEndpointArgs, ContextArgs, OutputArgs, PublicEndpointArgs, ReadContextArgs}, + diagnostics::{ + AdminPostArgs, DiagnosticsArgs, DiagnosticsCommand, NoteProvenanceArgs, RecentTracesArgs, + TraceBundleArgs, + }, + memory::{AddNoteArgs, BackfillArgs, StatusArgs}, + search::{AdminSearchArgs, PayloadLevel, SearchArgs, SearchMode}, }; - -use clap::{Args, Parser, Subcommand, ValueEnum}; - -const DEFAULT_API_URL: &str = "http://127.0.0.1:51892"; -const DEFAULT_ADMIN_URL: &str = "http://127.0.0.1:51891"; -const DEFAULT_TENANT_ID: &str = "local-tenant"; -const DEFAULT_PROJECT_ID: &str = "local-project"; -const DEFAULT_AGENT_ID: &str = "local-agent"; -const DEFAULT_READ_PROFILE: &str = "private_only"; - -#[derive(Debug, Parser)] -#[command( - version = elf_cli::VERSION, - rename_all = "kebab", - styles = elf_cli::styles(), - about = "Local ELF workflow wrappers over the HTTP API and repo benchmark tasks." -)] -pub(crate) struct Cli { - #[command(subcommand)] - pub(crate) command: Commands, -} - -#[derive(Debug, Args)] -pub(crate) struct PublicEndpointArgs { - /// Public ELF API base URL. - #[arg(long, env = "ELF_API_URL", default_value = DEFAULT_API_URL)] - pub(crate) api_url: String, - /// Optional bearer token for static-key auth. - #[arg(long, env = "ELF_USER_TOKEN")] - pub(crate) token: Option, -} - -#[derive(Debug, Args)] -pub(crate) struct AdminEndpointArgs { - /// Admin ELF API base URL. - #[arg(long, env = "ELF_ADMIN_URL", default_value = DEFAULT_ADMIN_URL)] - pub(crate) admin_url: String, - /// Optional admin bearer token for static-key auth. - #[arg(long, env = "ELF_ADMIN_TOKEN")] - pub(crate) admin_token: Option, -} - -#[derive(Clone, Debug, Args)] -pub(crate) struct ContextArgs { - /// Tenant id sent in X-ELF-Tenant-Id. - #[arg(long, env = "ELF_TENANT_ID", default_value = DEFAULT_TENANT_ID)] - pub(crate) tenant_id: String, - /// Project id sent in X-ELF-Project-Id. - #[arg(long, env = "ELF_PROJECT_ID", default_value = DEFAULT_PROJECT_ID)] - pub(crate) project_id: String, - /// Agent id sent in X-ELF-Agent-Id. - #[arg(long, env = "ELF_AGENT_ID", default_value = DEFAULT_AGENT_ID)] - pub(crate) agent_id: String, -} - -#[derive(Clone, Debug, Args)] -pub(crate) struct ReadContextArgs { - #[command(flatten)] - pub(crate) context: ContextArgs, - /// Read profile sent in X-ELF-Read-Profile. - #[arg(long, env = "ELF_READ_PROFILE", default_value = DEFAULT_READ_PROFILE)] - pub(crate) read_profile: String, -} - -#[derive(Debug, Args)] -pub(crate) struct OutputArgs { - /// Pretty-print the JSON output. - #[arg(long)] - pub(crate) pretty: bool, -} - -#[derive(Debug, Args)] -pub(crate) struct AddNoteArgs { - #[command(flatten)] - pub(crate) endpoint: PublicEndpointArgs, - #[command(flatten)] - pub(crate) context: ContextArgs, - #[command(flatten)] - pub(crate) output: OutputArgs, - /// Scope applied to the note. - #[arg(long, default_value = "agent_private")] - pub(crate) scope: String, - /// Memory note type. - #[arg(long = "type", default_value = "fact")] - pub(crate) note_type: String, - /// Optional note key used by the update resolver. - #[arg(long)] - pub(crate) key: Option, - /// English note text. - #[arg(long)] - pub(crate) text: String, - /// Ranking importance value. - #[arg(long, default_value_t = 0.7)] - pub(crate) importance: f32, - /// Ranking confidence value. - #[arg(long, default_value_t = 0.9)] - pub(crate) confidence: f32, - /// Optional TTL override in days. - #[arg(long)] - pub(crate) ttl_days: Option, - /// Operator-visible source id copied into source_ref.ref.source_id. - #[arg(long)] - pub(crate) source_id: Option, - /// Full JSON object source_ref override. - #[arg(long)] - pub(crate) source_ref_json: Option, -} - -#[derive(Debug, Args)] -pub(crate) struct SearchArgs { - #[command(flatten)] - pub(crate) endpoint: PublicEndpointArgs, - #[command(flatten)] - pub(crate) read_context: ReadContextArgs, - #[command(flatten)] - pub(crate) output: OutputArgs, - /// English query string. - #[arg(long)] - pub(crate) query: String, - /// Search mode to request from the service. - #[arg(long, value_enum, default_value_t = SearchMode::QuickFind)] - pub(crate) mode: SearchMode, - /// Number of final items to return. - #[arg(long)] - pub(crate) top_k: Option, - /// Candidate breadth before ranking. - #[arg(long)] - pub(crate) candidate_k: Option, - /// Payload level requested from the service. - #[arg(long, value_enum, default_value_t = PayloadLevel::L0)] - pub(crate) payload_level: PayloadLevel, - /// Optional search filter JSON object. - #[arg(long)] - pub(crate) filter_json: Option, -} - -#[derive(Debug, Args)] -pub(crate) struct StatusArgs { - #[command(flatten)] - pub(crate) endpoint: PublicEndpointArgs, - #[command(flatten)] - pub(crate) output: OutputArgs, -} - -#[derive(Debug, Args)] -pub(crate) struct BackfillArgs { - #[command(flatten)] - pub(crate) output: OutputArgs, - /// Backfill corpus document count override. - #[arg(long)] - pub(crate) docs: Option, - /// Worker concurrency override for the backfill runner. - #[arg(long)] - pub(crate) worker_concurrency: Option, - /// Use the checked-in 10k operator profile task. - #[arg(long)] - pub(crate) ten_k: bool, - /// Use the guarded 100k operator profile task. - #[arg(long, conflicts_with = "ten_k")] - pub(crate) hundred_k: bool, - /// Set the required expensive-run guard for the 100k task. - #[arg(long)] - pub(crate) enable_expensive: bool, - /// Print the resolved task and environment without running it. - #[arg(long)] - pub(crate) dry_run: bool, -} - -#[derive(Debug, Args)] -pub(crate) struct DiagnosticsArgs { - #[command(subcommand)] - pub(crate) command: DiagnosticsCommand, -} - -#[derive(Debug, Args)] -pub(crate) struct AdminPostArgs { - #[command(flatten)] - pub(crate) endpoint: AdminEndpointArgs, - #[command(flatten)] - pub(crate) context: ContextArgs, - #[command(flatten)] - pub(crate) output: OutputArgs, -} - -#[derive(Debug, Args)] -pub(crate) struct AdminSearchArgs { - #[command(flatten)] - pub(crate) endpoint: AdminEndpointArgs, - #[command(flatten)] - pub(crate) read_context: ReadContextArgs, - #[command(flatten)] - pub(crate) output: OutputArgs, - /// English query string. - #[arg(long)] - pub(crate) query: String, - /// Search mode to request from the service. - #[arg(long, value_enum, default_value_t = SearchMode::QuickFind)] - pub(crate) mode: SearchMode, - /// Number of final items to return. - #[arg(long)] - pub(crate) top_k: Option, - /// Candidate breadth before ranking. - #[arg(long)] - pub(crate) candidate_k: Option, - /// Payload level requested from the service. - #[arg(long, value_enum, default_value_t = PayloadLevel::L2)] - pub(crate) payload_level: PayloadLevel, - /// Optional search filter JSON object. - #[arg(long)] - pub(crate) filter_json: Option, -} - -#[derive(Debug, Args)] -pub(crate) struct RecentTracesArgs { - #[command(flatten)] - pub(crate) endpoint: AdminEndpointArgs, - #[command(flatten)] - pub(crate) context: ContextArgs, - #[command(flatten)] - pub(crate) output: OutputArgs, - /// Maximum trace headers to return. - #[arg(long)] - pub(crate) limit: Option, -} - -#[derive(Debug, Args)] -pub(crate) struct TraceBundleArgs { - #[command(flatten)] - pub(crate) endpoint: AdminEndpointArgs, - #[command(flatten)] - pub(crate) context: ContextArgs, - #[command(flatten)] - pub(crate) output: OutputArgs, - /// Trace id to load. - #[arg(long)] - pub(crate) trace_id: String, - /// Bundle mode: bounded or full. - #[arg(long, default_value = "bounded")] - pub(crate) mode: String, - /// Optional per-stage item cap. - #[arg(long)] - pub(crate) stage_items_limit: Option, - /// Optional replay candidate cap. - #[arg(long)] - pub(crate) candidates_limit: Option, -} - -#[derive(Debug, Args)] -pub(crate) struct NoteProvenanceArgs { - #[command(flatten)] - pub(crate) endpoint: AdminEndpointArgs, - #[command(flatten)] - pub(crate) context: ContextArgs, - #[command(flatten)] - pub(crate) output: OutputArgs, - /// Note id to inspect. - #[arg(long)] - pub(crate) note_id: String, -} - -#[derive(Debug, Subcommand)] -#[command(rename_all = "kebab")] -pub(crate) enum Commands { - /// Add one deterministic note through POST /v2/notes/ingest. - AddNote(AddNoteArgs), - /// Create a search session through POST /v2/searches. - Search(SearchArgs), - /// Check local API process health. - Status(StatusArgs), - /// Run the checked-in resumable backfill benchmark workflow. - Backfill(BackfillArgs), - /// Run or render checked-in live baseline benchmark reports. - Benchmark(BenchmarkArgs), - /// Read production diagnostics through admin HTTP endpoints. - Diagnostics(DiagnosticsArgs), -} - -#[derive(Clone, Copy, Debug, ValueEnum)] -#[value(rename_all = "snake_case")] -pub(crate) enum SearchMode { - QuickFind, - PlannedSearch, -} -impl SearchMode { - pub(crate) fn as_str(self) -> &'static str { - match self { - Self::QuickFind => "quick_find", - Self::PlannedSearch => "planned_search", - } - } -} - -#[derive(Clone, Copy, Debug, ValueEnum)] -#[value(rename_all = "lower")] -pub(crate) enum PayloadLevel { - L0, - L1, - L2, -} -impl PayloadLevel { - pub(crate) fn as_str(self) -> &'static str { - match self { - Self::L0 => "l0", - Self::L1 => "l1", - Self::L2 => "l2", - } - } -} - -#[derive(Debug, Subcommand)] -#[command(rename_all = "kebab")] -pub(crate) enum DiagnosticsCommand { - /// Rebuild Qdrant from Postgres vectors through the admin API. - QdrantRebuild(AdminPostArgs), - /// Run raw admin search and include trace/result/source_ref data. - RawSearch(AdminSearchArgs), - /// List recent persisted search traces. - RecentTraces(RecentTracesArgs), - /// Read a bounded or full trace bundle. - TraceBundle(TraceBundleArgs), - /// Read note provenance, ingest decisions, outbox rows, and recent traces. - NoteProvenance(NoteProvenanceArgs), -} diff --git a/apps/elf-cli/src/args/commands.rs b/apps/elf-cli/src/args/commands.rs new file mode 100644 index 00000000..66fbbb88 --- /dev/null +++ b/apps/elf-cli/src/args/commands.rs @@ -0,0 +1,34 @@ +use clap::{Parser, Subcommand}; + +use crate::args::{ + AddNoteArgs, BackfillArgs, BenchmarkArgs, DiagnosticsArgs, SearchArgs, StatusArgs, +}; + +#[derive(Debug, Parser)] +#[command( + version = elf_cli::VERSION, + rename_all = "kebab", + styles = elf_cli::styles(), + about = "Local ELF workflow wrappers over the HTTP API and repo benchmark tasks." +)] +pub(crate) struct Cli { + #[command(subcommand)] + pub(crate) command: Commands, +} + +#[derive(Debug, Subcommand)] +#[command(rename_all = "kebab")] +pub(crate) enum Commands { + /// Add one deterministic note through POST /v2/notes/ingest. + AddNote(AddNoteArgs), + /// Create a search session through POST /v2/searches. + Search(SearchArgs), + /// Check local API process health. + Status(StatusArgs), + /// Run the checked-in resumable backfill benchmark workflow. + Backfill(BackfillArgs), + /// Run or render checked-in live baseline benchmark reports. + Benchmark(BenchmarkArgs), + /// Read production diagnostics through admin HTTP endpoints. + Diagnostics(DiagnosticsArgs), +} diff --git a/apps/elf-cli/src/args/common.rs b/apps/elf-cli/src/args/common.rs new file mode 100644 index 00000000..73a40dfb --- /dev/null +++ b/apps/elf-cli/src/args/common.rs @@ -0,0 +1,55 @@ +use clap::Args; + +use crate::args::constants::{ + DEFAULT_ADMIN_URL, DEFAULT_AGENT_ID, DEFAULT_API_URL, DEFAULT_PROJECT_ID, DEFAULT_READ_PROFILE, + DEFAULT_TENANT_ID, +}; + +#[derive(Debug, Args)] +pub(crate) struct PublicEndpointArgs { + /// Public ELF API base URL. + #[arg(long, env = "ELF_API_URL", default_value = DEFAULT_API_URL)] + pub(crate) api_url: String, + /// Optional bearer token for static-key auth. + #[arg(long, env = "ELF_USER_TOKEN")] + pub(crate) token: Option, +} + +#[derive(Debug, Args)] +pub(crate) struct AdminEndpointArgs { + /// Admin ELF API base URL. + #[arg(long, env = "ELF_ADMIN_URL", default_value = DEFAULT_ADMIN_URL)] + pub(crate) admin_url: String, + /// Optional admin bearer token for static-key auth. + #[arg(long, env = "ELF_ADMIN_TOKEN")] + pub(crate) admin_token: Option, +} + +#[derive(Clone, Debug, Args)] +pub(crate) struct ContextArgs { + /// Tenant id sent in X-ELF-Tenant-Id. + #[arg(long, env = "ELF_TENANT_ID", default_value = DEFAULT_TENANT_ID)] + pub(crate) tenant_id: String, + /// Project id sent in X-ELF-Project-Id. + #[arg(long, env = "ELF_PROJECT_ID", default_value = DEFAULT_PROJECT_ID)] + pub(crate) project_id: String, + /// Agent id sent in X-ELF-Agent-Id. + #[arg(long, env = "ELF_AGENT_ID", default_value = DEFAULT_AGENT_ID)] + pub(crate) agent_id: String, +} + +#[derive(Clone, Debug, Args)] +pub(crate) struct ReadContextArgs { + #[command(flatten)] + pub(crate) context: ContextArgs, + /// Read profile sent in X-ELF-Read-Profile. + #[arg(long, env = "ELF_READ_PROFILE", default_value = DEFAULT_READ_PROFILE)] + pub(crate) read_profile: String, +} + +#[derive(Debug, Args)] +pub(crate) struct OutputArgs { + /// Pretty-print the JSON output. + #[arg(long)] + pub(crate) pretty: bool, +} diff --git a/apps/elf-cli/src/args/constants.rs b/apps/elf-cli/src/args/constants.rs new file mode 100644 index 00000000..f7d0f8b5 --- /dev/null +++ b/apps/elf-cli/src/args/constants.rs @@ -0,0 +1,6 @@ +pub(in crate::args) const DEFAULT_API_URL: &str = "http://127.0.0.1:51892"; +pub(in crate::args) const DEFAULT_ADMIN_URL: &str = "http://127.0.0.1:51891"; +pub(in crate::args) const DEFAULT_TENANT_ID: &str = "local-tenant"; +pub(in crate::args) const DEFAULT_PROJECT_ID: &str = "local-project"; +pub(in crate::args) const DEFAULT_AGENT_ID: &str = "local-agent"; +pub(in crate::args) const DEFAULT_READ_PROFILE: &str = "private_only"; diff --git a/apps/elf-cli/src/args/diagnostics.rs b/apps/elf-cli/src/args/diagnostics.rs new file mode 100644 index 00000000..2023e553 --- /dev/null +++ b/apps/elf-cli/src/args/diagnostics.rs @@ -0,0 +1,82 @@ +use clap::{Args, Subcommand}; + +use crate::args::{AdminEndpointArgs, AdminSearchArgs, ContextArgs, OutputArgs}; + +#[derive(Debug, Args)] +pub(crate) struct DiagnosticsArgs { + #[command(subcommand)] + pub(crate) command: DiagnosticsCommand, +} + +#[derive(Debug, Args)] +pub(crate) struct AdminPostArgs { + #[command(flatten)] + pub(crate) endpoint: AdminEndpointArgs, + #[command(flatten)] + pub(crate) context: ContextArgs, + #[command(flatten)] + pub(crate) output: OutputArgs, +} + +#[derive(Debug, Args)] +pub(crate) struct RecentTracesArgs { + #[command(flatten)] + pub(crate) endpoint: AdminEndpointArgs, + #[command(flatten)] + pub(crate) context: ContextArgs, + #[command(flatten)] + pub(crate) output: OutputArgs, + /// Maximum trace headers to return. + #[arg(long)] + pub(crate) limit: Option, +} + +#[derive(Debug, Args)] +pub(crate) struct TraceBundleArgs { + #[command(flatten)] + pub(crate) endpoint: AdminEndpointArgs, + #[command(flatten)] + pub(crate) context: ContextArgs, + #[command(flatten)] + pub(crate) output: OutputArgs, + /// Trace id to load. + #[arg(long)] + pub(crate) trace_id: String, + /// Bundle mode: bounded or full. + #[arg(long, default_value = "bounded")] + pub(crate) mode: String, + /// Optional per-stage item cap. + #[arg(long)] + pub(crate) stage_items_limit: Option, + /// Optional replay candidate cap. + #[arg(long)] + pub(crate) candidates_limit: Option, +} + +#[derive(Debug, Args)] +pub(crate) struct NoteProvenanceArgs { + #[command(flatten)] + pub(crate) endpoint: AdminEndpointArgs, + #[command(flatten)] + pub(crate) context: ContextArgs, + #[command(flatten)] + pub(crate) output: OutputArgs, + /// Note id to inspect. + #[arg(long)] + pub(crate) note_id: String, +} + +#[derive(Debug, Subcommand)] +#[command(rename_all = "kebab")] +pub(crate) enum DiagnosticsCommand { + /// Rebuild Qdrant from Postgres vectors through the admin API. + QdrantRebuild(AdminPostArgs), + /// Run raw admin search and include trace/result/source_ref data. + RawSearch(AdminSearchArgs), + /// List recent persisted search traces. + RecentTraces(RecentTracesArgs), + /// Read a bounded or full trace bundle. + TraceBundle(TraceBundleArgs), + /// Read note provenance, ingest decisions, outbox rows, and recent traces. + NoteProvenance(NoteProvenanceArgs), +} diff --git a/apps/elf-cli/src/args/memory.rs b/apps/elf-cli/src/args/memory.rs new file mode 100644 index 00000000..5c5022a7 --- /dev/null +++ b/apps/elf-cli/src/args/memory.rs @@ -0,0 +1,72 @@ +use clap::Args; + +use crate::args::{ContextArgs, OutputArgs, PublicEndpointArgs}; + +#[derive(Debug, Args)] +pub(crate) struct AddNoteArgs { + #[command(flatten)] + pub(crate) endpoint: PublicEndpointArgs, + #[command(flatten)] + pub(crate) context: ContextArgs, + #[command(flatten)] + pub(crate) output: OutputArgs, + /// Scope applied to the note. + #[arg(long, default_value = "agent_private")] + pub(crate) scope: String, + /// Memory note type. + #[arg(long = "type", default_value = "fact")] + pub(crate) note_type: String, + /// Optional note key used by the update resolver. + #[arg(long)] + pub(crate) key: Option, + /// English note text. + #[arg(long)] + pub(crate) text: String, + /// Ranking importance value. + #[arg(long, default_value_t = 0.7)] + pub(crate) importance: f32, + /// Ranking confidence value. + #[arg(long, default_value_t = 0.9)] + pub(crate) confidence: f32, + /// Optional TTL override in days. + #[arg(long)] + pub(crate) ttl_days: Option, + /// Operator-visible source id copied into source_ref.ref.source_id. + #[arg(long)] + pub(crate) source_id: Option, + /// Full JSON object source_ref override. + #[arg(long)] + pub(crate) source_ref_json: Option, +} + +#[derive(Debug, Args)] +pub(crate) struct StatusArgs { + #[command(flatten)] + pub(crate) endpoint: PublicEndpointArgs, + #[command(flatten)] + pub(crate) output: OutputArgs, +} + +#[derive(Debug, Args)] +pub(crate) struct BackfillArgs { + #[command(flatten)] + pub(crate) output: OutputArgs, + /// Backfill corpus document count override. + #[arg(long)] + pub(crate) docs: Option, + /// Worker concurrency override for the backfill runner. + #[arg(long)] + pub(crate) worker_concurrency: Option, + /// Use the checked-in 10k operator profile task. + #[arg(long)] + pub(crate) ten_k: bool, + /// Use the guarded 100k operator profile task. + #[arg(long, conflicts_with = "ten_k")] + pub(crate) hundred_k: bool, + /// Set the required expensive-run guard for the 100k task. + #[arg(long)] + pub(crate) enable_expensive: bool, + /// Print the resolved task and environment without running it. + #[arg(long)] + pub(crate) dry_run: bool, +} diff --git a/apps/elf-cli/src/args/search.rs b/apps/elf-cli/src/args/search.rs new file mode 100644 index 00000000..1187829e --- /dev/null +++ b/apps/elf-cli/src/args/search.rs @@ -0,0 +1,91 @@ +use clap::{Args, ValueEnum}; + +use crate::args::{AdminEndpointArgs, OutputArgs, PublicEndpointArgs, ReadContextArgs}; + +#[derive(Debug, Args)] +pub(crate) struct SearchArgs { + #[command(flatten)] + pub(crate) endpoint: PublicEndpointArgs, + #[command(flatten)] + pub(crate) read_context: ReadContextArgs, + #[command(flatten)] + pub(crate) output: OutputArgs, + /// English query string. + #[arg(long)] + pub(crate) query: String, + /// Search mode to request from the service. + #[arg(long, value_enum, default_value_t = SearchMode::QuickFind)] + pub(crate) mode: SearchMode, + /// Number of final items to return. + #[arg(long)] + pub(crate) top_k: Option, + /// Candidate breadth before ranking. + #[arg(long)] + pub(crate) candidate_k: Option, + /// Payload level requested from the service. + #[arg(long, value_enum, default_value_t = PayloadLevel::L0)] + pub(crate) payload_level: PayloadLevel, + /// Optional search filter JSON object. + #[arg(long)] + pub(crate) filter_json: Option, +} + +#[derive(Debug, Args)] +pub(crate) struct AdminSearchArgs { + #[command(flatten)] + pub(crate) endpoint: AdminEndpointArgs, + #[command(flatten)] + pub(crate) read_context: ReadContextArgs, + #[command(flatten)] + pub(crate) output: OutputArgs, + /// English query string. + #[arg(long)] + pub(crate) query: String, + /// Search mode to request from the service. + #[arg(long, value_enum, default_value_t = SearchMode::QuickFind)] + pub(crate) mode: SearchMode, + /// Number of final items to return. + #[arg(long)] + pub(crate) top_k: Option, + /// Candidate breadth before ranking. + #[arg(long)] + pub(crate) candidate_k: Option, + /// Payload level requested from the service. + #[arg(long, value_enum, default_value_t = PayloadLevel::L2)] + pub(crate) payload_level: PayloadLevel, + /// Optional search filter JSON object. + #[arg(long)] + pub(crate) filter_json: Option, +} + +#[derive(Clone, Copy, Debug, ValueEnum)] +#[value(rename_all = "snake_case")] +pub(crate) enum SearchMode { + QuickFind, + PlannedSearch, +} +impl SearchMode { + pub(crate) fn as_str(self) -> &'static str { + match self { + Self::QuickFind => "quick_find", + Self::PlannedSearch => "planned_search", + } + } +} + +#[derive(Clone, Copy, Debug, ValueEnum)] +#[value(rename_all = "lower")] +pub(crate) enum PayloadLevel { + L0, + L1, + L2, +} +impl PayloadLevel { + pub(crate) fn as_str(self) -> &'static str { + match self { + Self::L0 => "l0", + Self::L1 => "l1", + Self::L2 => "l2", + } + } +}