Skip to main content

qualia_core_db/q42/volume/
publication.rs

1//! Fail-closed publication class for unified `.q42` volumes.
2//!
3//! A Q42 file is a rights-bearing container. It may hold Selfhood (medical
4//! records, PEP files, sanctuary graphs) or Personhood artefacts for the
5//! Permissive Commons. Public magnets, HTTP web-seed, and IPFS pins are
6//! Commons *transport*, not a default. Unmarked files stay local.
7//!
8//! SocialWebNet (pairwise DID / WireGuard) remains the transport for
9//! Bilateral Micro-Commons. This module does not replace that mesh; it
10//! stops the volume/magnet path from treating every `.q42` as open data.
11
12use std::io;
13use std::path::Path;
14
15use serde::Serialize;
16
17use super::super::{
18    Q42Volume, FLAG_PERMISSIVE_COMMONS, FLAG_SANCTUARY, QUIN_SIZE, SUPERBLOCK_HEADER,
19    SUPERBLOCK_SIZE,
20};
21use crate::{NQuin, PermissiveRoutingLane, QUINS_PER_BLOCK};
22
23/// What the caller asserts about a file. Never overrides Quin-level rights.
24#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
25pub enum PublicationIntent {
26    /// Fail closed unless the file itself is affirmatively Commons.
27    Default,
28    /// Human principal marks a catalog/ontology artefact. Still denied if any
29    /// Quin is restricted, classified, medical, legal, fiduciary, or bilateral.
30    CommonsCatalog,
31}
32
33/// How this volume may move, if at all.
34#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
35pub enum Q42PublicationClass {
36    /// Affirmative Permissive Commons (header flag or catalog intent).
37    PermissiveCommons,
38    /// Commons lane present; still not Selfhood.
39    CommonsGated,
40    /// No restricted Quins, but no Commons declaration either.
41    UnmarkedLocal,
42    /// Selfhood / bilateral / medical / classified. Never a public hash.
43    Sanctuary,
44    /// Commons material mixed with Selfhood in one file. Deny until split.
45    MixedFailClosed,
46}
47
48/// Where a classified volume is allowed to travel.
49#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
50pub enum Q42Transport {
51    WebTorrentCommons,
52    SocialWebNetBilateral,
53    LocalSanctuaryOnly,
54}
55
56#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
57pub struct ClassificationCounts {
58    pub quins_scanned: u64,
59    pub public: u64,
60    pub restricted: u64,
61    pub classified: u64,
62    pub professional_tier: u64,
63    pub legal_tier: u64,
64    pub medical_tier: u64,
65    pub fiduciary_tier: u64,
66    pub passthrough: u64,
67    pub commons_lane: u64,
68    pub bilateral: u64,
69    pub spatial: u64,
70    pub decode_failures: u64,
71}
72
73#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
74pub struct Q42PublicationVerdict {
75    pub class: Q42PublicationClass,
76    pub may_emit_public_magnet: bool,
77    pub may_http_webseed: bool,
78    pub may_ipfs_pin: bool,
79    pub transport: Q42Transport,
80    pub reason: String,
81    pub counts: ClassificationCounts,
82    pub header_commons_flag: bool,
83    pub header_sanctuary_flag: bool,
84}
85
86impl Q42PublicationClass {
87    pub fn as_str(self) -> &'static str {
88        match self {
89            Self::PermissiveCommons => "permissive-commons",
90            Self::CommonsGated => "commons-gated",
91            Self::UnmarkedLocal => "unmarked-local",
92            Self::Sanctuary => "sanctuary",
93            Self::MixedFailClosed => "mixed-fail-closed",
94        }
95    }
96}
97
98impl Q42Transport {
99    pub fn as_str(self) -> &'static str {
100        match self {
101            Self::WebTorrentCommons => {
102                "WebTorrent Permissive Commons (hash-addressed; not open-data)"
103            }
104            Self::SocialWebNetBilateral => {
105                "SocialWebNet (pairwise DID / WireGuard). No public magnet."
106            }
107            Self::LocalSanctuaryOnly => "local Sanctuary only. No public magnet, web-seed, or IPFS.",
108        }
109    }
110}
111
112/// True when this Quin is Selfhood, bilateral, or a protected ODRL tier.
113pub fn quin_requires_sanctuary(quin: &NQuin) -> bool {
114    match quin.get_sensitivity_byte() {
115        NQuin::SENSITIVITY_RESTRICTED | NQuin::SENSITIVITY_CLASSIFIED => return true,
116        _ => {}
117    }
118    match quin.get_sensitivity_tier() {
119        NQuin::SENSITIVITY_TIER_LEGAL
120        | NQuin::SENSITIVITY_TIER_MEDICAL
121        | NQuin::SENSITIVITY_TIER_FIDUCIARY => return true,
122        _ => {}
123    }
124    quin.identify_routing_lane() == PermissiveRoutingLane::EnforceBilateralMicroCommons
125}
126
127pub fn classify_q42_path(
128    path: &Path,
129    intent: PublicationIntent,
130) -> io::Result<Q42PublicationVerdict> {
131    let volume = Q42Volume::open(path)?;
132    Ok(classify_q42_volume(&volume, intent))
133}
134
135pub fn classify_q42_volume(volume: &Q42Volume, intent: PublicationIntent) -> Q42PublicationVerdict {
136    let flags = volume.header().flags;
137    let header_commons = flags & FLAG_PERMISSIVE_COMMONS != 0;
138    let header_sanctuary = flags & FLAG_SANCTUARY != 0;
139    let mut counts = ClassificationCounts::default();
140    if volume.block_count() > 0 {
141        let mut decoded = [0u8; SUPERBLOCK_SIZE];
142        for block_index in 0..volume.block_count() as usize {
143            if volume.read_superblock_into(block_index, &mut decoded).is_err() {
144                counts.decode_failures += 1;
145                continue;
146            }
147            let live = u64::from_le_bytes(decoded[16..24].try_into().unwrap()) as usize;
148            if live == 0 || live > QUINS_PER_BLOCK {
149                counts.decode_failures += 1;
150                continue;
151            }
152            for quin_index in 0..live {
153                let offset = SUPERBLOCK_HEADER + quin_index * QUIN_SIZE;
154                let quin: NQuin =
155                    bytemuck::pod_read_unaligned(&decoded[offset..offset + QUIN_SIZE]);
156                accumulate(&mut counts, &quin);
157            }
158        }
159    }
160    decide(header_commons, header_sanctuary, intent, counts)
161}
162
163/// Volume-set: any child that cannot be public denies the whole public set.
164pub fn classify_q42_volume_set(
165    root: &Path,
166    intent: PublicationIntent,
167) -> io::Result<Q42PublicationVerdict> {
168    let root_verdict = classify_q42_path(root, intent)?;
169    if !root_verdict.may_emit_public_magnet {
170        return Ok(root_verdict);
171    }
172    let volume = Q42Volume::open(root)?;
173    let Some(manifest) = volume.volume_manifest()? else {
174        return Ok(root_verdict);
175    };
176    let parent = root.parent().unwrap_or(Path::new("."));
177    for entry in &manifest.segments {
178        let child = parent.join(&entry.locator);
179        let child_verdict = classify_q42_path(&child, intent)?;
180        if !child_verdict.may_emit_public_magnet {
181            return Ok(child_verdict);
182        }
183    }
184    Ok(root_verdict)
185}
186
187pub fn deny_public_publication(verdict: &Q42PublicationVerdict) -> io::Result<()> {
188    if verdict.may_emit_public_magnet {
189        return Ok(());
190    }
191    Err(io::Error::new(
192        io::ErrorKind::PermissionDenied,
193        verdict.reason.clone(),
194    ))
195}
196
197fn accumulate(counts: &mut ClassificationCounts, quin: &NQuin) {
198    counts.quins_scanned += 1;
199    match quin.get_sensitivity_byte() {
200        NQuin::SENSITIVITY_RESTRICTED => counts.restricted += 1,
201        NQuin::SENSITIVITY_CLASSIFIED => counts.classified += 1,
202        _ => counts.public += 1,
203    }
204    match quin.get_sensitivity_tier() {
205        NQuin::SENSITIVITY_TIER_PROFESSIONAL => counts.professional_tier += 1,
206        NQuin::SENSITIVITY_TIER_LEGAL => counts.legal_tier += 1,
207        NQuin::SENSITIVITY_TIER_MEDICAL => counts.medical_tier += 1,
208        NQuin::SENSITIVITY_TIER_FIDUCIARY => counts.fiduciary_tier += 1,
209        _ => {}
210    }
211    match quin.identify_routing_lane() {
212        PermissiveRoutingLane::PassthroughStandard => counts.passthrough += 1,
213        PermissiveRoutingLane::EnforcePermissiveCommons => counts.commons_lane += 1,
214        PermissiveRoutingLane::EnforceBilateralMicroCommons => counts.bilateral += 1,
215        PermissiveRoutingLane::SpatiotemporalAmbiguous => counts.spatial += 1,
216    }
217}
218
219fn sanctuary_quin_count(counts: &ClassificationCounts) -> u64 {
220    counts.restricted
221        + counts.classified
222        + counts.legal_tier
223        + counts.medical_tier
224        + counts.fiduciary_tier
225        + counts.bilateral
226}
227
228fn decide(
229    header_commons: bool,
230    header_sanctuary: bool,
231    intent: PublicationIntent,
232    counts: ClassificationCounts,
233) -> Q42PublicationVerdict {
234    let sanctuary_bits = header_sanctuary
235        || sanctuary_quin_count(&counts) > 0
236        || counts.decode_failures > 0;
237    let commons_bits = header_commons
238        || intent == PublicationIntent::CommonsCatalog
239        || counts.commons_lane > 0;
240
241    let (class, reason) = if sanctuary_bits && commons_bits {
242        (
243            Q42PublicationClass::MixedFailClosed,
244            "Q42 publication denied: this file mixes Permissive Commons material with Selfhood, medical, legal, fiduciary, classified, or bilateral Quins. Split the volume. Medical records of a person (including a politically exposed person) must not share a public magnet with a catalog.".into(),
245        )
246    } else if sanctuary_bits {
247        let why = if counts.decode_failures > 0 {
248            "Q42 publication denied: SuperBlocks could not be classified; fail closed."
249        } else if header_sanctuary {
250            "Q42 publication denied: FLAG_SANCTUARY. This volume stays in Sanctuary / SocialWebNet."
251        } else {
252            "Q42 publication denied: restricted, classified, medical, legal, fiduciary, or bilateral Quins. Public magnet, HTTP web-seed, and IPFS are Commons transport, not a dump of a person's file."
253        };
254        (Q42PublicationClass::Sanctuary, why.into())
255    } else if header_commons || intent == PublicationIntent::CommonsCatalog {
256        if counts.commons_lane > 0 && !header_commons && intent != PublicationIntent::CommonsCatalog
257        {
258            (
259                Q42PublicationClass::CommonsGated,
260                "Permissive Commons (gated lane). Magnet is hash-addressed Commons transport, not open data.".into(),
261            )
262        } else if counts.commons_lane > 0 {
263            (
264                Q42PublicationClass::CommonsGated,
265                "Permissive Commons catalog with EnforcePermissiveCommons Quins. Not open data; TrustGroup still applies on consume.".into(),
266            )
267        } else {
268            (
269                Q42PublicationClass::PermissiveCommons,
270                "Permissive Commons catalog. Magnet and web-seed are allowed as ICN transport.".into(),
271            )
272        }
273    } else if counts.commons_lane > 0 {
274        (
275            Q42PublicationClass::CommonsGated,
276            "Permissive Commons lane is present and no Selfhood bits were found.".into(),
277        )
278    } else {
279        (
280            Q42PublicationClass::UnmarkedLocal,
281            "Q42 publication denied: this file does not declare Permissive Commons and was not marked --commons. Unmarked volumes stay local so personal and medical records cannot become bot-scrapeable magnets by default. Catalog ontologies: set FLAG_PERMISSIVE_COMMONS or pass --commons. Person-to-person: SocialWebNet.".into(),
282        )
283    };
284
285    let may_public = matches!(
286        class,
287        Q42PublicationClass::PermissiveCommons | Q42PublicationClass::CommonsGated
288    );
289    let transport = match class {
290        Q42PublicationClass::PermissiveCommons | Q42PublicationClass::CommonsGated => {
291            Q42Transport::WebTorrentCommons
292        }
293        Q42PublicationClass::Sanctuary | Q42PublicationClass::MixedFailClosed => {
294            if counts.bilateral > 0 {
295                Q42Transport::SocialWebNetBilateral
296            } else {
297                Q42Transport::LocalSanctuaryOnly
298            }
299        }
300        Q42PublicationClass::UnmarkedLocal => Q42Transport::LocalSanctuaryOnly,
301    };
302
303    Q42PublicationVerdict {
304        class,
305        may_emit_public_magnet: may_public,
306        may_http_webseed: may_public,
307        may_ipfs_pin: may_public,
308        transport,
309        reason,
310        counts,
311        header_commons_flag: header_commons,
312        header_sanctuary_flag: header_sanctuary,
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319    use crate::q42_volume::{write_unified_volume, StreamingQ42VolumeWriter};
320    use std::collections::HashMap;
321    use tempfile::NamedTempFile;
322
323    fn public_quin(object: u64) -> NQuin {
324        NQuin {
325            subject: 1,
326            predicate: 2,
327            object,
328            context: 0,
329            metadata: 0,
330            parity: 1 ^ 2 ^ object,
331        }
332    }
333
334    fn medical_quin(object: u64) -> NQuin {
335        let mut q = public_quin(object);
336        q.set_sensitivity_byte(NQuin::SENSITIVITY_CLASSIFIED);
337        q.set_sensitivity_tier(NQuin::SENSITIVITY_TIER_MEDICAL);
338        q.parity = q.subject ^ q.predicate ^ q.object ^ q.context;
339        q
340    }
341
342    fn bilateral_quin(object: u64) -> NQuin {
343        let mut q = public_quin(object);
344        q.metadata |= 0b10u64 << 61;
345        q.parity = q.subject ^ q.predicate ^ q.object ^ q.context;
346        q
347    }
348
349    fn write_blocks(path: &Path, quins: &[NQuin]) {
350        let first = quins[0].object;
351        let last = quins[quins.len() - 1].object;
352        write_unified_volume(
353            path,
354            &HashMap::new(),
355            &[(first, last)],
356            &[quins.to_vec()],
357        )
358        .unwrap();
359    }
360
361    #[test]
362    fn unmarked_volume_is_local_only() {
363        let file = NamedTempFile::new().unwrap();
364        write_blocks(file.path(), &[public_quin(3)]);
365        let verdict = classify_q42_path(file.path(), PublicationIntent::Default).unwrap();
366        assert_eq!(verdict.class, Q42PublicationClass::UnmarkedLocal);
367        assert!(!verdict.may_emit_public_magnet);
368        assert!(!verdict.may_http_webseed);
369        assert!(!verdict.may_ipfs_pin);
370    }
371
372    #[test]
373    fn commons_intent_allows_unmarked_public_catalog() {
374        let file = NamedTempFile::new().unwrap();
375        write_blocks(file.path(), &[public_quin(3)]);
376        let verdict = classify_q42_path(file.path(), PublicationIntent::CommonsCatalog).unwrap();
377        assert_eq!(verdict.class, Q42PublicationClass::PermissiveCommons);
378        assert!(verdict.may_emit_public_magnet);
379    }
380
381    #[test]
382    fn header_commons_flag_allows_magnet() {
383        let file = NamedTempFile::new().unwrap();
384        let mut writer = StreamingQ42VolumeWriter::new(&HashMap::new()).unwrap();
385        writer.declare_permissive_commons();
386        writer.push_block(0, &[public_quin(3)]).unwrap();
387        writer.finish(file.path()).unwrap();
388        let verdict = classify_q42_path(file.path(), PublicationIntent::Default).unwrap();
389        assert!(verdict.header_commons_flag);
390        assert!(verdict.may_emit_public_magnet);
391        assert_eq!(verdict.class, Q42PublicationClass::PermissiveCommons);
392    }
393
394    #[test]
395    fn medical_classified_is_sanctuary() {
396        let file = NamedTempFile::new().unwrap();
397        write_blocks(file.path(), &[medical_quin(3)]);
398        let verdict = classify_q42_path(file.path(), PublicationIntent::Default).unwrap();
399        assert_eq!(verdict.class, Q42PublicationClass::Sanctuary);
400        assert!(!verdict.may_emit_public_magnet);
401        assert!(verdict.header_sanctuary_flag);
402        assert_eq!(verdict.counts.classified, 1);
403        assert_eq!(verdict.counts.medical_tier, 1);
404    }
405
406    #[test]
407    fn commons_intent_cannot_override_medical() {
408        let file = NamedTempFile::new().unwrap();
409        write_blocks(file.path(), &[medical_quin(3)]);
410        let verdict = classify_q42_path(file.path(), PublicationIntent::CommonsCatalog).unwrap();
411        assert_eq!(verdict.class, Q42PublicationClass::MixedFailClosed);
412        assert!(!verdict.may_emit_public_magnet);
413    }
414
415    #[test]
416    fn bilateral_lane_is_social_webnet() {
417        let file = NamedTempFile::new().unwrap();
418        write_blocks(file.path(), &[bilateral_quin(3)]);
419        let verdict = classify_q42_path(file.path(), PublicationIntent::Default).unwrap();
420        assert_eq!(verdict.class, Q42PublicationClass::Sanctuary);
421        assert_eq!(verdict.transport, Q42Transport::SocialWebNetBilateral);
422        assert!(!verdict.may_emit_public_magnet);
423    }
424
425    #[test]
426    fn mixed_commons_lane_and_classified_denies() {
427        let file = NamedTempFile::new().unwrap();
428        let mut commons = public_quin(3);
429        commons.metadata |= 0b01u64 << 61;
430        write_blocks(file.path(), &[commons, medical_quin(4)]);
431        let verdict = classify_q42_path(file.path(), PublicationIntent::Default).unwrap();
432        assert_eq!(verdict.class, Q42PublicationClass::MixedFailClosed);
433        assert!(!verdict.may_emit_public_magnet);
434    }
435}