From 74bc7e51e3bb9bdbd96fcbb95a962050e5d6e5c1 Mon Sep 17 00:00:00 2001 From: ebigunso Date: Mon, 21 Sep 2026 05:35:44 +0900 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Report=20stats=20projection=20gr?= =?UTF-8?q?aph=20reads=20accurately?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- docs/design/database/graph_schema_design.md | 2 ++ src/errors.rs | 16 +++++++--------- src/usecases/remember.rs | 21 +++++++++++++++++++-- src/usecases/stats_projection.rs | 10 +++++----- tests/write_planning_tests.rs | 2 +- 6 files changed, 35 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index bc3797e4..423b4476 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ if validation.iter().all(|candidate| candidate.status == CandidateValidationStat } ``` -`commit` revalidates the plan before writing. Graph-authoritative objects, links, provenance, lifecycle, and currentness are critical writes; vector indexing and retrieval-stat updates are repairable and are reported in `RememberOutcome`. +`commit` revalidates the plan before writing. Graph-authoritative objects, links, provenance, lifecycle, and currentness are critical writes; vector indexing and retrieval-stat updates are repairable and are reported in `RememberOutcome`. Failed graph reads feeding the stats projection, including endpoint hydration and currency lookup, report `StatsUpdateCause::GraphRead`; the nested `GraphQueryError` retains the underlying failure. For callers that want the standard write lifecycle in one call, `remember(RememberInput, RememberOptions)` composes `prepare`, `validate_plan`, and `commit` over the same graph-authoritative machinery. diff --git a/docs/design/database/graph_schema_design.md b/docs/design/database/graph_schema_design.md index aea1223c..650a42cb 100644 --- a/docs/design/database/graph_schema_design.md +++ b/docs/design/database/graph_schema_design.md @@ -100,6 +100,8 @@ The policy combination that includes suppressed memories while excluding superse Graph writes are critical. Vector and stats updates are repairable parts of the write outcome, with typed failures and affected IDs. A graph-only memory loses semantic recall until vector indexing is repaired; stale candidates are checked against graph existence and lifecycle before inclusion. Replaying an old plan does not re-index an interpreted memory that graph authority identifies as superseded. +A failed graph read feeding the stats projection is reported as `StatsUpdateCause::GraphRead` and retained in unhealthy stats as `RetrievalStatsHealthCause::GraphRead`. Both carry the underlying `GraphQueryError`, whether the read was endpoint hydration or currency lookup. + The library has no cross-store reconciliation or stats-rebuild operation. Stats health remains unhealthy after an internal failure; recovery requires a fresh store and caller-managed replay. Raw-source resolution and cross-store census operations belong to callers or operators. See the [schema cheat sheet](schema_cheat_sheet.md) for predicate tables and the [vector payload contract](vector_payload_design.md) for recall record fields. diff --git a/src/errors.rs b/src/errors.rs index d36c7dbd..f1eac546 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -224,8 +224,8 @@ pub enum VectorIndexingCause { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Error)] #[serde(tag = "cause", rename_all = "snake_case")] pub enum StatsUpdateCause { - #[error("stats endpoint hydration failed: {error}")] - EndpointHydration { error: GraphQueryError }, + #[error("graph read for stats projection failed: {error}")] + GraphRead { error: GraphQueryError }, #[error("stats edge write failed: {error}")] EdgeWrite { error: RetrievalStatsStoreError }, #[error("stats object-state write failed: {error}")] @@ -274,8 +274,8 @@ pub enum RetrievalStatsStoreError { pub enum RetrievalStatsHealthCause { #[error("stats store initialization failed: {error}")] StoreInitialization { error: RetrievalStatsStoreError }, - #[error("stats endpoint hydration failed: {error}")] - EndpointHydration { error: GraphQueryError }, + #[error("graph read for stats projection failed: {error}")] + GraphRead { error: GraphQueryError }, #[error("stats edge write failed: {error}")] EdgeWrite { error: RetrievalStatsStoreError }, #[error("stats object-state write failed: {error}")] @@ -291,11 +291,9 @@ pub enum RetrievalStatsHealthCause { impl StatsUpdateCause { pub(crate) fn health_cause(&self) -> Option { match self { - Self::EndpointHydration { error } => { - Some(RetrievalStatsHealthCause::EndpointHydration { - error: error.clone(), - }) - } + Self::GraphRead { error } => Some(RetrievalStatsHealthCause::GraphRead { + error: error.clone(), + }), Self::EdgeWrite { error } => Some(RetrievalStatsHealthCause::EdgeWrite { error: error.clone(), }), diff --git a/src/usecases/remember.rs b/src/usecases/remember.rs index 7c51d126..2941646a 100644 --- a/src/usecases/remember.rs +++ b/src/usecases/remember.rs @@ -419,13 +419,30 @@ mod tests { outcome .stats_update_status .failure + .as_ref() .unwrap() .causes .as_slice(), - [StatsUpdateCause::EndpointHydration { + [StatsUpdateCause::GraphRead { error: crate::errors::GraphQueryError::Selection { .. } }] )); + assert_eq!( + serde_json::to_value(&outcome.stats_update_status.failure.as_ref().unwrap().causes[0]) + .unwrap()["cause"], + "graph_read" + ); + let health_cause = stats.health().await.unwrap().last_error_cause.unwrap(); + assert!(matches!( + health_cause, + RetrievalStatsHealthCause::GraphRead { + error: crate::errors::GraphQueryError::Selection { .. } + } + )); + assert_eq!( + serde_json::to_value(&health_cause).unwrap()["operation"], + "graph_read" + ); assert!(outcome .repair_needed .iter() @@ -862,7 +879,7 @@ mod tests { assert!(matches!( failure.causes.as_slice(), [ - StatsUpdateCause::EndpointHydration { .. }, + StatsUpdateCause::GraphRead { .. }, StatsUpdateCause::EdgeWrite { .. } ] )); diff --git a/src/usecases/stats_projection.rs b/src/usecases/stats_projection.rs index 2717c2ca..617947b2 100644 --- a/src/usecases/stats_projection.rs +++ b/src/usecases/stats_projection.rs @@ -71,7 +71,7 @@ where let causes = if missing_endpoint_refs.is_empty() { Vec::new() } else { - vec![StatsUpdateCause::EndpointHydration { + vec![StatsUpdateCause::GraphRead { error: GraphQueryError::Hydration { detail: format_missing_endpoint_refs(&missing_endpoint_refs), }, @@ -84,7 +84,7 @@ where } Err(error) => ( objects.to_vec(), - vec![StatsUpdateCause::EndpointHydration { error }], + vec![StatsUpdateCause::GraphRead { error }], ), } }; @@ -138,7 +138,7 @@ where .await { Ok(ids) => ids.into_iter().collect(), - Err(error) => return vec![StatsUpdateCause::EndpointHydration { error }], + Err(error) => return vec![StatsUpdateCause::GraphRead { error }], } }; let states = retrieval_stats_object_states(objects, &superseded); @@ -509,13 +509,13 @@ mod tests { ); assert!(matches!( failure.causes.as_slice(), - [StatsUpdateCause::EndpointHydration { + [StatsUpdateCause::GraphRead { error: GraphQueryError::Hydration { .. }, }] )); assert!(matches!( stats_store.marked_causes.lock().unwrap().as_slice(), - [RetrievalStatsHealthCause::EndpointHydration { + [RetrievalStatsHealthCause::GraphRead { error: GraphQueryError::Hydration { .. }, }] )); diff --git a/tests/write_planning_tests.rs b/tests/write_planning_tests.rs index 3b370b00..fce75563 100644 --- a/tests/write_planning_tests.rs +++ b/tests/write_planning_tests.rs @@ -317,7 +317,7 @@ async fn authority_split_outcome_fields_are_coherent_on_healthy_commit() { assert!(matches!( failure.causes.as_slice(), [ - StatsUpdateCause::EndpointHydration { .. } + StatsUpdateCause::GraphRead { .. } | StatsUpdateCause::EdgeWrite { .. } | StatsUpdateCause::ObjectStateWrite { .. } | StatsUpdateCause::HealthCheck { .. }