|
| 1 | +//! Generic consumer-registration roundtrip for AUTHORITATIVE capability |
| 2 | +//! tables (operator, 2026-07-07). |
| 3 | +//! |
| 4 | +//! OGAR is the authoritative store: a domain table (e.g. |
| 5 | +//! [`crate::ocr_actions`], the planned thinking-styles best-practice table) |
| 6 | +//! declares WHAT exists and WHO is expected to execute it. The consumer |
| 7 | +//! "trägt sich ein" by exporting a [`CapabilityRegistration`] const and |
| 8 | +//! asserting [`verify_registration`] in its own tests — so the whole loop |
| 9 | +//! (table declared → consumer registered → classids activated) is checked |
| 10 | +//! in the one binary, at test time, with zero serialization and zero |
| 11 | +//! ontology payload outside OGAR. lance-graph carries NOTHING of this: its |
| 12 | +//! armed contract fuse only guards the wire mirror; the authority checks |
| 13 | +//! (unique mint, correct classid activation, executor coverage) live HERE. |
| 14 | +//! |
| 15 | +//! Drift bangs once: a capability added to the table without a consumer arm, |
| 16 | +//! a consumer covering a capability the table dropped, a wrong/missing |
| 17 | +//! classid activation, or an unregistered executor name — each is a failed |
| 18 | +//! [`verify_registration`] in the consumer's test suite. |
| 19 | +
|
| 20 | +/// A consumer's self-registration against one authoritative table. |
| 21 | +#[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 22 | +pub struct CapabilityRegistration { |
| 23 | + /// Consumer crate name — must be one of the table's expected executors. |
| 24 | + pub consumer: &'static str, |
| 25 | + /// Capability names the consumer's executor covers (its dispatch arms). |
| 26 | + pub covered: &'static [&'static str], |
| 27 | + /// The subject classids the consumer activated (canon-high concept ids |
| 28 | + /// from [`crate::class_ids`]) — must equal the table's subject set. |
| 29 | + pub subject_classids: &'static [u16], |
| 30 | +} |
| 31 | + |
| 32 | +/// Why a registration failed the roundtrip. |
| 33 | +#[derive(Debug, Clone, PartialEq, Eq)] |
| 34 | +pub enum RegistrationDrift { |
| 35 | + /// Consumer name is not in the table's expected-executor list. |
| 36 | + UnexpectedConsumer(String), |
| 37 | + /// Declared capability with no consumer arm. |
| 38 | + Uncovered(String), |
| 39 | + /// Consumer arm for a capability the table does not declare. |
| 40 | + Undeclared(String), |
| 41 | + /// Consumer's activated classid set differs from the table's subjects. |
| 42 | + ClassidMismatch { |
| 43 | + /// Subject ids the table declares. |
| 44 | + declared: Vec<u16>, |
| 45 | + /// Ids the consumer activated. |
| 46 | + activated: Vec<u16>, |
| 47 | + }, |
| 48 | + /// A subject classid is not minted in [`crate::class_ids::ALL`]. |
| 49 | + Unminted(u16), |
| 50 | +} |
| 51 | + |
| 52 | +/// Verify one consumer registration against an authoritative table's |
| 53 | +/// capability names, subject classids and expected-executor list. |
| 54 | +/// |
| 55 | +/// Checks, in order: consumer expected; coverage both directions |
| 56 | +/// (declared ⊆ covered AND covered ⊆ declared); activated classid set == |
| 57 | +/// declared subject set; every id minted (unique mint is guarded by the |
| 58 | +/// codebook's own `no_duplicate_ids` test — a concept is assigned once, |
| 59 | +/// here we only require it exists). |
| 60 | +pub fn verify_registration( |
| 61 | + reg: &CapabilityRegistration, |
| 62 | + declared_capabilities: &[&str], |
| 63 | + declared_subjects: &[u16], |
| 64 | + expected_executors: &[&str], |
| 65 | +) -> Result<(), RegistrationDrift> { |
| 66 | + if !expected_executors.contains(®.consumer) { |
| 67 | + return Err(RegistrationDrift::UnexpectedConsumer(reg.consumer.into())); |
| 68 | + } |
| 69 | + for cap in declared_capabilities { |
| 70 | + if !reg.covered.contains(cap) { |
| 71 | + return Err(RegistrationDrift::Uncovered((*cap).into())); |
| 72 | + } |
| 73 | + } |
| 74 | + for cap in reg.covered { |
| 75 | + if !declared_capabilities.contains(cap) { |
| 76 | + return Err(RegistrationDrift::Undeclared((*cap).into())); |
| 77 | + } |
| 78 | + } |
| 79 | + let mut declared: Vec<u16> = declared_subjects.to_vec(); |
| 80 | + declared.sort_unstable(); |
| 81 | + declared.dedup(); |
| 82 | + let mut activated: Vec<u16> = reg.subject_classids.to_vec(); |
| 83 | + activated.sort_unstable(); |
| 84 | + activated.dedup(); |
| 85 | + if declared != activated { |
| 86 | + return Err(RegistrationDrift::ClassidMismatch { |
| 87 | + declared, |
| 88 | + activated, |
| 89 | + }); |
| 90 | + } |
| 91 | + for &id in &activated { |
| 92 | + if !crate::class_ids::ALL.iter().any(|&(_, cid)| cid == id) { |
| 93 | + return Err(RegistrationDrift::Unminted(id)); |
| 94 | + } |
| 95 | + } |
| 96 | + Ok(()) |
| 97 | +} |
| 98 | + |
| 99 | +#[cfg(test)] |
| 100 | +mod tests { |
| 101 | + use super::*; |
| 102 | + |
| 103 | + const EXPECTED: &[&str] = &["demo-executor"]; |
| 104 | + const CAPS: &[&str] = &["a", "b"]; |
| 105 | + |
| 106 | + fn reg(covered: &'static [&'static str], ids: &'static [u16]) -> CapabilityRegistration { |
| 107 | + CapabilityRegistration { |
| 108 | + consumer: "demo-executor", |
| 109 | + covered, |
| 110 | + subject_classids: ids, |
| 111 | + } |
| 112 | + } |
| 113 | + |
| 114 | + #[test] |
| 115 | + fn green_roundtrip() { |
| 116 | + // 0x0805 textline is a real mint. |
| 117 | + assert_eq!( |
| 118 | + verify_registration(®(&["a", "b"], &[0x0805]), CAPS, &[0x0805], EXPECTED), |
| 119 | + Ok(()) |
| 120 | + ); |
| 121 | + } |
| 122 | + |
| 123 | + #[test] |
| 124 | + fn each_drift_arm_bangs() { |
| 125 | + let mut r = reg(&["a", "b"], &[0x0805]); |
| 126 | + r.consumer = "stranger"; |
| 127 | + assert!(matches!( |
| 128 | + verify_registration(&r, CAPS, &[0x0805], EXPECTED), |
| 129 | + Err(RegistrationDrift::UnexpectedConsumer(_)) |
| 130 | + )); |
| 131 | + assert!(matches!( |
| 132 | + verify_registration(®(&["a"], &[0x0805]), CAPS, &[0x0805], EXPECTED), |
| 133 | + Err(RegistrationDrift::Uncovered(_)) |
| 134 | + )); |
| 135 | + assert!(matches!( |
| 136 | + verify_registration(®(&["a", "b", "c"], &[0x0805]), CAPS, &[0x0805], EXPECTED), |
| 137 | + Err(RegistrationDrift::Undeclared(_)) |
| 138 | + )); |
| 139 | + assert!(matches!( |
| 140 | + verify_registration(®(&["a", "b"], &[0x0806]), CAPS, &[0x0805], EXPECTED), |
| 141 | + Err(RegistrationDrift::ClassidMismatch { .. }) |
| 142 | + )); |
| 143 | + assert!(matches!( |
| 144 | + verify_registration(®(&["a", "b"], &[0xFFFE]), CAPS, &[0xFFFE], EXPECTED), |
| 145 | + Err(RegistrationDrift::Unminted(0xFFFE)) |
| 146 | + )); |
| 147 | + } |
| 148 | +} |
0 commit comments