Skip to main content

qualia_core_db/entity_view/
package.rs

1//! Bifurcated socio-cognitive package wings (layout + digests).
2//!
3//! Skeleton for private vs offered/commons representation - not encryption by itself.
4
5use super::entity_id::EntityId;
6use super::observer::RepresentationWing;
7use crate::hypermedia::fnv60;
8use serde::{Deserialize, Serialize};
9
10/// One wing of a bifurcated package (fixed header; bodies referenced by digest).
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12pub struct PackageWing {
13    pub wing: RepresentationWing,
14    /// Entities included in this wing (bounded list in host; here free Vec for cold path).
15    pub entity_ids: Vec<u64>,
16    /// Content digest of wing payload bytes (0 if empty).
17    pub payload_digest: u64,
18}
19
20/// Bifurcated presentation / socio-cognitive package v1.
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct BifurcatedPackage {
23    pub version: u32,
24    pub package_id: u64,
25    pub private_wing: PackageWing,
26    pub offered_wing: PackageWing,
27    pub commons_wing: PackageWing,
28    /// Combined digest of the three wing digests (ordering fixed).
29    pub package_digest: u64,
30}
31
32impl BifurcatedPackage {
33    pub const VERSION: u32 = 1;
34
35    pub fn new(
36        package_key: &str,
37        private_ids: &[EntityId],
38        offered_ids: &[EntityId],
39        commons_ids: &[EntityId],
40    ) -> Self {
41        let private_wing = wing(RepresentationWing::Private, private_ids);
42        let offered_wing = wing(RepresentationWing::Offered, offered_ids);
43        let commons_wing = wing(RepresentationWing::Commons, commons_ids);
44        let package_id = fnv60(package_key.as_bytes());
45        let package_digest = fold_digests(&[
46            private_wing.payload_digest,
47            offered_wing.payload_digest,
48            commons_wing.payload_digest,
49            package_id,
50        ]);
51        Self {
52            version: Self::VERSION,
53            package_id,
54            private_wing,
55            offered_wing,
56            commons_wing,
57            package_digest,
58        }
59    }
60
61    /// Select wing for observer without exposing other wing entity lists to caller policy.
62    pub fn wing_for(&self, wing: RepresentationWing) -> &PackageWing {
63        match wing {
64            RepresentationWing::Private => &self.private_wing,
65            RepresentationWing::Offered => &self.offered_wing,
66            RepresentationWing::Commons => &self.commons_wing,
67        }
68    }
69}
70
71fn wing(kind: RepresentationWing, ids: &[EntityId]) -> PackageWing {
72    let entity_ids: Vec<u64> = ids.iter().map(|e| e.raw()).collect();
73    let mut bytes = Vec::with_capacity(entity_ids.len() * 8 + 1);
74    bytes.push(kind as u8);
75    for id in &entity_ids {
76        bytes.extend_from_slice(&id.to_le_bytes());
77    }
78    PackageWing {
79        wing: kind,
80        payload_digest: if entity_ids.is_empty() {
81            0
82        } else {
83            fnv60(&bytes)
84        },
85        entity_ids,
86    }
87}
88
89fn fold_digests(parts: &[u64]) -> u64 {
90    let mut bytes = Vec::with_capacity(parts.len() * 8);
91    for p in parts {
92        bytes.extend_from_slice(&p.to_le_bytes());
93    }
94    fnv60(&bytes)
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use crate::entity_view::entity_id::EntityId;
101
102    #[test]
103    fn bifurcate_package_stable_digest() {
104        let a = EntityId::from_uri("urn:a");
105        let b = EntityId::from_uri("urn:b");
106        let p1 = BifurcatedPackage::new("pkg:1", &[a], &[b], &[]);
107        let p2 = BifurcatedPackage::new("pkg:1", &[a], &[b], &[]);
108        assert_eq!(p1.package_digest, p2.package_digest);
109        assert_eq!(p1.private_wing.entity_ids.len(), 1);
110        assert_eq!(p1.offered_wing.entity_ids.len(), 1);
111        assert!(p1.commons_wing.entity_ids.is_empty());
112    }
113}