qualia_core_db/container_10d/axis_role.rs
1//! Normative axis-role taxonomy for the `.10d` container header.
2//!
3//! Each of the ten tensor axes `[q, v, w, x, y, z, t, alpha, mu, sigma]` is
4//! declared in the header with one of the roles below. The taxonomy is the
5//! prerequisite for honest queryability: a `COORDINATE` participates in
6//! distance, a `SELECTOR` is excluded from every distance sum, and a `CARRIER`
7//! is the in-band provenance lane. `mu` carries a dual role (it is both a
8//! measured coordinate and the provenance carrier) and so gets its own variant
9//! `CoordinateCarrier`.
10//!
11//! **Status: PROPOSED, not yet frozen.** Timothy Charles Holborn confirmed the
12//! Option A assignment on 2026-07-04 as the baseline to encode, but the
13//! taxonomy is not normatively frozen in the `.10d` v1 spec until the P0.7
14//! conformance vectors land. The parser in [`crate::container_10d::header`]
15//! accepts any non-`Undefined` assignment today; a future task can tighten it
16//! to reject deviations from the frozen table once blessed.
17//!
18//! Reference: `docs/plans/native-computational-geometry.md` §4.1, and the
19//! execution plan's ⚑ "Axis-role taxonomy sign-off" curation datum.
20
21/// Canonical axis order — matches the `Tensor10D` field order exactly so a
22/// `Tensor10D` can be reinterpreted as ten `f32` lanes in this order without
23/// re-shuffling. Index `i` here is index `i` into `axis_roles` in the header
24/// and into the `folded_axes` bitmask of [`super::metric_check`].
25///
26/// The three spectral/epistemic axes use their normative Greek letters (`α`,
27/// `μ`, `σ`) — the vocabulary the `.10d` spec and the AGENTS.md bit layout
28/// define — not ASCII substitutes. The `Tensor10D` Rust struct fields are
29/// named `alpha`/`mu`/`sigma` (Rust identifier convention), but the normative
30/// axis names in the header, error messages, and this table are `α`/`μ`/`σ`.
31pub const AXIS_ORDER: [&str; 10] = ["q", "v", "w", "x", "y", "z", "t", "α", "μ", "σ"];
32
33/// Axis indices for the seven COORDINATE lanes (the ones that may participate
34/// in distance). `μ` is included here because it is a coordinate in addition
35/// to being the carrier.
36pub const COORDINATE_AXES: [usize; 7] = [3, 4, 5, 6, 7, 8, 9]; // x,y,z,t,α,μ,σ
37
38/// Axis indices for the three SELECTOR lanes (excluded from every distance sum).
39pub const SELECTOR_AXES: [usize; 3] = [0, 1, 2]; // q,v,w
40
41/// Index of `μ` — the dual-role coordinate + provenance carrier.
42pub const MU_AXIS: usize = 8;
43
44/// The role a single tensor axis plays in the `.10d` distance/query model.
45///
46/// Encoded as a `u8` in the header's `axis_roles[10]` array. `Undefined` is the
47/// sentinel the parser rejects — a header with any axis left undefined fails
48/// closed (the "missing-or-undefined axis role" acceptance gate).
49#[repr(u8)]
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum AxisRole {
52 /// Sentinel for "no role assigned". The parser rejects any header carrying
53 /// this value — every axis must declare a concrete role.
54 Undefined = 0,
55 /// Excluded from every distance sum. `q`, `v`, `w`.
56 Selector = 1,
57 /// Participates in distance. `x`, `y`, `z`, `t`, `α`, `σ`.
58 Coordinate = 2,
59 /// In-band provenance lane only (not a coordinate). Reserved for a future
60 /// taxonomy where `μ` is carrier-only; not used by the proposed table.
61 Carrier = 3,
62 /// Dual-role: a `COORDINATE` that is also the in-band provenance `CARRIER`.
63 /// This is `μ`'s role under the proposed Option A taxonomy — it preserves
64 /// the foundational promise that provenance/consent can exert geometric
65 /// proximity or weight during queries.
66 CoordinateCarrier = 4,
67}
68
69impl AxisRole {
70 /// True if this role participates in distance (pure coordinate or the
71 /// dual-role coordinate+carrier).
72 #[inline]
73 pub const fn is_coordinate(self) -> bool {
74 matches!(self, AxisRole::Coordinate | AxisRole::CoordinateCarrier)
75 }
76
77 /// True if this role carries the provenance lane.
78 #[inline]
79 pub const fn is_carrier(self) -> bool {
80 matches!(self, AxisRole::Carrier | AxisRole::CoordinateCarrier)
81 }
82
83 /// Decode a raw `u8` back to an `AxisRole`, or `None` if the value is not a
84 /// defined variant. Used by the parser.
85 #[inline]
86 pub const fn from_u8(raw: u8) -> Option<AxisRole> {
87 match raw {
88 0 => Some(AxisRole::Undefined),
89 1 => Some(AxisRole::Selector),
90 2 => Some(AxisRole::Coordinate),
91 3 => Some(AxisRole::Carrier),
92 4 => Some(AxisRole::CoordinateCarrier),
93 _ => None,
94 }
95 }
96}
97
98/// The proposed (not-yet-frozen) normative axis-role table, indexed by
99/// `AXIS_ORDER`. This is the Option A assignment Timothy confirmed on
100/// 2026-07-04:
101///
102/// - `q, v, w` → `Selector`
103/// - `x, y, z, t, α, σ` → `Coordinate`
104/// - `μ` → `CoordinateCarrier` (dual-role: coordinate + provenance carrier)
105///
106/// The header's `axis_roles` field is initialised from this table by
107/// [`super::header::Container10dHeader::proposed`].
108pub const PROPOSED_AXIS_ROLES: [AxisRole; 10] = [
109 AxisRole::Selector, // q
110 AxisRole::Selector, // v
111 AxisRole::Selector, // w
112 AxisRole::Coordinate, // x
113 AxisRole::Coordinate, // y
114 AxisRole::Coordinate, // z
115 AxisRole::Coordinate, // t
116 AxisRole::Coordinate, // α
117 AxisRole::CoordinateCarrier, // μ
118 AxisRole::Coordinate, // σ
119];
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn proposed_table_matches_option_a_confirmation() {
127 // q,v,w are selectors
128 for &i in &SELECTOR_AXES {
129 assert_eq!(
130 PROPOSED_AXIS_ROLES[i],
131 AxisRole::Selector,
132 "axis {} should be Selector",
133 AXIS_ORDER[i]
134 );
135 }
136 // x,y,z,t,α,σ are pure coordinates
137 for &i in &[3usize, 4, 5, 6, 7, 9] {
138 assert_eq!(
139 PROPOSED_AXIS_ROLES[i],
140 AxisRole::Coordinate,
141 "axis {} should be Coordinate",
142 AXIS_ORDER[i]
143 );
144 }
145 // μ is the dual-role coordinate+carrier
146 assert_eq!(PROPOSED_AXIS_ROLES[MU_AXIS], AxisRole::CoordinateCarrier);
147 assert!(PROPOSED_AXIS_ROLES[MU_AXIS].is_coordinate());
148 assert!(PROPOSED_AXIS_ROLES[MU_AXIS].is_carrier());
149 }
150
151 #[test]
152 fn axis_role_round_trips_through_u8() {
153 for raw in 0u8..=4 {
154 let role = AxisRole::from_u8(raw).expect("0..=4 are defined");
155 assert_eq!(role as u8, raw);
156 }
157 assert!(AxisRole::from_u8(5).is_none());
158 assert!(AxisRole::from_u8(255).is_none());
159 }
160
161 #[test]
162 fn coordinate_axes_cover_seven_lanes_including_mu() {
163 assert_eq!(COORDINATE_AXES.len(), 7);
164 assert!(
165 COORDINATE_AXES.contains(&MU_AXIS),
166 "mu is a coordinate under Option A"
167 );
168 }
169}