Skip to main content

khive_runtime/
curation.rs

1// Licensed under the Apache License, Version 2.0.
2
3//! Curation operations: entity update/merge and edge-list filter type.
4
5use std::any::Any;
6use std::collections::{HashMap, HashSet, VecDeque};
7
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use uuid::Uuid;
11
12use khive_db::{pool::RuntimeWriteOperation, SqliteError};
13use khive_storage::note::{FilterOp, Note, NoteFilter, PropertyFilter};
14use khive_storage::types::{EdgeFilter, PageRequest, SqlValue, TextDocument};
15use khive_storage::{AtomicUnitOp, EdgeRelation, Entity, SqlStatement, SubstrateKind};
16use khive_types::{Details, EdgeEndpointRule, EventKind, KhiveError};
17use rusqlite::OptionalExtension;
18
19use crate::error::{RuntimeError, RuntimeResult};
20use crate::event_store_guard::EventAttribution;
21use crate::operations::{base_entity_rule_allows, canonical_edge_endpoints, endpoint_matches};
22use crate::runtime::{KhiveRuntime, NamespaceToken};
23
24mod embedding_text;
25mod properties;
26
27pub use embedding_text::{
28    entity_embedding_text, entity_fts_document, note_embedding_text, note_fts_document,
29};
30pub(crate) use embedding_text::{note_embedding_text_ref, note_fts_scalars};
31#[cfg(test)]
32use properties::merge_json;
33pub(crate) use properties::{
34    count_new_property_keys, kind_owned_properties, merge_properties, merge_string_field,
35    owner_established_property_named_in, preserve_owner_established_properties, union_tags,
36    OWNER_ESTABLISHED_PROPERTIES,
37};
38use properties::{
39    message_is_quarantined, note_update_values_equal, preserve_property_keys,
40    reject_pack_managed_schedule_mutation,
41};
42
43mod note_curation;
44mod note_merge;
45mod note_merge_guard;
46pub(crate) mod note_reindex;
47mod outbound_messages;
48
49pub use note_merge_guard::{GuardedNoteMerge, MergeAssertion, NoteMergeGuard};
50
51/// Restrict an outbox scan before its SQL page bound is applied. A held row
52/// for another or unconfigured channel must not consume a channel's page.
53enum OutboxSlugFilter<'a> {
54    Any,
55    Exact(&'a str),
56    Missing,
57}
58
59/// Test-only pause point at the read/write boundary of a guarded
60/// read-modify-write, so a race between two concurrent callers of the same
61/// PRODUCTION entry point (not the underlying store primitive) can be
62/// reproduced deterministically instead of relying on scheduler luck or
63/// sleeps. A no-op unless the calling task runs inside
64/// `AFTER_READ_BARRIER.scope(...)`; production code never establishes that
65/// scope, so `pause_after_read` costs nothing outside these regression
66/// tests, and it does not exist at all in non-test builds.
67#[cfg(test)]
68#[path = "curation/race_seam_tests.rs"]
69pub(crate) mod race_seam;
70
71mod types_and_guards;
72
73use types_and_guards::{
74    append_merge_event_in_transaction, edge_row_budget_bytes, map_merge_entity_storage_error,
75    map_merge_note_storage_error, EmbeddingModelPlan, EntityMergeRefusal, EntityMergeValidation,
76    MergeEventContext, MergeSqlError, MergeTxBudget,
77};
78pub use types_and_guards::{
79    entity_merge_guard_compared_values, entity_merge_guard_error,
80    entity_merge_guard_refusal_message, validate_entity_merge_floor, ContentMergeStrategy,
81    EdgeListFilter, EdgePatch, EntityDedupMergePolicy, EntityMergeGuard, EntityPatch,
82    MergeEdgeConflictPreimage, MergeEdgePreimage, MergeSummary, MergeTxBudgetReport, MergeTxLimits,
83    NotePatch, NoteUpdatePolicy,
84};
85pub(crate) use types_and_guards::{
86    normalize_note_update_tags, stale_edge_snapshot_error, stale_entity_snapshot_error,
87    stale_note_snapshot_error,
88};
89#[cfg(test)]
90use types_and_guards::{MERGE_TX_MAX_BYTES, MERGE_TX_MAX_ROWS};
91
92mod merge_sql;
93use merge_sql::{merge_entity_sql, merge_note_sql};
94
95mod entity_curation;
96mod merge_edges;
97
98use merge_edges::{
99    collect_merge_drop_incident_edge_preimages, delete_merge_drop_edges, edge_row_preimage,
100    merge_rewire_endpoint_contract_allows, resolve_merge_edge_endpoint_budgeted, EdgeRow,
101};
102
103// ---------------------------------------------------------------------------
104// Implementation
105// ---------------------------------------------------------------------------
106
107/// SQL-bind–ready scalars derived from [`note_fts_document`].
108///
109/// Used by `merge_note_sql` to guarantee that the raw SQL FTS INSERT stores
110/// exactly what [`Fts5TextSearch::upsert_document`] would write, preventing
111/// null/empty-string divergence on the `title` column for nameless notes.
112pub(crate) struct NoteFtsScalars {
113    /// Granular note kind used by the indexed corpus classifier.
114    pub record_kind: String,
115    /// Empty string when `note.name` is `None` — matches the `unwrap_or("")` in
116    /// `Fts5TextSearch::upsert_document`.
117    pub title: String,
118    pub body: String,
119    /// Always the JSON array `"[]"`.
120    pub tags: String,
121    /// Serialised `note.properties`, or `None` when properties are absent.
122    pub metadata: Option<String>,
123    /// `note.updated_at` converted to `DateTime<Utc>` timestamp_micros.
124    pub updated_at_micros: i64,
125}
126
127// ---------------------------------------------------------------------------
128// INLINE TEST JUSTIFICATION: tests here exercise patch/merge helpers and the
129// update_note/update_entity paths that share private merge_properties logic.
130// Moving them to tests/ would require pub-exporting merge_properties, which is
131// an internal invariant not suitable for the public API surface. Broad
132// behavioral curation tests live in tests/integration.rs.
133// ---------------------------------------------------------------------------
134
135#[cfg(test)]
136mod merge_reservation_tests;
137
138#[cfg(test)]
139#[path = "curation_tests.rs"]
140mod tests;