From cc9d74ba2ac963dbb112d4bcb25c079553a879ea Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 08:00:26 +0000 Subject: [PATCH 1/2] lance-graph-ogar: a plug that asks only for a reading activates (D-HPR-8) Reading is not a capability (operator decision, 2026-10-11). A HotPlug with an empty `covered` list now skips the capability join, so a concept with no action table, or a table whose executor list does not name the consumer, no longer refuses the plug. Palette seats and canon storage domains keep their existing routing. Every other id must still be a minted concept, and its row still goes through the mirror check. A plug that names any capability goes through the join unchanged. This unblocks the SAP and Odoo glove plugs on 0x0103; the glove-parity pin is updated to match. Tests: 2 new in lance-graph-ogar (can-fire, can-stay-silent). Red before the fix (NoCapabilitiesFor(0x0103)). A disable run that made every plug reading-only turned the silence test and 5 others red. lance-graph-ogar 109 tests and lance-graph-glove-parity 16 tests pass; fmt clean; no new clippy findings. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019fbZ92dFKQ5BJHrA3YcH9A --- .claude/board/STATUS_BOARD.md | 2 +- .../plans/v3-mandatory-hotplug-reading-v1.md | 2 +- crates/lance-graph-glove-parity/src/basis.rs | 3 +- .../tests/authority.rs | 25 ++++- crates/lance-graph-ogar/src/lib.rs | 91 +++++++++++++++++++ 5 files changed, 117 insertions(+), 6 deletions(-) diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 2abaa464f..1ffd7a149 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -25,7 +25,7 @@ Plan: `.claude/plans/v3-mandatory-hotplug-reading-v1.md`. | **D-HPR-5** | Stored legacy keys keep resolving; a `SlabDeclaration` may override for its own data; no forced removal | Holds today (status quo). Caveat: a `SlabDeclaration` overrides `value_schema` only, never the tail, so it cannot protect a V1/V2-tail slab (`hotplug.rs:231-257, 392`) | — | | **D-HPR-6** | No V1 default for an unknown classid; `guid-v3-tail` not a gate | Queued — split: **6a** un-gate `guid-v3-tail` (default-on, and no fleet consumer disables it, so only `--no-default-features` builds change; there the 5 V3 fossils read `DEFAULT` today); **6b** unknown classid → error. Decided 2026-10-11: go, staged after 6a, D-HPR-3 and a stored-classid check | unknown classid → `NoReadingFor` | | **D-HPR-7** | Storage readings leave the concept key: out of `concept_override`, into the slab's `SlabDeclaration` or the owning consumer's plug (O3, decided (a) 2026-10-11) | Queued | a non-Odoo plug of `0x0202` reads `PLUG_AND_PLAY_V3`; Odoo's slab still reads `ERP_V3` | -| **D-HPR-8** | A plug that asks only for a reading succeeds (reading is not a capability; decided 2026-10-11) | Queued | `[0x0103]` with `covered: &[]` → `Ok`, `PLUG_AND_PLAY_V3`; with a covered capability → refused as today | +| **D-HPR-8** | A plug that asks only for a reading succeeds (reading is not a capability; decided 2026-10-11) | In PR (`OgarAuthority::activate` skips the capability join when `covered` is empty; ids still must be minted; red before the fix with `NoCapabilitiesFor(0x0103)`; disable run turns the twin red) | `[0x0103]` with `covered: &[]` → `Ok`, `PLUG_AND_PLAY_V3`; with a covered capability → refused as today | ## D-XGP — Cross-glove business parity: Odoo × SAP over one Quack algebra (2026-10-10) diff --git a/.claude/plans/v3-mandatory-hotplug-reading-v1.md b/.claude/plans/v3-mandatory-hotplug-reading-v1.md index 50898b1de..fad2df731 100644 --- a/.claude/plans/v3-mandatory-hotplug-reading-v1.md +++ b/.claude/plans/v3-mandatory-hotplug-reading-v1.md @@ -64,7 +64,7 @@ What it means, in rules: - Split (2026-10-11). **W6a**: un-gate `guid-v3-tail`. It is default-on and no fleet consumer disables it, so only `--no-default-features` builds change; there the 5 V3 fossils read `DEFAULT` today (coresearch probe P-X3). **W6b**: an unknown classid errors. This is a behaviour change, and both q2 and MedCare have already hit the V1 default once, so it needs operator approval. - **Decided (operator, 2026-10-11: "Recommended accepted"): go, staged.** 1. W6a. 2. W3 moves the remaining mint paths onto plug readings. 3. A check that every stored artifact's classid resolves. 4. Only then W6b. - [ ] **W7 — D-HPR-7: storage readings leave the concept key** (O3, decided (a)). The five domain storage readings move out of `concept_override` (keyed by concept) into the slab's `SlabDeclaration` or the owning consumer's plug. A consumer plugging a shared concept then reads `PLUG_AND_PLAY_V3`. Falsifier: a plug of `0x0202` by a non-Odoo consumer reads `PLUG_AND_PLAY_V3`, and Odoo's own slab still reads `ERP_V3`. -- [ ] **W8 — D-HPR-8: a plug that asks only for a reading succeeds** (decided). Reading is not a capability. A plug with `covered` empty gets readings only, also for a concept that has a capability table. A plug that names a capability still needs the table. Falsifier: `[0x0103]` with `covered: &[]` → `Ok` and `read_mode_for(0x0103) == PLUG_AND_PLAY_V3`; with `covered: &["anything"]` → refused as today. +- [x] **W8 — D-HPR-8: a plug that asks only for a reading succeeds** (decided). **Shipped:** `lance-graph-ogar` `OgarAuthority::activate`, tests `a_plug_covering_nothing_activates_for_its_reading_only` (SAP glove on `0x0103`, a non-executor on `0x0901`) and `a_plug_that_covers_a_capability_is_still_refused_without_its_table`. The glove-parity pin now says both glove plugs activate for their reading; claiming a capability on `0x0103` is still `NoCapabilitiesFor`. Reading is not a capability. A plug with `covered` empty gets readings only, also for a concept that has a capability table. A plug that names a capability still needs the table. Falsifier: `[0x0103]` with `covered: &[]` → `Ok` and `read_mode_for(0x0103) == PLUG_AND_PLAY_V3`; with `covered: &["anything"]` → refused as today. ## Open questions (operator) diff --git a/crates/lance-graph-glove-parity/src/basis.rs b/crates/lance-graph-glove-parity/src/basis.rs index e3b9aeee9..a40bcb219 100644 --- a/crates/lance-graph-glove-parity/src/basis.rs +++ b/crates/lance-graph-glove-parity/src/basis.rs @@ -28,7 +28,8 @@ use lance_graph_ogar::OgarClassView; pub const CONCEPTS: &[u16] = &[BILLABLE_WORK_ENTRY]; /// SAP CATS. No capabilities are covered: the authority declares none on -/// `0x0103` yet, so activation refuses with `NoCapabilitiesFor`. +/// `0x0103` yet, so the plug asks for a reading only and activates with it +/// (D-HPR-8: reading is not a capability). pub const SAP_HOT_PLUG: HotPlug = HotPlug { consumer: "sap-glove", classids: CONCEPTS, diff --git a/crates/lance-graph-glove-parity/tests/authority.rs b/crates/lance-graph-glove-parity/tests/authority.rs index 4efa2a225..1bf0d2476 100644 --- a/crates/lance-graph-glove-parity/tests/authority.rs +++ b/crates/lance-graph-glove-parity/tests/authority.rs @@ -1,5 +1,6 @@ //! D-XGP-2: the hot-plugs and the canonical basis, against the authority. -use lance_graph_contract::hotplug::{ActivationDrift, CapabilityAuthority}; +use lance_graph_contract::canonical_node::ReadMode; +use lance_graph_contract::hotplug::{ActivationDrift, CapabilityAuthority, HotPlug}; use lance_graph_glove_parity::basis::{ canonical_fields, Grade, ANCHORS, ODOO_FIELDS, ODOO_HOT_PLUG, SAP_FIELDS, SAP_HOT_PLUG, WOA_FIELDS, WOA_HOURS_TABLE, WOA_PINNED_TABLE, @@ -8,11 +9,29 @@ use lance_graph_ogar::ogar_vocab::class_ids::BILLABLE_WORK_ENTRY; use lance_graph_ogar::OgarAuthority; use lance_graph_sap::schema::FIELDS; +/// D-HPR-8: both gloves cover no capability, so they plug `0x0103` for its +/// reading alone and activate, although OGAR declares no capabilities on it. #[test] -fn both_plugs_refuse_until_the_authority_declares_capabilities_on_0x0103() { +fn both_plugs_activate_for_their_reading_while_0x0103_has_no_capabilities() { for plug in [SAP_HOT_PLUG, ODOO_HOT_PLUG] { + let act = OgarAuthority + .activate(&plug) + .unwrap_or_else(|d| panic!("{}: {d:?}", plug.consumer)); assert_eq!( - OgarAuthority.activate(&plug).map(|_| ()), + act.read_mode_for(BILLABLE_WORK_ENTRY), + Ok(ReadMode::PLUG_AND_PLAY_V3), + "{}", + plug.consumer + ); + assert!(act.capabilities.is_empty(), "{}", plug.consumer); + // Twin: claiming a capability still goes through the join, which has + // no table for 0x0103. When OGAR declares one, write `covered`. + let claiming = HotPlug { + covered: &["log_time"], + ..plug + }; + assert_eq!( + OgarAuthority.activate(&claiming).map(|_| ()), Err(ActivationDrift::NoCapabilitiesFor(BILLABLE_WORK_ENTRY)), "{}: OGAR now declares capabilities on 0x0103; write `covered`", plug.consumer diff --git a/crates/lance-graph-ogar/src/lib.rs b/crates/lance-graph-ogar/src/lib.rs index 43bb61cb2..de317bd5c 100644 --- a/crates/lance-graph-ogar/src/lib.rs +++ b/crates/lance-graph-ogar/src/lib.rs @@ -617,6 +617,32 @@ impl lance_graph_contract::hotplug::CapabilityAuthority for OgarAuthority { return Err(ActivationDrift::UnexpectedConsumer(plug.consumer.into())); } + // D-HPR-8 (decided 2026-10-11): a plug that covers no capability asks + // for a reading only, and reading is not a capability. So the join is + // not consulted: a concept with no action table, or a table whose + // executor list does not name this consumer, cannot refuse it. The + // ids still have to be real. Palette seats and canon storage domains + // keep their routing above (no concept row, as before); every other + // id must be a minted concept, and its row goes through the mirror + // check. A plug that names any capability falls through to the join + // unchanged. + if plug.covered.is_empty() { + let mut concepts = Vec::with_capacity(capability.len()); + for &id in &capability { + match ogar_vocab::class_ids::ALL + .iter() + .find(|&&(_, cid)| cid == id) + { + Some(&(name, cid)) => concepts.push((name.to_string(), cid)), + None => return Err(ActivationDrift::UnknownClassid(id)), + } + } + if let Some(drift) = lance_graph_contract::hotplug::verify_against_mirror(&concepts) { + return Err(drift); + } + return Ok(Activation::new(concepts, Vec::new(), plug_readings(plug))); + } + // Pure-palette plug: no capability ids at all, so the join is not // consulted (it is pinned to refuse `0x17XX`). if !palette.is_empty() && capability.is_empty() { @@ -1001,6 +1027,71 @@ mod plug_and_play_reading { )); } + /// D-HPR-8 CAN FIRE: a plug that covers no capability asks for a reading + /// only, and gets one — for a concept that has no capability table + /// (`0x0103`, the SAP/Odoo glove plugs) and for one that has a table it + /// does not serve (`0x0901`, whose healthcare table expects only + /// `medcare-rs`). Reading is not a capability, so neither the missing table + /// nor the executor list stands in the way. + #[test] + fn a_plug_covering_nothing_activates_for_its_reading_only() { + let plugs = [ + ( + HotPlug { + consumer: "sap-glove", + classids: &[0x0103], + covered: &[], + }, + 0x0103u16, + "billable_work_entry", + ), + ( + HotPlug { + consumer: "some-reader", + classids: &[0x0901], + covered: &[], + }, + 0x0901, + "patient", + ), + ]; + for (plug, id, name) in plugs { + let act = super::OgarAuthority.activate(&plug).unwrap_or_else(|d| { + panic!("{} reading-only plug of {id:#06x}: {d:?}", plug.consumer) + }); + assert_eq!(act.read_mode_for(id), Ok(ReadMode::PLUG_AND_PLAY_V3)); + // Concept rows still come from the codebook, so the mirror check + // keeps running; capabilities are empty because none were asked. + assert_eq!(act.concepts, vec![(name.to_string(), id)]); + assert!(act.capabilities.is_empty()); + } + } + + /// D-HPR-8 CAN STAY SILENT: the moment a plug names a capability it goes + /// through the join exactly as before. `0x0103` still has no table, and a + /// non-executor covering a healthcare capability is still an impostor. + #[test] + fn a_plug_that_covers_a_capability_is_still_refused_without_its_table() { + let sap_claiming = HotPlug { + consumer: "sap-glove", + classids: &[0x0103], + covered: &["anything"], + }; + assert_eq!( + super::OgarAuthority.activate(&sap_claiming), + Err(ActivationDrift::NoCapabilitiesFor(0x0103)) + ); + let impostor = HotPlug { + consumer: "some-reader", + classids: &[0x0901], + covered: &["register_patient"], + }; + assert!(matches!( + super::OgarAuthority.activate(&impostor), + Err(ActivationDrift::UnexpectedConsumer(c)) if c == "some-reader" + )); + } + /// CAN STAY SILENT: an UNPLUGGED concept still bangs. /// /// "V3 for all appids in plug-and-play" must not become "V3 for From 6d27ad0a0fea3d9479c97af6849dffabb65ebf53 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 08:03:21 +0000 Subject: [PATCH 2/2] lance-graph-ogar: storage readings leave the concept key (D-HPR-7) concept_override no longer maps the five canon domains (0x0701, 0x0A01, 0x0E01, 0x0101, 0x0202) to their storage models. Three of these u16s are also consumer appid heads, so any consumer that plugged the shared concept got one app's storage model (O3; option (a) decided by the operator 2026-10-11). The domain models differed from PLUG_AND_PLAY_V3 only in value_schema, which the slab's SlabDeclaration already overrides, so the domain's own slab restores its model. Reading-only routing for these ids is unchanged. Tests: the D-HPR-2 pin is re-pinned. Each domain now reads plug-and-play, and resolve_for_context with the domain's slab declaration equals classid_read_mode of its *_V3 key. Both re-pinned tests failed before the change. lance-graph-ogar and lance-graph-glove-parity pass; fmt clean; clippy unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019fbZ92dFKQ5BJHrA3YcH9A --- .claude/board/STATUS_BOARD.md | 4 +- .../plans/v3-mandatory-hotplug-reading-v1.md | 2 +- crates/lance-graph-ogar/src/lib.rs | 81 ++++++++++--------- 3 files changed, 48 insertions(+), 39 deletions(-) diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 1ffd7a149..6210a297c 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -24,8 +24,8 @@ Plan: `.claude/plans/v3-mandatory-hotplug-reading-v1.md`. | **D-HPR-4** | `0x1000` / render prefixes documented as deprecated, kept (no eviction) | Shipped for `0x1000` (#1464; doc notes on `CLASSID_*_V3` + `classid_read_mode`); render prefixes, `NodeGuid::read_mode` and OGAR/q2 sites still carry no note | — | | **D-HPR-5** | Stored legacy keys keep resolving; a `SlabDeclaration` may override for its own data; no forced removal | Holds today (status quo). Caveat: a `SlabDeclaration` overrides `value_schema` only, never the tail, so it cannot protect a V1/V2-tail slab (`hotplug.rs:231-257, 392`) | — | | **D-HPR-6** | No V1 default for an unknown classid; `guid-v3-tail` not a gate | Queued — split: **6a** un-gate `guid-v3-tail` (default-on, and no fleet consumer disables it, so only `--no-default-features` builds change; there the 5 V3 fossils read `DEFAULT` today); **6b** unknown classid → error. Decided 2026-10-11: go, staged after 6a, D-HPR-3 and a stored-classid check | unknown classid → `NoReadingFor` | -| **D-HPR-7** | Storage readings leave the concept key: out of `concept_override`, into the slab's `SlabDeclaration` or the owning consumer's plug (O3, decided (a) 2026-10-11) | Queued | a non-Odoo plug of `0x0202` reads `PLUG_AND_PLAY_V3`; Odoo's slab still reads `ERP_V3` | -| **D-HPR-8** | A plug that asks only for a reading succeeds (reading is not a capability; decided 2026-10-11) | In PR (`OgarAuthority::activate` skips the capability join when `covered` is empty; ids still must be minted; red before the fix with `NoCapabilitiesFor(0x0103)`; disable run turns the twin red) | `[0x0103]` with `covered: &[]` → `Ok`, `PLUG_AND_PLAY_V3`; with a covered capability → refused as today | +| **D-HPR-7** | Storage readings leave the concept key: out of `concept_override`, into the slab's `SlabDeclaration` or the owning consumer's plug (O3, decided (a) 2026-10-11) | In PR (#1474; the five domain arms removed; the domain models differed from plug-and-play only in `value_schema`, which `SlabDeclaration` overrides; tests red before, green after) | a non-Odoo plug of `0x0202` reads `PLUG_AND_PLAY_V3`; Odoo's slab still reads `ERP_V3` | +| **D-HPR-8** | A plug that asks only for a reading succeeds (reading is not a capability; decided 2026-10-11) | In PR (#1474; `OgarAuthority::activate` skips the capability join when `covered` is empty; ids still must be minted; red before the fix with `NoCapabilitiesFor(0x0103)`; disable run turns the twin red) | `[0x0103]` with `covered: &[]` → `Ok`, `PLUG_AND_PLAY_V3`; with a covered capability → refused as today | ## D-XGP — Cross-glove business parity: Odoo × SAP over one Quack algebra (2026-10-10) diff --git a/.claude/plans/v3-mandatory-hotplug-reading-v1.md b/.claude/plans/v3-mandatory-hotplug-reading-v1.md index fad2df731..3c767763b 100644 --- a/.claude/plans/v3-mandatory-hotplug-reading-v1.md +++ b/.claude/plans/v3-mandatory-hotplug-reading-v1.md @@ -63,7 +63,7 @@ What it means, in rules: - [ ] **W6 — D-HPR-6: V1 cannot be reached by default.** `ReadMode::DEFAULT` stops being the fallback for an unknown classid (it becomes `NoReadingFor`, as on the plug path), and `guid-v3-tail` stops being a feature gate. - Split (2026-10-11). **W6a**: un-gate `guid-v3-tail`. It is default-on and no fleet consumer disables it, so only `--no-default-features` builds change; there the 5 V3 fossils read `DEFAULT` today (coresearch probe P-X3). **W6b**: an unknown classid errors. This is a behaviour change, and both q2 and MedCare have already hit the V1 default once, so it needs operator approval. - **Decided (operator, 2026-10-11: "Recommended accepted"): go, staged.** 1. W6a. 2. W3 moves the remaining mint paths onto plug readings. 3. A check that every stored artifact's classid resolves. 4. Only then W6b. -- [ ] **W7 — D-HPR-7: storage readings leave the concept key** (O3, decided (a)). The five domain storage readings move out of `concept_override` (keyed by concept) into the slab's `SlabDeclaration` or the owning consumer's plug. A consumer plugging a shared concept then reads `PLUG_AND_PLAY_V3`. Falsifier: a plug of `0x0202` by a non-Odoo consumer reads `PLUG_AND_PLAY_V3`, and Odoo's own slab still reads `ERP_V3`. +- [x] **W7 — D-HPR-7: storage readings leave the concept key** (O3, decided (a)). **Shipped (#1474):** the five domain arms are removed from `concept_override`. The domain models differed from `PLUG_AND_PLAY_V3` only in `value_schema`, which a `SlabDeclaration` overrides, so no new mechanism was needed. Test `canon_domains_read_plug_and_play_and_their_slab_restores_the_domain_model`. The five domain storage readings move out of `concept_override` (keyed by concept) into the slab's `SlabDeclaration` or the owning consumer's plug. A consumer plugging a shared concept then reads `PLUG_AND_PLAY_V3`. Falsifier: a plug of `0x0202` by a non-Odoo consumer reads `PLUG_AND_PLAY_V3`, and Odoo's own slab still reads `ERP_V3`. - [x] **W8 — D-HPR-8: a plug that asks only for a reading succeeds** (decided). **Shipped:** `lance-graph-ogar` `OgarAuthority::activate`, tests `a_plug_covering_nothing_activates_for_its_reading_only` (SAP glove on `0x0103`, a non-executor on `0x0901`) and `a_plug_that_covers_a_capability_is_still_refused_without_its_table`. The glove-parity pin now says both glove plugs activate for their reading; claiming a capability on `0x0103` is still `NoCapabilitiesFor`. Reading is not a capability. A plug with `covered` empty gets readings only, also for a concept that has a capability table. A plug that names a capability still needs the table. Falsifier: `[0x0103]` with `covered: &[]` → `Ok` and `read_mode_for(0x0103) == PLUG_AND_PLAY_V3`; with `covered: &["anything"]` → refused as today. ## Open questions (operator) diff --git a/crates/lance-graph-ogar/src/lib.rs b/crates/lance-graph-ogar/src/lib.rs index de317bd5c..34ae31f29 100644 --- a/crates/lance-graph-ogar/src/lib.rs +++ b/crates/lance-graph-ogar/src/lib.rs @@ -480,16 +480,13 @@ const fn concept_override(classid: u16) -> Option { value_schema: ValueSchema::Bootstrap, edge_codec: EdgeCodecFlavor::CoarseOnly, }), - // The canon domains' value models, keyed by concept (D-HPR-2, plan - // `v3-mandatory-hotplug-reading-v1`). The same readings - // `classid_read_mode` gives their `CLASSID_*_V3` keys, so a consumer - // plugging one of these concepts reads what the classid table reads, - // with no classview involved. - 0x0701 => Some(ReadMode::OSINT_V3), - 0x0A01 => Some(ReadMode::FMA_V3), - 0x0E01 => Some(ReadMode::CPIC_V3), - 0x0101 => Some(ReadMode::PROJECT_V3), - 0x0202 => Some(ReadMode::ERP_V3), + // The five canon domains (0x0701, 0x0A01, 0x0E01, 0x0101, 0x0202) + // carried their storage models here under D-HPR-2. They were removed + // by D-HPR-7 (decided 2026-10-11): three of those u16s are also + // consumer appid heads, so any consumer plugging the shared concept + // got one app's storage model. A domain's model now comes from the + // slab holding its data (`SlabDeclaration`, which overrides exactly + // the value schema in which these readings differed). _ => None, } } @@ -505,9 +502,11 @@ const fn is_palette_seat(classid: u16) -> bool { classid >= 0x1717 && (classid >> 8) == 0x17 } -/// `true` for the five canon storage domains whose reading lives in -/// [`concept_override`] (D-HPR-2): OSINT `0x0701`, FMA `0x0A01`, CPIC -/// `0x0E01`, PROJECT `0x0101`, ERP `0x0202`. +/// `true` for the five canon storage domains: OSINT `0x0701`, FMA `0x0A01`, +/// CPIC `0x0E01`, PROJECT `0x0101`, ERP `0x0202`. Only their ROUTING is special +/// (reading-only when the join refuses them); since D-HPR-7 they read +/// plug-and-play, and a domain's storage model comes from its slab's +/// `SlabDeclaration`. const fn is_canon_storage_domain(classid: u16) -> bool { matches!(classid, 0x0701 | 0x0A01 | 0x0E01 | 0x0101 | 0x0202) } @@ -846,11 +845,18 @@ mod plug_and_play_reading { assert!(super::OgarAuthority.activate(&BLOCKLY).is_ok()); } - /// D-HPR-2: the canon domains' readings come from the plug, keyed by - /// concept, and equal what `classid_read_mode` gives their `*_V3` keys. + /// D-HPR-7 (decided 2026-10-11): plugging a canon domain's concept reads + /// plug-and-play, like any other plugged concept. The domain's own storage + /// model belongs to the slab that holds its data, so the slab's + /// declaration restores it, and then it equals what `classid_read_mode` + /// gives the domain's `*_V3` key. Reading-only routing (D-HPR-2's codex P1 + /// fix) is unchanged: the plug still activates although the join refuses + /// every one of these ids. #[test] - fn canon_domains_read_from_the_plug_as_the_classid_table_reads_them() { - use lance_graph_contract::canonical_node::NodeGuid; + fn canon_domains_read_plug_and_play_and_their_slab_restores_the_domain_model() { + use lance_graph_contract::canonical_node::{NodeGuid, ValueSchema}; + use lance_graph_contract::hotplug::{SlabDeclaration, SlabReading}; + use lance_graph_contract::soa_envelope::ENVELOPE_LAYOUT_VERSION; let domains = [ (0x0701u16, NodeGuid::CLASSID_OSINT_V3), (0x0A01, NodeGuid::CLASSID_FMA_V3), @@ -863,38 +869,39 @@ mod plug_and_play_reading { classids: &[0x0701, 0x0A01, 0x0E01, 0x0101, 0x0202], covered: &[], }; - // Through the public path, not the private helper (codex P1 on - // #1465): every one of these ids is refused by the capability join, - // so a reading reachable only via `plug_readings` would be dead. let act = super::OgarAuthority .activate(&plug) .expect("a canon-domain plug activates for its reading"); - let readings = act.declared_readings().to_vec(); for (concept, classid) in domains { + assert_eq!(classid >> 16, u32::from(concept), "fixture pairs"); assert_eq!( - classid >> 16, - u32::from(concept), - "fixture pairs concept and classid" + act.read_mode_for(concept), + Ok(ReadMode::PLUG_AND_PLAY_V3), + "0x{concept:04X}: the concept alone carries no storage model" ); - let from_plug = readings - .iter() - .find_map(|(c, m)| (*c == concept).then_some(*m)) - .expect("every plugged concept reads"); - assert_eq!(from_plug, classid_read_mode(classid), "0x{concept:04X}"); - assert_eq!(from_plug.tail_variant, TailVariant::V3); + let stored = classid_read_mode(classid); + let slab = SlabDeclaration { + reading: SlabReading::Facet96, + value_schema: stored.value_schema, + layout_version: ENVELOPE_LAYOUT_VERSION, + }; + let resolved = act + .resolve_for_context(concept, Some(&slab)) + .expect("a declared slab resolves"); + assert_eq!(resolved.read_mode, stored, "0x{concept:04X}"); } - // Anti-vacuity: the domains do not all read the plug-and-play default, - // so the overrides are what this test checks. - assert!(domains.iter().any(|(c, _)| readings + // Anti-vacuity: the stored domain models are not all plug-and-play, + // so the slab declaration is what restores them. + assert!(domains .iter() - .any(|(rc, m)| rc == c && *m != ReadMode::PLUG_AND_PLAY_V3))); + .any(|(_, c)| classid_read_mode(*c).value_schema != ValueSchema::Full)); } /// A canon storage domain plugged alongside capability ids: the domain /// gets its reading without reaching the join, and the capability half /// still resolves (codex P1 on #1465). #[test] - fn a_canon_domain_next_to_capability_ids_activates_and_reads_its_domain() { + fn a_canon_domain_next_to_capability_ids_activates_and_reads_plug_and_play() { let mixed = HotPlug { consumer: "medcare-rs", classids: &[0x0A01, 0x0901, 0x0902], @@ -912,7 +919,9 @@ mod plug_and_play_reading { let act = super::OgarAuthority .activate(&mixed) .expect("the canon domain must not make the plug refuse"); - assert_eq!(act.read_mode_for(0x0A01), Ok(ReadMode::FMA_V3)); + // D-HPR-7: the concept alone reads plug-and-play; FMA's Compressed + // model comes from its slab's declaration. + assert_eq!(act.read_mode_for(0x0A01), Ok(ReadMode::PLUG_AND_PLAY_V3)); assert_eq!(act.read_mode_for(0x0901), Ok(ReadMode::PLUG_AND_PLAY_V3)); assert!(!act.capabilities.is_empty()); assert!(act.concepts.iter().all(|(_, id)| *id != 0x0A01));