use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use crate::ast::{Json, Map};
pub const PROTECTED_SYSTEM_FIELDS: &[&str] = &[
"version",
"created_at",
"updated_at",
"created_tx",
"updated_tx",
"state",
"origin",
"space_seq",
];
wire_enum! {
#[derive(Default)]
pub enum ElementKind {
#[default]
Concept = "concept",
Proposition = "proposition",
Assertion = "assertion",
Evidence = "evidence",
Activity = "activity",
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct ElementEnvelope {
pub id: String,
pub kind: ElementKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub space_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub governance: Option<GovernanceState>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub retention: Option<Retention>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub facets: BTreeMap<String, Map<String, Json>>,
#[serde(default, rename = "_system", skip_serializing_if = "Option::is_none")]
pub system: Option<SystemState>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct GovernanceState {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub classification: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub policy_ref: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub authority_class: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub authority_lineage: Vec<String>,
#[serde(flatten, default, skip_serializing_if = "Map::is_empty")]
pub extensions: Map<String, Json>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct Retention {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub retention_class: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub legal_hold: Option<bool>,
}
pub const AUTHORITY_CLASSES: &[&str] = &["descriptive", "advisory", "behavioral", "executable"];
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
pub struct PlaneVersions {
#[serde(default)]
pub attributes: u64,
#[serde(default)]
pub structural: u64,
#[serde(default)]
pub retention: u64,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub facets: BTreeMap<String, u64>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct SystemState {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub input_references: Vec<Json>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input_versions: Option<Map<String, Json>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_versions: Option<Map<String, Json>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dependency_validity: Option<Json>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub plane_versions: Option<PlaneVersions>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub created_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub updated_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub created_tx: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub updated_tx: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub state: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub space_seq: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub origin: Option<Origin>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct Origin {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub principal_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub channel: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub import_id: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct Concept {
#[serde(flatten)]
pub envelope: ElementEnvelope,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub schema_ref: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub key: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub canonical_id: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub aliases: Vec<String>,
#[serde(default, skip_serializing_if = "Map::is_empty")]
pub attributes: Map<String, Json>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct Proposition {
#[serde(flatten)]
pub envelope: ElementEnvelope,
pub subject: Json,
pub predicate_ref: String,
pub object: Json,
}
wire_enum! {
pub enum Stance {
Support = "support",
Reject = "reject",
Uncertain = "uncertain",
}
}
wire_enum! {
pub enum AssertionMode {
Observed = "observed",
Stated = "stated",
Inferred = "inferred",
Predicted = "predicted",
Hypothetical = "hypothetical",
Imported = "imported",
}
}
wire_enum! {
pub enum AssertionStatus {
Active = "active",
Retracted = "retracted",
Superseded = "superseded",
Expired = "expired",
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct ValidTime {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub from: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub until: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct EvidenceRef {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub role: Option<String>,
}
impl EvidenceRef {
pub fn new(id: impl Into<String>, role: impl Into<String>) -> Self {
Self {
id: id.into(),
role: Some(role.into()),
}
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct Assertion {
#[serde(flatten)]
pub envelope: ElementEnvelope,
pub proposition: Json,
pub asserted_by: Json,
pub stance: Option<Stance>,
pub mode: Option<AssertionMode>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub confidence: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub asserted_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub valid_time: Option<ValidTime>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub evidence: Vec<EvidenceRef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub context_refs: Vec<Json>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lifecycle: Option<AssertionLifecycle>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct AssertionLifecycle {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<AssertionStatus>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub supersedes: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub superseded_by: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub retracted_at: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct EvidencePayload {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mode: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub inline: Option<Json>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content_ref: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct Evidence {
#[serde(flatten)]
pub envelope: ElementEnvelope,
pub evidence_class: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub payload: Option<EvidencePayload>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content_digest: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub media_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub observed_at: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub source: Vec<Json>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub generated_by: Option<Json>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lifecycle: Option<EvidenceLifecycle>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct EvidenceLifecycle {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub corrects: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub corrected_by: Vec<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct Activity {
#[serde(flatten)]
pub envelope: ElementEnvelope,
pub activity_class: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub started_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ended_at: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub inputs: Vec<Json>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub outputs: Vec<Json>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub associated_actors: Vec<Json>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parameters_digest: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
}
wire_enum! {
#[derive(Default)]
pub enum BeliefStatus {
Accepted = "accepted",
Rejected = "rejected",
Contested = "contested",
Uncertain = "uncertain",
#[default]
Insufficient = "insufficient",
}
}
impl BeliefStatus {
pub fn is_decided(&self) -> bool {
matches!(self, BeliefStatus::Accepted | BeliefStatus::Rejected)
}
}
pub const EVIDENCE_CLASSES: &[&str] = &[
"observation",
"user_statement",
"agent_statement",
"tool_result",
"measurement",
"message",
"document",
"web_resource",
"external_assertion",
"human_feedback",
"derived_result",
"outcome",
];
pub const ACTIVITY_CLASSES: &[&str] = &[
"extraction",
"tool_execution",
"human_review",
"inference",
"summarization",
"semantic_consolidation",
"procedural_consolidation",
"skill_compilation",
"import",
"schema_migration",
"entity_merge",
"experience_formation",
"belief_revision",
];
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct Projection {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub basis: Option<ProjectionBasis>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub candidate_status: Option<BeliefStatus>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub slot_status: Option<BeliefStatus>,
#[serde(default)]
pub conflict_refs: Vec<String>,
#[serde(default)]
pub conflict_reasons: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub proposition_id: Option<String>,
pub status: BeliefStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub support: Option<ProjectionSide>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub opposition: Option<ProjectionSide>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub uncertainty: Option<ProjectionUncertainty>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub temporal: Option<ProjectionTemporal>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub policy: Option<crate::request::PolicyIdentity>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub explanation: Option<Json>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct ProjectionSide {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub score: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub score_semantics: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub assertion_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub root_groups: Vec<Json>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct ProjectionUncertainty {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub level: Option<Json>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub reasons: Vec<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct ProjectionTemporal {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub valid_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub as_of_seq: Option<u64>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct ProjectionBasis {
pub space_id: String,
pub snapshot_seq: u64,
pub schema_environment_version: u64,
pub identity_version: u64,
pub policy: ProjectionPolicyVersion,
pub trust_version: String,
pub authorization_view: String,
pub context_refs: Vec<String>,
pub purpose: String,
pub risk: String,
pub valid_at: String,
pub next_invalid_at: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct ProjectionPolicyVersion {
pub id: String,
pub version: String,
}
pub const SCORE_SEMANTICS: &[&str] = &[
"ordinal_strength",
"normalized_support",
"calibrated_probability",
"log_odds",
"implementation_specific",
];
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct ChangeEnvelope {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub control_changes: Vec<ControlChange>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub coverage: Option<ChangeCoverage>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kip: Option<String>,
pub space_id: String,
pub space_seq: u64,
pub tx_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub committed_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transaction_class: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub schema_environment_version: Option<u64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub changes: Vec<ChangeEntry>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extensions: Option<Map<String, Json>>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct ControlChange {
pub kind: String,
pub version: String,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct ChangeCoverage {
pub through_seq: u64,
pub authorization_view: String,
pub complete: bool,
}
pub const CHANGE_TRANSITION_EXTENSION: &str = "anda/transition";
impl ChangeEnvelope {
pub fn transition_detail(snapshot_seq: u64, status: &str) -> Map<String, Json> {
let mut detail = Map::new();
detail.insert("snapshot_seq".to_string(), Json::from(snapshot_seq));
detail.insert("status".to_string(), Json::from(status));
detail
}
pub fn dedup_key(&self) -> (&str, u64, &str) {
(&self.space_id, self.space_seq, &self.tx_id)
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct ChangeEntry {
pub op: ChangeOp,
pub kind: ElementKind,
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub schema_ref: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub old_version: Option<u64>,
pub new_version: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub state: Option<ChangeState>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub refs: Option<ChangeRefs>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub touched: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub planes: Option<PlaneVersions>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extensions: Option<Map<String, Json>>,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum ChangeOp {
Create,
Update,
Lifecycle,
Retention,
Merge,
Purge,
PayloadPurge,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
pub struct ChangeState {
pub from: String,
pub to: String,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
pub struct ChangeRefs {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub proposition: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subject: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub predicate_ref: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub merged_into: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct Capabilities {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub profiles: Vec<crate::conformance::ConformanceProfile>,
#[serde(default, skip_serializing_if = "Map::is_empty")]
pub supported: Map<String, Json>,
#[serde(default, skip_serializing_if = "Map::is_empty")]
pub available: Map<String, Json>,
#[serde(default, skip_serializing_if = "Map::is_empty")]
pub limits: Map<String, Json>,
#[serde(flatten, default, skip_serializing_if = "Map::is_empty")]
pub extensions: Map<String, Json>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_wire_vocabulary_has_exactly_one_spelling_per_value() {
macro_rules! check {
($ty:ty) => {{
let label = stringify!($ty);
let all = <$ty>::ALL;
let names = <$ty>::NAMES;
assert_eq!(all.len(), names.len(), "{label}: ALL and NAMES disagree");
assert!(!all.is_empty(), "{label}: an empty vocabulary");
for (value, name) in all.iter().zip(names) {
assert_eq!(value.as_str(), *name, "{label}: as_str");
assert_eq!(&value.to_string(), name, "{label}: Display");
assert_eq!(
serde_json::to_value(value).expect("serializes"),
Json::String((*name).to_string()),
"{label}: Serialize"
);
assert_eq!(
&name.parse::<$ty>().expect("FromStr takes its own name"),
value,
"{label}: FromStr"
);
assert_eq!(
&serde_json::from_value::<$ty>(Json::String((*name).to_string()))
.expect("Deserialize takes its own name"),
value,
"{label}: Deserialize"
);
assert_eq!(<$ty>::from_wire(name), Some(*value), "{label}: from_wire");
}
assert!(
"kip/not-a-value".parse::<$ty>().is_err(),
"{label}: FromStr"
);
assert!(
serde_json::from_str::<$ty>(r#"{"kip/not-a-value":null}"#).is_err(),
"{label}: a wire vocabulary is a string, never a tagged map"
);
let unique: BTreeMap<&str, ()> = names.iter().map(|n| (*n, ())).collect();
assert_eq!(unique.len(), names.len(), "{label}: a repeated spelling");
}};
}
check!(ElementKind);
check!(Stance);
check!(AssertionMode);
check!(AssertionStatus);
check!(BeliefStatus);
check!(crate::error::ErrorCategory);
check!(crate::error::RetryClass);
check!(crate::request::SearchMode);
check!(crate::conformance::ConformanceProfile);
}
#[test]
fn vocabularies_use_their_wire_spellings() {
assert_eq!(
serde_json::to_string(&Stance::Support).unwrap(),
r#""support""#
);
assert_eq!(
serde_json::to_string(&AssertionMode::Hypothetical).unwrap(),
r#""hypothetical""#
);
assert_eq!(
serde_json::to_string(&BeliefStatus::Insufficient).unwrap(),
r#""insufficient""#
);
assert_eq!(
serde_json::to_string(&ElementKind::Proposition).unwrap(),
r#""proposition""#
);
}
#[test]
fn an_unfilled_projection_is_unknown_and_not_a_verdict() {
assert_eq!(Projection::default().status, BeliefStatus::Insufficient);
assert_eq!(
serde_json::to_value(Projection::default()).unwrap(),
serde_json::json!({ "status": "insufficient", "conflict_refs": [], "conflict_reasons": [] })
);
assert_eq!(Projection::default().proposition_id, None);
}
#[test]
fn a_projection_reports_support_and_opposition_separately() {
let projection = Projection {
proposition_id: Some("P-1".into()),
status: BeliefStatus::Contested,
support: Some(ProjectionSide {
score: Some(0.8),
score_semantics: Some("ordinal_strength".into()),
assertion_ids: vec!["A-1".into()],
root_groups: vec![serde_json::json!({ "roots": ["E-1"] })],
}),
opposition: Some(ProjectionSide {
score: Some(0.7),
score_semantics: Some("ordinal_strength".into()),
assertion_ids: vec!["A-2".into()],
root_groups: Vec::new(),
}),
uncertainty: Some(ProjectionUncertainty {
level: Some(serde_json::json!("high")),
reasons: vec!["conflicting independent roots".into()],
}),
temporal: Some(ProjectionTemporal {
valid_at: Some("2026-01-01T00:00:00.000Z".into()),
as_of_seq: Some(1500),
}),
policy: Some(crate::request::PolicyIdentity::new("kip:policy:baseline")),
explanation: None,
..Default::default()
};
let json = serde_json::to_value(&projection).unwrap();
assert_eq!(json["status"], "contested");
assert_eq!(json["support"]["score"], 0.8);
assert_eq!(json["opposition"]["score"], 0.7);
assert_eq!(json["support"]["score_semantics"], "ordinal_strength");
assert_eq!(json["temporal"]["as_of_seq"], 1500);
assert_eq!(
serde_json::from_value::<Projection>(json).unwrap(),
projection
);
}
#[test]
fn a_change_envelope_dedupes_on_the_key_the_spec_names() {
let mut extensions = Map::new();
extensions.insert(
CHANGE_TRANSITION_EXTENSION.to_string(),
Json::Object(ChangeEnvelope::transition_detail(1500, "committed")),
);
let envelope = ChangeEnvelope {
control_changes: vec![],
coverage: None,
kip: Some("2.0".into()),
space_id: "space-1".into(),
space_seq: 1501,
tx_id: "tx-900".into(),
committed_at: Some("2026-01-01T00:00:00.000Z".into()),
transaction_class: Some("cognitive".into()),
schema_environment_version: Some(1),
extensions: Some(extensions),
changes: vec![ChangeEntry {
op: ChangeOp::Create,
kind: ElementKind::Concept,
id: "C-1".into(),
schema_ref: Some("kip://profiles/cognitive-memory@2.0.0/Person".into()),
old_version: None,
new_version: 1,
state: None,
refs: None,
touched: vec!["attributes.name".into()],
planes: Some(PlaneVersions {
attributes: 1,
..Default::default()
}),
extensions: None,
}],
};
assert_eq!(envelope.dedup_key(), ("space-1", 1501, "tx-900"));
let entry = serde_json::to_value(&envelope.changes[0]).unwrap();
assert_eq!(entry["op"], "create");
assert_eq!(entry["kind"], "concept");
assert_eq!(entry["planes"]["attributes"], 1);
assert!(entry.get("old_version").is_none());
let lifecycle = ChangeEntry {
op: ChangeOp::Lifecycle,
kind: ElementKind::Assertion,
id: "A-1".into(),
schema_ref: None,
old_version: Some(2),
new_version: 3,
state: Some(ChangeState {
from: "active".into(),
to: "superseded".into(),
}),
refs: Some(ChangeRefs {
proposition: Some("P-1".into()),
..Default::default()
}),
touched: vec![],
planes: None,
extensions: None,
};
let json = serde_json::to_value(&lifecycle).unwrap();
assert_eq!(json["op"], "lifecycle");
assert_eq!(json["state"]["to"], "superseded");
assert_eq!(json["refs"]["proposition"], "P-1");
assert!(json["refs"].get("subject").is_none());
let replayed = envelope.clone();
assert_eq!(replayed.dedup_key(), envelope.dedup_key());
let minimal = ChangeEnvelope {
space_id: "space-1".into(),
space_seq: 1501,
tx_id: "tx-900".into(),
..Default::default()
};
let json = serde_json::to_value(&minimal).unwrap();
assert_eq!(json.as_object().unwrap().len(), 3);
assert_eq!(json["space_seq"], 1501);
}
#[test]
fn capabilities_keep_supported_and_available_apart() {
let capabilities = Capabilities {
profiles: vec![
crate::conformance::ConformanceProfile::Core,
crate::conformance::ConformanceProfile::Kql,
],
supported: serde_json::json!({ "belief_slot": true, "capsule_import": true })
.as_object()
.cloned()
.unwrap(),
available: serde_json::json!({ "belief_slot": true })
.as_object()
.cloned()
.unwrap(),
limits: serde_json::json!({ "max_limit": 1000 })
.as_object()
.cloned()
.unwrap(),
extensions: Map::new(),
};
let json = serde_json::to_value(&capabilities).unwrap();
assert_eq!(json["profiles"], serde_json::json!(["KIP-Core", "KIP-KQL"]));
assert_eq!(json["supported"]["capsule_import"], true);
assert!(
json["available"].get("capsule_import").is_none(),
"supported but not available must stay visible as exactly that"
);
assert_eq!(
serde_json::from_value::<Capabilities>(json).unwrap(),
capabilities
);
}
#[test]
fn the_envelope_nests_engine_truth_under_its_reserved_name() {
let concept = Concept {
envelope: ElementEnvelope {
id: "C-1".into(),
kind: ElementKind::Concept,
space_id: Some("space-1".into()),
system: Some(SystemState {
version: Some(3),
..Default::default()
}),
..Default::default()
},
name: Some("Alice".into()),
..Default::default()
};
let json = serde_json::to_value(&concept).unwrap();
assert_eq!(json["_system"]["version"], 3);
assert_eq!(json["name"], "Alice");
assert_eq!(json["kind"], "concept");
let decoded: Concept = serde_json::from_value(json).unwrap();
assert_eq!(decoded, concept);
}
#[test]
fn absence_of_support_is_not_rejection() {
assert!(!BeliefStatus::Insufficient.is_decided());
assert!(!BeliefStatus::Contested.is_decided());
assert!(BeliefStatus::Rejected.is_decided());
}
#[test]
fn retention_and_valid_time_are_different_fields() {
let retention = Retention {
expires_at: Some("2027-01-01T00:00:00.000Z".into()),
..Default::default()
};
let valid = ValidTime {
until: Some("2026-06-01T00:00:00.000Z".into()),
..Default::default()
};
assert_ne!(retention.expires_at, valid.until);
}
#[test]
fn the_protected_system_field_list_matches_the_spec() {
assert_eq!(PROTECTED_SYSTEM_FIELDS.len(), 8);
assert!(PROTECTED_SYSTEM_FIELDS.contains(&"space_seq"));
assert!(PROTECTED_SYSTEM_FIELDS.contains(&"origin"));
}
#[test]
fn assertion_round_trips_with_its_epistemic_payload() {
let assertion = Assertion {
envelope: ElementEnvelope {
id: "A-1".into(),
kind: ElementKind::Assertion,
..Default::default()
},
proposition: serde_json::json!({"id": "P-1"}),
asserted_by: serde_json::json!({"id": "C-alice"}),
stance: Some(Stance::Support),
mode: Some(AssertionMode::Stated),
confidence: Some(0.9),
evidence: vec![EvidenceRef::new("E-1", "support")],
..Default::default()
};
let encoded = serde_json::to_string(&assertion).unwrap();
let decoded: Assertion = serde_json::from_str(&encoded).unwrap();
assert_eq!(decoded, assertion);
}
#[test]
fn the_core_elements_use_the_field_names_the_spec_gives_them() {
let assertion = serde_json::to_value(Assertion {
proposition: serde_json::json!({"id": "P-1"}),
asserted_by: serde_json::json!({"id": "C-alice"}),
evidence: vec![EvidenceRef::new("E-1", "support")],
..Default::default()
})
.unwrap();
assert_eq!(assertion["proposition"], serde_json::json!({"id": "P-1"}));
assert_eq!(
assertion["evidence"],
serde_json::json!([{"id": "E-1", "role": "support"}])
);
for gone in ["proposition_id", "evidence_refs"] {
assert!(
assertion.get(gone).is_none(),
"{gone} is not a KIP 2.0 slot"
);
}
let evidence = serde_json::to_value(Evidence {
evidence_class: "tool_result".into(),
source: vec![serde_json::json!({"id": "C-tool"})],
..Default::default()
})
.unwrap();
assert_eq!(evidence["source"], serde_json::json!([{"id": "C-tool"}]));
assert!(evidence.get("source_refs").is_none());
}
}