use std::str::FromStr;
use serde_json::{Value, json};
use super::PromotionRefusal;
use crate::db::{DbConnection, DbError, DbOperation, StoredMemory};
use crate::models::MemoryKind;
use crate::models::memory::{
canonicalize_typed_memory_fields_json, canonicalize_typed_memory_fields_json_with_redactor,
};
#[derive(Default)]
pub(super) struct PromotionPayload {
typed_fields: Option<String>,
family_evidence: Option<Value>,
}
fn invalid_payload() -> DbError {
DbError::MalformedRow {
operation: DbOperation::Query,
message: "Could not verify structured global-promotion data".to_owned(),
}
}
fn canonical_fields(kind: &str, raw: Option<&str>) -> crate::db::Result<Option<String>> {
let kind = MemoryKind::from_str(kind).map_err(|_| invalid_payload())?;
raw.map(|raw| canonicalize_typed_memory_fields_json(&kind, raw).map_err(|_| invalid_payload()))
.transpose()
}
impl PromotionPayload {
pub(super) fn capture(
db: &DbConnection,
memory: &StoredMemory,
) -> Result<(Self, Option<PromotionRefusal>), String> {
let logical_id = db
.get_memory_logical_id(&memory.id)
.map_err(|_| "Could not verify promotion logical identity".to_owned())?
.unwrap_or_else(|| memory.id.clone());
let membership = db
.get_attempt_family_membership_snapshot(&memory.workspace_id, &logical_id)
.map_err(|_| "Could not verify promotion attempt-family evidence".to_owned())?;
if !membership.is_promotion_eligible() {
let posture = membership
.promotion_posture()
.ok_or_else(|| "Promotion attempt-family posture is missing".to_owned())?;
return Ok((
Self::default(),
Some(PromotionRefusal::AttemptFamily { posture }),
));
}
let family_evidence = membership.families.first().map(|family| {
let state = family.multiplicity();
json!({
"familyAlias": crate::models::public_attempt_family_alias(&family.family_id),
"declaredSize": state.declared_size,
"recordedSlots": state.recorded_slots,
"selectedCount": state.selected_count,
"rejectedCount": state.rejected_count,
"promotionPosture": state.promotion_posture().as_str(),
})
});
let raw = db
.get_memory_typed_fields_json(&memory.id)
.map_err(|_| "Could not read promotion typed fields".to_owned())?;
let kind = MemoryKind::from_str(&memory.kind)
.map_err(|_| "Invalid promotion memory kind".to_owned())?;
let mut reasons = Vec::new();
let typed_fields = raw
.as_deref()
.map(|raw| {
canonicalize_typed_memory_fields_json_with_redactor(&kind, raw, |text| {
let redaction = crate::policy::redact_secret_like_content(text);
if redaction.redacted {
reasons.extend(redaction.redacted_reasons);
}
text.to_owned()
})
})
.transpose()
.map_err(|_| "Invalid promotion typed fields; source withheld".to_owned())?;
if !reasons.is_empty() {
reasons.sort_unstable();
reasons.dedup();
return Ok((
Self::default(),
Some(PromotionRefusal::RedactionRefused { reasons }),
));
}
Ok((
Self {
typed_fields,
family_evidence,
},
None,
))
}
pub(super) fn matches_fields(&self, kind: &str, raw: Option<&str>) -> crate::db::Result<bool> {
Ok(self.typed_fields == canonical_fields(kind, raw)?)
}
pub(super) fn apply(&self, db: &DbConnection, memory_id: &str) -> crate::db::Result<()> {
if let Some(fields) = &self.typed_fields
&& !db.set_memory_typed_fields_json(memory_id, Some(fields))?
{
return Err(DbError::MalformedRow {
operation: DbOperation::Execute,
message: "Promoted memory did not retain its structured payload".to_owned(),
});
}
Ok(())
}
pub(super) fn audit_evidence(&self) -> Value {
json!({
"schema": "ee.global_promotion.source_payload.v1",
"typedFieldsHash": self.typed_fields.as_ref().map(|fields| {
format!("blake3:{}", blake3::hash(fields.as_bytes()).to_hex())
}),
"attemptFamily": self.family_evidence,
})
}
}
#[cfg(test)]
#[path = "global_promotion_payload_tests.rs"]
mod tests;
#[cfg(test)]
mod field_policy_tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::*;
use crate::db::{CreateMemoryInput, CreateWorkspaceInput};
#[test]
fn decoded_multiline_and_list_fields_are_withheld_before_publication() {
let root = tempfile::tempdir().unwrap();
let db = DbConnection::open_file(&root.path().join("fields.db")).unwrap();
db.migrate().unwrap();
let workspace = "wsp_00000000000000000000000189";
let id = "mem_00000000000000000000000189";
db.insert_workspace(
workspace,
&CreateWorkspaceInput {
path: root.path().display().to_string(),
name: None,
},
)
.unwrap();
db.insert_memory(
id,
&CreateMemoryInput {
workspace_id: workspace.to_owned(),
level: "semantic".to_owned(),
kind: "decision".to_owned(),
content: "Use the reviewed design.".to_owned(),
workflow_id: None,
confidence: 0.9,
utility: 0.5,
importance: 0.5,
provenance_uri: None,
trust_class: "human_explicit".to_owned(),
trust_subclass: None,
tags: Vec::new(),
valid_from: None,
valid_to: None,
},
)
.unwrap();
let key = "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7\n-----END PRIVATE KEY-----";
for fields in [
json!({"rationale": key}),
json!({"options": ["public", key]}),
] {
db.set_memory_typed_fields_json(id, Some(&fields.to_string()))
.unwrap();
let memory = db.get_memory(id).unwrap().unwrap();
let (payload, refusal) = PromotionPayload::capture(&db, &memory).unwrap();
assert!(matches!(
refusal,
Some(PromotionRefusal::RedactionRefused { .. })
));
assert!(payload.typed_fields.is_none());
assert!(payload.audit_evidence()["typedFieldsHash"].is_null());
assert_eq!(db.count_table_rows("search_index_jobs").unwrap(), 0);
}
}
}