use serde::{Deserialize, Serialize};
pub const PAGE_PENDING_REBUILD_MESSAGE: &str = "evidence updated, prose rebuild pending";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Page {
pub id: String,
pub title: String,
pub summary: Option<String>,
pub content: String,
pub entity_id: Option<String>,
#[serde(default, alias = "domain")]
pub space: Option<String>,
pub source_memory_ids: Vec<String>,
pub version: i64,
pub status: String,
pub created_at: String,
pub last_compiled: String,
pub last_modified: String,
pub sources_updated_count: i64,
pub stale_reason: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pending_rebuild: Option<String>,
pub user_edited: bool,
#[serde(default, skip_serializing_if = "is_zero_f32")]
pub relevance_score: f32,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_edited_by: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_edited_at: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_delta_summary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub changelog: Option<String>,
#[serde(default)]
pub workspace: Option<String>,
#[serde(default = "default_creation_kind")]
pub creation_kind: String,
#[serde(default = "default_review_status")]
pub review_status: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub citations: Vec<PageCitation>,
}
fn default_creation_kind() -> String {
"distilled".to_string()
}
fn default_review_status() -> String {
"confirmed".to_string()
}
fn is_zero_f32(v: &f32) -> bool {
*v == 0.0
}
pub fn pending_rebuild_for_stale_reason(stale_reason: Option<&str>) -> Option<String> {
match stale_reason {
Some("source_updated") => Some(PAGE_PENDING_REBUILD_MESSAGE.to_string()),
_ => None,
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PageEvidence {
pub page_id: String,
pub source_kind: String, pub locator: Option<String>,
pub title: Option<String>,
pub linked_at: i64,
pub link_reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageCitation {
pub occurrence: u32,
pub marker: u32,
pub source_kind: String,
pub locator: String,
pub score: f64,
pub status: String,
#[serde(default = "default_citation_scope")]
pub scope: String,
}
fn default_citation_scope() -> String {
"sentence".to_string()
}
#[cfg(test)]
mod citation_tests {
use super::*;
#[test]
fn old_page_json_deserializes_with_empty_citations() {
let json = r#"{"id":"p1","title":"T","content":"body","source_memory_ids":[],
"version":1,"status":"active","created_at":"x","last_compiled":"x",
"last_modified":"x","sources_updated_count":0,"user_edited":false}"#;
let p: Page = serde_json::from_str(json).unwrap();
assert!(p.citations.is_empty());
}
#[test]
fn citation_roundtrip() {
let c = PageCitation {
occurrence: 2,
marker: 1,
source_kind: "memory".into(),
locator: "mem_a".into(),
score: 0.31,
status: "unverified".into(),
scope: "paragraph".into(),
};
let s = serde_json::to_string(&vec![c]).unwrap();
let back: Vec<PageCitation> = serde_json::from_str(&s).unwrap();
assert_eq!(back[0].occurrence, 2);
assert_eq!(back[0].status, "unverified");
assert_eq!(back[0].scope, "paragraph");
let legacy: PageCitation = serde_json::from_str(
r#"{"occurrence":1,"marker":1,"source_kind":"memory","locator":"m","score":1.0,"status":"verified"}"#,
)
.unwrap();
assert_eq!(legacy.scope, "sentence");
}
}