use super::op_error::ContractOpError;
use crate::contracts::args::slot_for_payload;
use saya_store::{KnowledgeItem, KnowledgeItemStore, SqliteStateStore};
use saya_types::{
ClaimId, ClaimOrigin, ClaimPayload, DatabaseObjectRef, KnowledgeSlot, KnowledgeState,
SchemaBinding, SchemaFingerprint,
};
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum RememberOutcome {
Stored { id: ClaimId },
Duplicate { id: ClaimId, state: KnowledgeState },
Replaced { id: ClaimId, previous: String },
}
pub(crate) async fn remember(
store: &SqliteStateStore,
object: &DatabaseObjectRef,
payload: &ClaimPayload,
fingerprint: SchemaFingerprint,
) -> Result<RememberOutcome, ContractOpError> {
let slot = slot_for_payload(payload).ok_or(ContractOpError::Invalid)?;
if let Some(existing) = find_existing(store, object, &slot, payload).await? {
if existing.state != KnowledgeState::Dismissed {
if is_reason_revision(payload, &existing.value) {
let id = store_active_item(store, object, &slot, payload, fingerprint).await?;
return Ok(RememberOutcome::Stored { id });
}
if slot.cardinality().is_single() {
let (_old_col, previous) =
crate::agent::recall_context::claim_value(&existing.value);
let (_new_col, new_val) = crate::agent::recall_context::claim_value(payload);
if previous != new_val {
let id = store_active_item(store, object, &slot, payload, fingerprint).await?;
return Ok(RememberOutcome::Replaced { id, previous });
}
}
}
let id = ClaimId::parse(&existing.id).map_err(|_| ContractOpError::Invalid)?;
return Ok(RememberOutcome::Duplicate {
id,
state: existing.state,
});
}
let id = store_active_item(store, object, &slot, payload, fingerprint).await?;
Ok(RememberOutcome::Stored { id })
}
async fn store_active_item(
store: &SqliteStateStore,
object: &DatabaseObjectRef,
slot: &KnowledgeSlot,
payload: &ClaimPayload,
fingerprint: SchemaFingerprint,
) -> Result<ClaimId, ContractOpError> {
let binding = SchemaBinding::derive(slot, payload).ok_or(ContractOpError::Invalid)?;
let binding_json = serde_json::to_string(&binding).map_err(|_| ContractOpError::Unavailable)?;
store
.put_knowledge_item(saya_store::KnowledgeItemRequest {
object: object.clone(),
slot: slot.clone(),
value: payload.clone(),
source: ClaimOrigin::UserExplicit,
state: KnowledgeState::Active,
schema_binding_json: binding_json,
fingerprint,
})
.await?;
item_id_for(object, slot, payload)
}
fn is_reason_revision(new: &ClaimPayload, old: &ClaimPayload) -> bool {
use saya_types::ClaimPayload as P;
match (new, old) {
(
P::DefaultTimeColumn {
column: c1,
reason: r1,
..
},
P::DefaultTimeColumn {
column: c2,
reason: r2,
..
},
) => c1 == c2 && r1.is_some() && r1 != r2,
(
P::TableGrain {
description: d1,
reason: r1,
..
},
P::TableGrain {
description: d2,
reason: r2,
..
},
) => d1 == d2 && r1.is_some() && r1 != r2,
(
P::ColumnRole {
column: c1,
role: rl1,
reason: rr1,
..
},
P::ColumnRole {
column: c2,
role: rl2,
reason: rr2,
..
},
) => c1 == c2 && rl1 == rl2 && rr1.is_some() && rr1 != rr2,
_ => false,
}
}
async fn find_existing(
store: &SqliteStateStore,
object: &DatabaseObjectRef,
slot: &KnowledgeSlot,
payload: &ClaimPayload,
) -> Result<Option<KnowledgeItem>, ContractOpError> {
let serialized = serde_json::to_string(payload).map_err(|_| ContractOpError::Unavailable)?;
let id = saya_store::knowledge_item_id_for(object, slot, &serialized);
Ok(store.get_knowledge_item(&id).await?)
}
fn item_id_for(
object: &DatabaseObjectRef,
slot: &KnowledgeSlot,
payload: &ClaimPayload,
) -> Result<ClaimId, ContractOpError> {
let serialized = serde_json::to_string(payload).map_err(|_| ContractOpError::Unavailable)?;
let id = saya_store::knowledge_item_id_for(object, slot, &serialized);
ClaimId::parse(&id).map_err(|_| ContractOpError::Invalid)
}