Skip to main content

qualia_core_db/container_10d/
header.rs

1//! Normative `.10d` container header — the barrier surface every later P0
2//! task consumes.
3//!
4//! Layout: 64 bytes, `repr(C)`, all fields naturally aligned, one explicit
5//! `pad0[2]` field that must be zero. The header carries:
6//!
7//! - magic + version (the parse-rejection gates for bad magic / unknown version)
8//! - `axis_roles[10]` — the normative axis-role taxonomy (one `AxisRole` per
9//!   axis in `AXIS_ORDER`); any `Undefined` entry is rejected
10//! - `metric_descriptor` — the [`super::metric_check::MetricCompletenessDescriptor`]
11//!   verified against `Tensor10D::full_distance`'s actual v-branch behaviour;
12//!   a diverging descriptor is rejected (the "queryability claim == code" gate)
13//! - `header_crc32c` — **spec-reserved in P0.1**: the field exists and is
14//!   written as zero on encode, but P0.3 wires the shared CRC-32C (delegated
15//!   from `q42/p64_weight.rs`) and starts enforcing it. P0.1 does not enforce
16//!   the CRC — that is P0.3's acceptance gate, not P0.1's.
17//! - `reserved[8]` — zero, future use (governance default-disposition flags,
18//!   capability bits, time-base selector — all P0.2+ territory).
19//!
20//! The header is the foundation every later P0 task writes into, so its byte
21//! layout is frozen at v1 and asserted by `header_is_pod_with_exact_size`:
22//! `size_of::<Container10dHeader>() == 64` and the named pad/reserved fields
23//! are zero.
24
25use bytemuck::{Pod, Zeroable};
26
27use crate::container_10d::axis_role::{AxisRole, PROPOSED_AXIS_ROLES};
28use crate::container_10d::metric_check::{
29    proposed_metric_descriptor, verify_descriptor_against_reality, MetricCompletenessDescriptor,
30};
31
32/// `.10d` container magic — ASCII `"10d\0"`.
33pub const MAGIC_10D: [u8; 4] = *b"10d\0";
34
35/// `.10d` header version. Increment only when the POD layout or
36/// caller-buffer contract changes (forward-compat is by version, not by
37/// flag bits).
38pub const HEADER_VERSION: u16 = 1;
39
40/// Header flag bit 0: default disposition = Refuse. A reader that ignores the
41/// Governance section still fails closed. Always set in v1 headers produced by
42/// [`Container10dHeader::proposed`].
43pub const FLAG_DEFAULT_DISPOSITION_REFUSE: u16 = 1 << 0;
44
45/// Exact byte size of the header POD. Asserted by the size-of test so a
46/// future field addition cannot silently shift the layout.
47pub const HEADER_BYTE_SIZE: usize = 64;
48
49/// Parse error categories — one per acceptance-gate rejection.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum HeaderParseError {
52    /// Input shorter than `HEADER_BYTE_SIZE`.
53    TooShort { got: usize },
54    /// Magic bytes do not match `MAGIC_10D`.
55    BadMagic { got: [u8; 4] },
56    /// Version is not `HEADER_VERSION`.
57    UnknownVersion { got: u16 },
58    /// A padding/reserved field is non-zero (the "zero padding" gate).
59    NonZeroPadding { field: &'static str },
60    /// An axis role byte is not a defined `AxisRole` variant.
61    UndefinedAxisRole { axis_index: usize, got: u8 },
62    /// The metric-completeness descriptor diverges from `full_distance`'s
63    /// actual v-branch behaviour. Carries the divergence detail.
64    MetricDivergence(String),
65    /// The section-table pointer in the header is inconsistent: offset is not
66    /// `0` (no table) and not `>= HEADER_BYTE_SIZE` and within the file, or
67    /// `section_count` exceeds `MAX_SECTION_COUNT`, or exactly one of
68    /// (offset, count) is zero.
69    BadSectionTablePointer { offset: u32, count: u32 },
70}
71
72impl std::fmt::Display for HeaderParseError {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        match self {
75            Self::TooShort { got } => write!(
76                f,
77                "10d header too short: got {got} bytes, need {HEADER_BYTE_SIZE}"
78            ),
79            Self::BadMagic { got } => write!(f, "10d bad magic: got {got:?}, need {MAGIC_10D:?}"),
80            Self::UnknownVersion { got } => {
81                write!(f, "10d unknown version: got {got}, need {HEADER_VERSION}")
82            }
83            Self::NonZeroPadding { field } => write!(f, "10d non-zero padding in field {field:?}"),
84            Self::UndefinedAxisRole { axis_index, got } => write!(
85                f,
86                "10d undefined axis role at index {axis_index}: raw {got}"
87            ),
88            Self::MetricDivergence(msg) => write!(f, "10d {msg}"),
89            Self::BadSectionTablePointer { offset, count } => write!(
90                f,
91                "10d bad section-table pointer: offset={offset}, count={count}"
92            ),
93        }
94    }
95}
96
97impl std::error::Error for HeaderParseError {}
98
99/// Maximum number of section descriptors the parser will accept. Bounds the
100/// section-table allocation against a hostile/malformed file. 1024 is far
101/// beyond any realistic `.10d` (the design's section types number ~10) while
102/// keeping the table trivially small.
103pub const MAX_SECTION_COUNT: u32 = 1024;
104
105/// The normative `.10d` v1 header — 64 bytes, `repr(C)`, naturally aligned.
106///
107/// Field layout (offsets):
108/// ```text
109/// offset  size  field
110/// 0       4     magic
111/// 4       2     version
112/// 6       2     flags
113/// 8       10    axis_roles          (one AxisRole u8 per AXIS_ORDER axis)
114/// 18      2     pad0                (must be zero — aligns metric_descriptor to 4)
115/// 20      32    metric_descriptor   (4 x MetricBranchDescriptor, 8 bytes each)
116/// 52      4     header_crc32c       (spec-reserved in P0.1; P0.3 wires shared CRC-32C)
117/// 56      4     section_table_offset (byte offset from file start; 0 = no table)
118/// 60      4     section_count        (number of SectionDescriptor rows; 0 = no table)
119/// ```
120///
121/// The `section_table_offset` + `section_count` fields at offsets 56–63 were
122/// the `reserved[8]` field in the initial P0.1 landing; P0.2 defines their
123/// meaning within v1 (the POD layout is unchanged — only a reserved field's
124/// semantics are now specified). See the P0.2 progress-log entry.
125#[repr(C)]
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Pod, Zeroable)]
127pub struct Container10dHeader {
128    pub magic: [u8; 4],
129    pub version: u16,
130    pub flags: u16,
131    pub axis_roles: [u8; 10],
132    pub pad0: [u8; 2],
133    pub metric_descriptor: MetricCompletenessDescriptor,
134    pub header_crc32c: u32,
135    /// Byte offset of the section table from the start of the file. `0` means
136    /// no section table (a bare header — `section_count` must also be `0`).
137    /// Otherwise must be `>= HEADER_BYTE_SIZE` and within the file.
138    pub section_table_offset: u32,
139    /// Number of `SectionDescriptor` rows in the section table. `0` means no
140    /// table (a bare header — `section_table_offset` must also be `0`).
141    /// Must be `<= MAX_SECTION_COUNT`.
142    pub section_count: u32,
143}
144
145impl Default for Container10dHeader {
146    fn default() -> Self {
147        Self::proposed()
148    }
149}
150
151impl Container10dHeader {
152    /// The proposed (not-yet-frozen) v1 header: Option A axis-role taxonomy +
153    /// option (b) metric-completeness descriptor (the documented limitation
154    /// matching current `full_distance` reality) + default-disposition-Refuse
155    /// flag set. CRC left zero (P0.3 wires the shared CRC-32C).
156    pub fn proposed() -> Self {
157        let mut axis_roles = [0u8; 10];
158        for (i, role) in PROPOSED_AXIS_ROLES.iter().enumerate() {
159            axis_roles[i] = *role as u8;
160        }
161        Self {
162            magic: MAGIC_10D,
163            version: HEADER_VERSION,
164            flags: FLAG_DEFAULT_DISPOSITION_REFUSE,
165            axis_roles,
166            pad0: [0, 0],
167            metric_descriptor: proposed_metric_descriptor(),
168            header_crc32c: 0,
169            section_table_offset: 0,
170            section_count: 0,
171        }
172    }
173
174    /// Encode the header into a caller-supplied 64-byte buffer (little-endian
175    /// where applicable; the POD is already LE-friendly). Zero-alloc.
176    pub fn encode(&self, out: &mut [u8; HEADER_BYTE_SIZE]) {
177        // SAFETY: Container10dHeader is repr(C) + Pod + size 64. Casting to
178        // bytes is sound; copy into the caller buffer.
179        let bytes: &[u8; HEADER_BYTE_SIZE] = bytemuck::cast_ref(self);
180        *out = *bytes;
181    }
182
183    /// Encode into a freshly-owned 64-byte array. Convenience for tests and
184    /// writers that are not on a zero-heap hot path.
185    pub fn encode_to_vec64(&self) -> [u8; HEADER_BYTE_SIZE] {
186        let mut out = [0u8; HEADER_BYTE_SIZE];
187        self.encode(&mut out);
188        out
189    }
190
191    /// Parse and validate a 64-byte header. Runs every P0.1 acceptance gate:
192    /// bad magic, unknown version, non-zero structural padding, undefined axis
193    /// role, metric-completeness divergence from `full_distance` reality, and
194    /// (P0.2) a consistent section-table pointer.
195    pub fn parse(data: &[u8]) -> Result<Container10dHeader, HeaderParseError> {
196        if data.len() < HEADER_BYTE_SIZE {
197            return Err(HeaderParseError::TooShort { got: data.len() });
198        }
199        let mut buf = [0u8; HEADER_BYTE_SIZE];
200        buf.copy_from_slice(&data[..HEADER_BYTE_SIZE]);
201        // SAFETY: Container10dHeader is repr(C) + Pod + size 64; the buffer is
202        // exactly 64 bytes and initialised.
203        let header: Container10dHeader = *bytemuck::from_bytes(&buf);
204
205        if header.magic != MAGIC_10D {
206            return Err(HeaderParseError::BadMagic { got: header.magic });
207        }
208        if header.version != HEADER_VERSION {
209            return Err(HeaderParseError::UnknownVersion {
210                got: header.version,
211            });
212        }
213        if header.pad0 != [0, 0] {
214            return Err(HeaderParseError::NonZeroPadding { field: "pad0" });
215        }
216        // Section-table pointer consistency (P0.2). Either both zero (no
217        // table — a bare header) or both non-zero with offset >= header size,
218        // offset within the file, and count <= MAX_SECTION_COUNT. The
219        // table-bytes-within-file and per-descriptor validation is done by
220        // the section-table reader in `section.rs`; here we only gate the
221        // header-level pointer.
222        let (off, cnt) = (header.section_table_offset, header.section_count);
223        let both_zero = off == 0 && cnt == 0;
224        let both_nonzero = off != 0 && cnt != 0;
225        let valid_nonzero = both_nonzero
226            && off as usize >= HEADER_BYTE_SIZE
227            && off as usize <= data.len()
228            && cnt <= MAX_SECTION_COUNT;
229        if !both_zero && !valid_nonzero {
230            return Err(HeaderParseError::BadSectionTablePointer {
231                offset: off,
232                count: cnt,
233            });
234        }
235        for (i, &raw) in header.axis_roles.iter().enumerate() {
236            if AxisRole::from_u8(raw).is_none() || raw == AxisRole::Undefined as u8 {
237                return Err(HeaderParseError::UndefinedAxisRole {
238                    axis_index: i,
239                    got: raw,
240                });
241            }
242        }
243        verify_descriptor_against_reality(&header.metric_descriptor)
244            .map_err(|d| HeaderParseError::MetricDivergence(d.to_string()))?;
245        Ok(header)
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252    use crate::container_10d::axis_role::{AxisRole, AXIS_ORDER};
253    use crate::container_10d::metric_check::{MetricKind, BOUNDARY_CLIQUE_BRANCH_INDEX};
254
255    #[test]
256    fn header_is_pod_with_exact_size_and_zero_padding() {
257        assert_eq!(
258            std::mem::size_of::<Container10dHeader>(),
259            HEADER_BYTE_SIZE,
260            "header must be exactly {HEADER_BYTE_SIZE} bytes"
261        );
262        // Offset assertions so doc and code cannot drift.
263        assert_eq!(std::mem::offset_of!(Container10dHeader, magic), 0);
264        assert_eq!(std::mem::offset_of!(Container10dHeader, version), 4);
265        assert_eq!(std::mem::offset_of!(Container10dHeader, flags), 6);
266        assert_eq!(std::mem::offset_of!(Container10dHeader, axis_roles), 8);
267        assert_eq!(std::mem::offset_of!(Container10dHeader, pad0), 18);
268        assert_eq!(
269            std::mem::offset_of!(Container10dHeader, metric_descriptor),
270            20
271        );
272        assert_eq!(std::mem::offset_of!(Container10dHeader, header_crc32c), 52);
273        assert_eq!(
274            std::mem::offset_of!(Container10dHeader, section_table_offset),
275            56
276        );
277        assert_eq!(std::mem::offset_of!(Container10dHeader, section_count), 60);
278        // The proposed (bare) header has zero pad and no section table.
279        let h = Container10dHeader::proposed();
280        assert_eq!(h.pad0, [0, 0]);
281        assert_eq!(h.section_table_offset, 0);
282        assert_eq!(h.section_count, 0);
283    }
284
285    #[test]
286    fn encode_is_bit_identical_across_two_runs() {
287        let h = Container10dHeader::proposed();
288        let mut a = [0u8; HEADER_BYTE_SIZE];
289        let mut b = [0u8; HEADER_BYTE_SIZE];
290        h.encode(&mut a);
291        h.encode(&mut b);
292        assert_eq!(
293            a, b,
294            "two encodes of the same header must be byte-identical"
295        );
296    }
297
298    #[test]
299    fn round_trip_proposed_header() {
300        let h = Container10dHeader::proposed();
301        let bytes = h.encode_to_vec64();
302        let parsed = Container10dHeader::parse(&bytes).expect("proposed header must parse");
303        assert_eq!(parsed, h);
304    }
305
306    #[test]
307    fn parse_rejects_bad_magic() {
308        let mut bytes = Container10dHeader::proposed().encode_to_vec64();
309        bytes[0] = b'x';
310        let err = Container10dHeader::parse(&bytes).expect_err("bad magic must reject");
311        assert!(matches!(err, HeaderParseError::BadMagic { .. }), "{err}");
312    }
313
314    #[test]
315    fn parse_rejects_unknown_version() {
316        let mut bytes = Container10dHeader::proposed().encode_to_vec64();
317        // version is at offset 4, little-endian u16.
318        bytes[4] = 0xff;
319        bytes[5] = 0xff;
320        let err = Container10dHeader::parse(&bytes).expect_err("unknown version must reject");
321        assert!(
322            matches!(err, HeaderParseError::UnknownVersion { got: 0xffff }),
323            "{err}"
324        );
325    }
326
327    #[test]
328    fn parse_rejects_undefined_axis_role() {
329        let mut bytes = Container10dHeader::proposed().encode_to_vec64();
330        // axis_roles starts at offset 8; set the first (q) to Undefined (0).
331        bytes[8] = AxisRole::Undefined as u8;
332        let err = Container10dHeader::parse(&bytes).expect_err("undefined axis role must reject");
333        assert!(
334            matches!(
335                err,
336                HeaderParseError::UndefinedAxisRole { axis_index: 0, .. }
337            ),
338            "{err}"
339        );
340    }
341
342    #[test]
343    fn parse_rejects_unknown_axis_role_byte() {
344        let mut bytes = Container10dHeader::proposed().encode_to_vec64();
345        // axis_roles[3] (x) — set to an undefined raw value (5).
346        bytes[8 + 3] = 5;
347        let err =
348            Container10dHeader::parse(&bytes).expect_err("unknown axis role byte must reject");
349        assert!(
350            matches!(
351                err,
352                HeaderParseError::UndefinedAxisRole {
353                    axis_index: 3,
354                    got: 5
355                }
356            ),
357            "{err}"
358        );
359    }
360
361    #[test]
362    fn parse_rejects_non_zero_pad0() {
363        let mut bytes = Container10dHeader::proposed().encode_to_vec64();
364        bytes[18] = 1;
365        let err = Container10dHeader::parse(&bytes).expect_err("non-zero pad0 must reject");
366        assert!(
367            matches!(err, HeaderParseError::NonZeroPadding { field: "pad0" }),
368            "{err}"
369        );
370    }
371
372    #[test]
373    fn parse_rejects_section_table_offset_below_header_size() {
374        // section_table_offset at offset 56 (u32 LE). Set it to 8 (< 64) with
375        // a non-zero count — must reject.
376        let mut bytes = Container10dHeader::proposed().encode_to_vec64();
377        bytes[56..60].copy_from_slice(&8u32.to_le_bytes());
378        bytes[60..64].copy_from_slice(&1u32.to_le_bytes());
379        let err = Container10dHeader::parse(&bytes).expect_err("offset < header size must reject");
380        assert!(
381            matches!(err, HeaderParseError::BadSectionTablePointer { .. }),
382            "{err}"
383        );
384    }
385
386    #[test]
387    fn parse_rejects_section_count_without_offset() {
388        // offset = 0 but count != 0 — inconsistent, must reject.
389        let mut bytes = Container10dHeader::proposed().encode_to_vec64();
390        bytes[60..64].copy_from_slice(&1u32.to_le_bytes());
391        let err = Container10dHeader::parse(&bytes).expect_err("count without offset must reject");
392        assert!(
393            matches!(err, HeaderParseError::BadSectionTablePointer { .. }),
394            "{err}"
395        );
396    }
397
398    #[test]
399    fn parse_rejects_offset_without_count() {
400        // offset != 0 but count = 0 — inconsistent, must reject.
401        let mut bytes = Container10dHeader::proposed().encode_to_vec64();
402        bytes[56..60].copy_from_slice(&64u32.to_le_bytes());
403        let err = Container10dHeader::parse(&bytes).expect_err("offset without count must reject");
404        assert!(
405            matches!(err, HeaderParseError::BadSectionTablePointer { .. }),
406            "{err}"
407        );
408    }
409
410    #[test]
411    fn parse_accepts_bare_header_no_section_table() {
412        // The proposed header has offset=0, count=0 — a bare header. Must parse.
413        let bytes = Container10dHeader::proposed().encode_to_vec64();
414        let parsed = Container10dHeader::parse(&bytes).expect("bare header must parse");
415        assert_eq!(parsed.section_table_offset, 0);
416        assert_eq!(parsed.section_count, 0);
417    }
418
419    #[test]
420    fn parse_rejects_metric_completeness_claiming_v1_folds_t() {
421        let mut bytes = Container10dHeader::proposed().encode_to_vec64();
422        // metric_descriptor starts at offset 20. Each branch is 8 bytes:
423        //   v_class@+0, metric_kind@+1, folded_axes@+2 (u16 LE), reserved@+4
424        // Branch 1 (v=1 cyclic) starts at offset 20 + 8 = 28.
425        // Set the t bit (bit 6) in folded_axes at offset 28 + 2.
426        let folded_offset = 28 + 2;
427        bytes[folded_offset] |= 1 << 6;
428        let err =
429            Container10dHeader::parse(&bytes).expect_err("diverging metric descriptor must reject");
430        match err {
431            HeaderParseError::MetricDivergence(msg) => {
432                assert!(msg.contains("v=1"), "message must name v=1: {msg}");
433                assert!(msg.contains("t"), "message must name axis t: {msg}");
434            }
435            other => panic!("expected MetricDivergence, got {other:?}"),
436        }
437    }
438
439    #[test]
440    fn parse_rejects_metric_completeness_claiming_v3_folds_x() {
441        let mut bytes = Container10dHeader::proposed().encode_to_vec64();
442        // Branch 3 (v>=3 catch-all) starts at offset 20 + 3*8 = 44.
443        // folded_axes at offset 44 + 2. Set bit 3 (x).
444        let folded_offset = 44 + 2;
445        bytes[folded_offset] |= 1 << 3;
446        let err =
447            Container10dHeader::parse(&bytes).expect_err("diverging metric descriptor must reject");
448        assert!(
449            matches!(err, HeaderParseError::MetricDivergence(_)),
450            "{err:?}"
451        );
452    }
453
454    #[test]
455    fn parse_rejects_too_short_input() {
456        let short = [0u8; 32];
457        let err = Container10dHeader::parse(&short).expect_err("short input must reject");
458        assert!(
459            matches!(err, HeaderParseError::TooShort { got: 32 }),
460            "{err}"
461        );
462    }
463
464    #[test]
465    fn proposed_header_carries_option_a_taxonomy() {
466        let h = Container10dHeader::proposed();
467        for (i, &raw) in h.axis_roles.iter().enumerate() {
468            let role = AxisRole::from_u8(raw).expect("proposed roles are all defined");
469            assert_eq!(
470                role as u8, PROPOSED_AXIS_ROLES[i] as u8,
471                "axis {}",
472                AXIS_ORDER[i]
473            );
474        }
475        // μ (index 8) is the dual-role coordinate+carrier
476        assert_eq!(h.axis_roles[8], AxisRole::CoordinateCarrier as u8);
477    }
478
479    #[test]
480    fn proposed_header_carries_documented_limitation_descriptor() {
481        let h = Container10dHeader::proposed();
482        let d = &h.metric_descriptor;
483        // v=0 euclidean folds all seven
484        assert_eq!(d.branches[0].v_class, 0);
485        assert_eq!(d.branches[0].metric_kind, MetricKind::Euclidean as u8);
486        assert_eq!(
487            d.branches[0].folded_axes.count_ones(),
488            7,
489            "v=0 must fold all seven coordinate axes"
490        );
491        // v=1 / v=2 fold xyz only (3 bits)
492        assert_eq!(d.branches[1].folded_axes.count_ones(), 3);
493        assert_eq!(d.branches[2].folded_axes.count_ones(), 3);
494        // v>=3 catch-all folds none
495        assert_eq!(d.branches[BOUNDARY_CLIQUE_BRANCH_INDEX].folded_axes, 0);
496        assert_eq!(
497            d.branches[BOUNDARY_CLIQUE_BRANCH_INDEX].metric_kind,
498            MetricKind::BoundaryClique as u8
499        );
500    }
501
502    #[test]
503    fn proposed_header_default_disposition_is_refuse() {
504        let h = Container10dHeader::proposed();
505        assert_ne!(
506            h.flags & FLAG_DEFAULT_DISPOSITION_REFUSE,
507            0,
508            "v1 header must fail closed by default"
509        );
510    }
511}