Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude/board/STATUS_BOARD.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) | Queued | `[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)

Expand Down
4 changes: 2 additions & 2 deletions .claude/plans/v3-mandatory-hotplug-reading-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ 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`.
- [ ] **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] **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)

Expand Down
3 changes: 2 additions & 1 deletion crates/lance-graph-glove-parity/src/basis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
25 changes: 22 additions & 3 deletions crates/lance-graph-glove-parity/tests/authority.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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
Expand Down
172 changes: 136 additions & 36 deletions crates/lance-graph-ogar/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -480,16 +480,13 @@ const fn concept_override(classid: u16) -> Option<ReadMode> {
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,
}
}
Expand All @@ -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)
}
Expand Down Expand Up @@ -617,6 +616,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() {
Expand Down Expand Up @@ -820,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),
Expand All @@ -837,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],
Expand All @@ -886,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));
Expand Down Expand Up @@ -1001,6 +1036,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
Expand Down
Loading