diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 588d324bc..9c4dbfd94 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -4,11 +4,11 @@ Plan: `.claude/plans/v3-mandatory-hotplug-reading-v1.md`. | D-id | Deliverable | Status | Gate | |---|---|---|---| -| **D-HPR-0** | Ruling + plan + caller inventory | In PR (#1464) | — | -| **D-HPR-1** | Contract modules (`ocr`, `aiwar`, `nan_projection`, `soa_graph`) take the reading as a parameter | In PR (#1464; 4 `_with`/`_plugged` forms, disable runs 4/4 red) | new form returns the handed reading even where `classid_read_mode` disagrees | -| **D-HPR-2** | Canon domain readings move into `OgarAuthority::concept_override` | Queued | `read_mode_for(concept)` == today's `classid_read_mode()` per domain | +| **D-HPR-0** | Ruling + plan + caller inventory | Shipped (#1464) | — | +| **D-HPR-1** | Contract modules (`ocr`, `aiwar`, `nan_projection`, `soa_graph`) take the reading as a parameter | Shipped (#1464; 4 `_with`/`_plugged` forms, disable runs 4/4 red) | new form returns the handed reading even where `classid_read_mode` disagrees | +| **D-HPR-2** | Canon domain readings move into `OgarAuthority::concept_override` | In PR (5 concept arms; red with an arm removed) | `read_mode_for(concept)` == today's `classid_read_mode()` per domain | | **D-HPR-3** | Consumers move (lance-graph, q2, MedCare); `classid_read_mode` deprecated | Queued | no production caller left | -| **D-HPR-4** | `0x1000` / render prefixes documented as deprecated, kept (no eviction) | In PR (#1464; doc notes on `CLASSID_*_V3` + `classid_read_mode`) | — | +| **D-HPR-4** | `0x1000` / render prefixes documented as deprecated, kept (no eviction) | Shipped (#1464; doc notes on `CLASSID_*_V3` + `classid_read_mode`) | — | | **D-HPR-5** | Stored legacy keys keep resolving; a `SlabDeclaration` may override for its own data; no forced removal | Queued | — | | **D-HPR-6** | No V1 default for an unknown classid; `guid-v3-tail` not a gate | Queued | unknown classid → `NoReadingFor` | diff --git a/.claude/plans/v3-mandatory-hotplug-reading-v1.md b/.claude/plans/v3-mandatory-hotplug-reading-v1.md index b1557d0eb..11d86cf64 100644 --- a/.claude/plans/v3-mandatory-hotplug-reading-v1.md +++ b/.claude/plans/v3-mandatory-hotplug-reading-v1.md @@ -52,7 +52,8 @@ What it means, in rules: - [x] **W1 — D-HPR-1: contract takes the reading as a parameter.** `ocr`, `aiwar`, `nan_projection`, `soa_graph` get variants that take a `ReadMode` (or a `ResolvedReading`) instead of looking the classid up. `DomainSpec` carries its reading. The classid-lookup forms stay, documented as legacy, until W3. Additive: no consumer breaks. - Falsifier: a test per module that the new form yields the reading it was handed, including one where the handed reading differs from what `classid_read_mode` would return (so a caller that silently re-looks-up fails). - **Shipped:** `ocr::LayoutBlock::to_node_row_with`, `aiwar::aiwar_node_rows_with`, `soa_graph::{project_snapshot_with, nearest_anchor_with}`, `nan_projection::project_energy_nonfinite_plugged` (resolves each run through `Activation::resolve_tenant_reading`; an unplugged concept is `NoReadingFor`). Tests use classid `0x1718_0000`, which the lookup reads as V1 / `Full`; each is red when its form is changed to re-look-up (disable runs, 4/4). -- [ ] **W2 — D-HPR-2: the canon domains get their reading from the authority.** The domain value models (`OSINT`/`PROJECT`/`ERP` Cognitive, `FMA`/`CPIC` Compressed) move into `concept_override`, keyed by concept, all on a V3 tail. `OgarAuthority` then answers for those concepts with no classview involved. +- [x] **W2 — D-HPR-2: the canon domains get their reading from the authority.** The domain value models (`OSINT`/`PROJECT`/`ERP` Cognitive, `FMA`/`CPIC` Compressed) move into `concept_override`, keyed by concept, all on a V3 tail. `OgarAuthority` then answers for those concepts with no classview involved. + - **Shipped:** `lance-graph-ogar::concept_override` arms `0x0701`/`0x0A01`/`0x0E01`/`0x0101`/`0x0202` → `ReadMode::{OSINT,FMA,CPIC,PROJECT,ERP}_V3`. Test `canon_domains_read_from_the_plug_as_the_classid_table_reads_them` pins each equal to `classid_read_mode(CLASSID_*_V3)`; red with an arm removed. No consumer plugs these concepts today (`grep` of every `HotPlug` in lance-graph, OGAR, MedCare-rs, q2, odoo-rs, openproject-nexgen-rs, HubSPO-rs, spear), so no live reading changes. - Falsifier: `read_mode_for(concept)` equals today's `classid_read_mode()` for each canon domain, so the move changes no reading. - [ ] **W3 — D-HPR-3: consumers move.** lance-graph callcenter / deepnsm-v2 / probes, then q2 osint-bake + geo, MedCare cohorts. Each takes its reading from its `Activation`. Then `classid_read_mode` gets `#[deprecated]`. - [x] **W4 — D-HPR-4: document the fossils as deprecated.** Done in `lance-graph-contract`: the five `CLASSID_*_V3` constants and `classid_read_mode` carry a "Deprecated, kept" doc note. A doc note, not `#[deprecated]`: the attribute would turn every existing caller red under `-D warnings`, which is the forced migration the ruling rules out. OGAR (`ogar-osm` `CLASSVIEW_V3_SUBSTRATE`, ro, dismech, loco) and q2 `geo` get the same note when next touched; existing mints stay. diff --git a/crates/lance-graph-ogar/src/lib.rs b/crates/lance-graph-ogar/src/lib.rs index e76659c12..43bb61cb2 100644 --- a/crates/lance-graph-ogar/src/lib.rs +++ b/crates/lance-graph-ogar/src/lib.rs @@ -480,6 +480,16 @@ 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), _ => None, } } @@ -495,6 +505,29 @@ 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`. +const fn is_canon_storage_domain(classid: u16) -> bool { + matches!(classid, 0x0701 | 0x0A01 | 0x0E01 | 0x0101 | 0x0202) +} + +/// A canon storage domain that the capability join does not serve, so it is +/// plugged for its reading alone (codex P1 on #1465). The join answers +/// `UnknownClassid` for a concept outside the capability codebook and +/// `NoCapabilitiesFor` for one with no action table; either way there is no +/// capability side, and routing it to the join would refuse the whole plug. +/// Probed per id rather than listed, so a domain that later gains actions +/// goes back through the join and its capabilities are not dropped. +fn is_reading_only_canon_domain(consumer: &str, classid: u16) -> bool { + use ogar_vocab::capability_registry::{resolve_hotplug, HotplugDrift}; + is_canon_storage_domain(classid) + && matches!( + resolve_hotplug(consumer, &[classid], &[]), + Err(HotplugDrift::UnknownClassid(_) | HotplugDrift::NoCapabilitiesFor(_)) + ) +} + /// The consumer a palette seat is KNOWN to belong to, when one is known. /// /// Deliberately a short list of CLAIMED seats, not a roster of participants. @@ -571,8 +604,12 @@ impl lance_graph_contract::hotplug::CapabilityAuthority for OgarAuthority { // The two id kinds answer to different authorities, so they are // routed separately and the results merged; the reading covers the // whole plug either way, because it is derived from the plug. - let (palette, capability): (Vec, Vec) = - plug.classids.iter().partition(|&&id| is_palette_seat(id)); + // Canon storage domains with no capability side are reading-only too + // (codex P1 on #1465): the join refuses them, so they are routed with + // the palette seats and get their reading from `plug_readings`. + let (palette, capability): (Vec, Vec) = plug.classids.iter().partition(|&&id| { + is_palette_seat(id) || is_reading_only_canon_domain(plug.consumer, id) + }); // A CLAIMED seat is its owner's, whichever arm it arrives on. An // unclaimed one is plug-and-play for whoever plugs it. @@ -783,6 +820,78 @@ 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. + #[test] + fn canon_domains_read_from_the_plug_as_the_classid_table_reads_them() { + use lance_graph_contract::canonical_node::NodeGuid; + let domains = [ + (0x0701u16, NodeGuid::CLASSID_OSINT_V3), + (0x0A01, NodeGuid::CLASSID_FMA_V3), + (0x0E01, NodeGuid::CLASSID_CPIC_V3), + (0x0101, NodeGuid::CLASSID_PROJECT_V3), + (0x0202, NodeGuid::CLASSID_ERP_V3), + ]; + let plug = HotPlug { + consumer: "canon-domains", + 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 concept and classid" + ); + 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); + } + // 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 + .iter() + .any(|(rc, m)| rc == c && *m != ReadMode::PLUG_AND_PLAY_V3))); + } + + /// 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() { + let mixed = HotPlug { + consumer: "medcare-rs", + classids: &[0x0A01, 0x0901, 0x0902], + covered: &[ + "register_patient", + "get_patient_record", + "list_patients", + "update_patient_access", + "add_diagnosis", + "get_diagnosis", + "list_diagnoses", + "delete_diagnosis", + ], + }; + 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)); + 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)); + } + /// THE MEDCARE CASE — the one this rewrite exists for. /// /// A capability consumer outside `0x17XX` is as plugged in as a palette,