diff --git a/.claude/board/ISSUES.md b/.claude/board/ISSUES.md index 3f1693ac..2274e8e1 100644 --- a/.claude/board/ISSUES.md +++ b/.claude/board/ISSUES.md @@ -1,5 +1,40 @@ # Issues Log — Open + Resolved (double-entry, append-only) +## 2026-07-08 — ISS-V1-TAIL-RESIDUE — woa-rs arm — RESOLVED (`make_account_guid_bytes` migrated to the V3 tail) + +**Status:** RESOLVED (operator ruling 2026-07-08, landed in woa-rs — sibling repo, +not this branch). This is a THIRD residue arm under the `ISS-V1-TAIL-RESIDUE` +umbrella, alongside the two lance-graph-contract mint sites (`ocr.rs`/`aiwar.rs`, +resolved 2026-07-07 below) — woa-rs's ERP account-GUID minter carried its own +independent V1-tail residue (`family` hash stuffing the Personenkonten trailing +digits) that the lance-graph-side fix did not touch. + +**Landed:** woa-rs `src/erp/canon.rs::make_account_guid_bytes` now produces +`leaf(u16)/family(u16)/identity(u16)` byte-identical to `NodeGuid::new_v2`/ +`mint_for(TailVariant::V3, …)` — the V1→V3 semantic move: the Personenkonten +trailing digits (SKR03 70000-99999) that V1 stuffed into the `family` **hash** +now live in the **LEAF tier** as a real `(ten_bucket:final_digit)` `(part_of:is_a)` +rail (`skr_leaf()` decomposes `70123` → LEAF `(2,3)`), matching the same +canonical `HEEL·HIP·TWIG·LEAF·family·identity` cascade shape this branch's +`mint_for` dispatch uses. **Parallelbetrieb invariant pinned** (doc block +"READ THIS FIRST" in `canon.rs`): the MySQL ORM mapping stays authoritative — +`identity` = the MySQL `erp_accounts` row id, converted `u16`-by-signature with +a loud `try_from` (`.expect("Parallelbetrieb: erp_accounts row id must …")`) at +every call site, never a silent `as` alias/truncation. **11/11 tests green** in +woa-rs (`src/erp/canon.rs` — 4-digit accounts, Personenkonten LEAF decompose, +round-trip byte layout, MySQL-id round-trip). + +**Scope note:** woa-rs is a sibling repo (not this lance-graph worktree) — +this entry is board-hygiene bookkeeping only, landed same-commit as any other +ISSUES.md bookkeeping in this session per the V3 plan's standing gate 3 +("board hygiene same-commit"). No lance-graph-contract code changed for this +arm. + +Cross-ref: woa-rs `src/erp/canon.rs` (`make_account_guid_bytes`, `skr_leaf`); +this file's `ISS-V1-TAIL-RESIDUE` 2026-07-07 / 2026-07-04 entries (the +lance-graph-contract arms of the same residue umbrella); `.claude/v3/INTEGRATION-PLAN.md` +standing gate 3. + ## 2026-07-07 — ISS-V1-TAIL-RESIDUE — RESOLVED (un-gate + default-on + both mint sites V3-routed; aiwar mints real V3) **Status:** RESOLVED. Landed in one PR (#663): diff --git a/crates/causal-edge/src/edge.rs b/crates/causal-edge/src/edge.rs index e5b1abc3..02aeb407 100644 --- a/crates/causal-edge/src/edge.rs +++ b/crates/causal-edge/src/edge.rs @@ -30,6 +30,10 @@ pub enum InferenceType { } impl InferenceType { + // Used only on the v1 (non-`causal-edge-v2-layout`) read path; under v2 the + // 4-bit signed mantissa decode (`from_mantissa`) supersedes it. Dead under + // the default v2 feature, live under `--no-default-features`. + #[allow(dead_code)] #[inline] fn from_bits(v: u8) -> Self { match v & 0b111 { @@ -165,6 +169,9 @@ const CONF_SHIFT: u32 = 32; const CAUSAL_SHIFT: u32 = 40; const DIR_SHIFT: u32 = 43; const INFER_SHIFT: u32 = 46; +// v1 plasticity bit position; under v2 plasticity moves to `crate::layout::PLAST_SHIFT`. +// Live on the v1 pack/read paths, dead under the default v2 feature. +#[allow(dead_code)] const PLAST_SHIFT: u32 = 49; const TEMPORAL_SHIFT: u32 = 52; @@ -639,6 +646,7 @@ impl CausalEdge64 { // weight.inference_type() is the v1 fallback below; v2 uses mantissa let resolved_infer = InferenceType::from_mantissa(weight.inference_mantissa()); #[cfg(not(feature = "causal-edge-v2-layout"))] + #[allow(deprecated)] // v1 layout: 3-bit unsigned inference type is the canonical read let resolved_infer = weight.inference_type(); let (f_out, c_out) = match resolved_infer { InferenceType::Deduction => { @@ -679,7 +687,8 @@ impl CausalEdge64 { let mask_out = CausalMask::from_bits((self.causal_mask() as u8) & (weight.causal_mask() as u8)); - // 4. Temporal: latest of the two + // 4. Temporal: latest of the two (v1 read; the pack() below drops it under v2) + #[allow(deprecated)] let t_out = self.temporal().max(weight.temporal()); // 5. Inherit plasticity from weight (the "learned" edge) @@ -1082,6 +1091,9 @@ impl CausalEdge64 { } impl std::fmt::Debug for CausalEdge64 { + // Debug prints the v1 views (`inference_type`/`temporal`) for readability; under + // v2 these are deprecated accessors but harmless for a diagnostic dump. + #[allow(deprecated)] fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("CausalEdge64") .field("spo", &(self.s_idx(), self.p_idx(), self.o_idx())) diff --git a/crates/causal-edge/src/network.rs b/crates/causal-edge/src/network.rs index 8a44e5cf..98e102f7 100644 --- a/crates/causal-edge/src/network.rs +++ b/crates/causal-edge/src/network.rs @@ -156,6 +156,10 @@ impl CausalNetwork { .filter(|e| e.s_idx() == s && e.p_idx() == p && e.o_idx() == o) .collect(); + // v1 temporal ordering; under v2 `temporal()` is deprecated (temporal is + // structural — chain position + Triplet.timestamp — so the sort degrades to + // a stable no-op there). Kept for v1 evidence-trail parity. + #[allow(deprecated)] trail.sort_by_key(|e| e.temporal()); trail } diff --git a/crates/lance-graph-contract/src/canonical_node.rs b/crates/lance-graph-contract/src/canonical_node.rs index 40fca1a9..3fe9375c 100644 --- a/crates/lance-graph-contract/src/canonical_node.rs +++ b/crates/lance-graph-contract/src/canonical_node.rs @@ -156,6 +156,42 @@ impl NodeGuid { #[cfg(feature = "guid-v3-tail")] pub const CLASSID_CPIC_V3_LEGACY: u32 = 0x1000_0E00; + /// **PROJECT-V3** — project-management (OpenProject ↔ Redmine) on a + /// [`TailVariant::V3`] cascade tail, minted for **OpenProject** (appid + /// `0x01` — the consumer's OWN appid, operator ruling 2026-07-08: "consumers + /// own their own appid … they do NOT ride q2's 0x01"). Canon `0x0101` HIGH + /// (project-mgmt domain `0x01`, appid `0x01`); the V3 marker `0x1000` in the + /// LOW/custom u16 — `0x01:01::1000`. Same encoding shape as + /// [`CLASSID_OSINT_V3`] (domain:appid canon-HIGH, fixed `0x1000` marker + /// custom-LOW) — NOT the `render_classid`/`AppPrefix` render-lens layout + /// (concept-HIGH, app-prefix-LOW): the V3 mint tail and the OGAR render + /// lens are two independent composition axes that happen to share the + /// `0x01`/`0x02` app byte values here. + /// [`classid_concept_domain`](crate::ogar_codebook::classid_concept_domain) + /// routes [`ProjectMgmt`](crate::ogar_codebook::ConceptDomain::ProjectMgmt). + /// Resolves to [`ReadMode::PROJECT_V3`] (same hot `Cognitive` model as legacy PROJECT). + #[cfg(feature = "guid-v3-tail")] + pub const CLASSID_PROJECT_V3: u32 = 0x0101_1000; + /// Pre-flip stored form of [`CLASSID_PROJECT_V3`] (marker HIGH, canon + /// `0x0100` LOW — pre-normalization appid `:00`) — read-only legacy alias. + #[cfg(feature = "guid-v3-tail")] + pub const CLASSID_PROJECT_V3_LEGACY: u32 = 0x1000_0100; + + /// **ERP-V3** — commerce/ERP (Odoo ↔ OSB) on a [`TailVariant::V3`] cascade + /// tail, minted for **Odoo** (appid `0x02` — the consumer's OWN appid, same + /// 2026-07-08 ruling as [`CLASSID_PROJECT_V3`]). Canon `0x0202` HIGH + /// (commerce domain `0x02`, appid `0x02`); the V3 marker `0x1000` in the + /// LOW/custom u16 — `0x02:02::1000`. + /// [`classid_concept_domain`](crate::ogar_codebook::classid_concept_domain) + /// routes [`Commerce`](crate::ogar_codebook::ConceptDomain::Commerce). + /// Resolves to [`ReadMode::ERP_V3`] (same hot `Cognitive` model as legacy ERP). + #[cfg(feature = "guid-v3-tail")] + pub const CLASSID_ERP_V3: u32 = 0x0202_1000; + /// Pre-flip stored form of [`CLASSID_ERP_V3`] (marker HIGH, canon `0x0200` + /// LOW — pre-normalization appid `:00`) — read-only legacy alias. + #[cfg(feature = "guid-v3-tail")] + pub const CLASSID_ERP_V3_LEGACY: u32 = 0x1000_0200; + /// Construct from the six canonical groups. `family`/`identity` use their low 3 bytes. /// /// Panics (incl. const-eval) when `family` or `identity` exceed 24 bits — the @@ -1155,6 +1191,26 @@ impl ReadMode { edge_codec: EdgeCodecFlavor::CoarseOnly, }; + /// The **PROJECT-V3** read-mode ([`NodeGuid::CLASSID_PROJECT_V3`]): the same + /// hot [`ValueSchema::Cognitive`] value model as legacy [`PROJECT`](ReadMode::PROJECT), + /// read through the new-generation [`TailVariant::V3`] cascade tail. + #[cfg(feature = "guid-v3-tail")] + pub const PROJECT_V3: ReadMode = ReadMode { + tail_variant: TailVariant::V3, + value_schema: ValueSchema::Cognitive, + edge_codec: EdgeCodecFlavor::CoarseOnly, + }; + + /// The **ERP-V3** read-mode ([`NodeGuid::CLASSID_ERP_V3`]): the same hot + /// [`ValueSchema::Cognitive`] value model as legacy [`ERP`](ReadMode::ERP), + /// read through the new-generation [`TailVariant::V3`] cascade tail. + #[cfg(feature = "guid-v3-tail")] + pub const ERP_V3: ReadMode = ReadMode { + tail_variant: TailVariant::V3, + value_schema: ValueSchema::Cognitive, + edge_codec: EdgeCodecFlavor::CoarseOnly, + }; + /// All three axes are layout-preserving (a tail-variant/preset/flavor /// re-interprets reserved bytes, never a stride change), so adopting any /// read-mode needs no `ENVELOPE_LAYOUT_VERSION` bump. @@ -1218,9 +1274,13 @@ static BUILTIN_READ_MODES: LazyLock> = LazyLock::new(|| { m.insert(NodeGuid::CLASSID_OSINT_V3, ReadMode::OSINT_V3); m.insert(NodeGuid::CLASSID_FMA_V3, ReadMode::FMA_V3); m.insert(NodeGuid::CLASSID_CPIC_V3, ReadMode::CPIC_V3); + m.insert(NodeGuid::CLASSID_PROJECT_V3, ReadMode::PROJECT_V3); + m.insert(NodeGuid::CLASSID_ERP_V3, ReadMode::ERP_V3); m.insert(NodeGuid::CLASSID_OSINT_V3_LEGACY, ReadMode::OSINT_V3); m.insert(NodeGuid::CLASSID_FMA_V3_LEGACY, ReadMode::FMA_V3); m.insert(NodeGuid::CLASSID_CPIC_V3_LEGACY, ReadMode::CPIC_V3); + m.insert(NodeGuid::CLASSID_PROJECT_V3_LEGACY, ReadMode::PROJECT_V3); + m.insert(NodeGuid::CLASSID_ERP_V3_LEGACY, ReadMode::ERP_V3); } m }); diff --git a/crates/lance-graph-contract/src/soa_graph.rs b/crates/lance-graph-contract/src/soa_graph.rs index 52c0b466..56362901 100644 --- a/crates/lance-graph-contract/src/soa_graph.rs +++ b/crates/lance-graph-contract/src/soa_graph.rs @@ -108,8 +108,18 @@ pub const OSINT_GOTHAM: DomainSpec = DomainSpec { /// structural entities, family = body region, `out_family` = part-of. Anchor /// families are the *bones* (the skeleton the soft tissue hangs off); the /// default declares none — a caller supplies the bone families. +/// The FMA domain classid: the V3 anatomy class (`CLASSID_FMA_V3`) in a normal +/// build; the legacy V1 `CLASSID_FMA` only under `--no-default-features`. +/// Matches q2's `osint-bake/body` (`/helix` substrate) which already mints +/// `CLASSID_FMA_V3`. V3 routes on the `from_guid_prefix_v3` fold (classid not +/// folded) — `hhtl_path`/`nearest_anchor` are already tail-aware. +#[cfg(feature = "guid-v3-tail")] +const FMA_ANATOMY_CLASSID: u32 = NodeGuid::CLASSID_FMA_V3; +#[cfg(not(feature = "guid-v3-tail"))] +const FMA_ANATOMY_CLASSID: u32 = NodeGuid::CLASSID_FMA; + pub const FMA_ANATOMY: DomainSpec = DomainSpec { - classid: NodeGuid::CLASSID_FMA, + classid: FMA_ANATOMY_CLASSID, name: "FMA-Anatomy", anchor_families: &[], in_family_edge: "adjacent-to", @@ -117,12 +127,20 @@ pub const FMA_ANATOMY: DomainSpec = DomainSpec { member_edge: "part-of", }; -/// The **project-management** domain (classid [`NodeGuid::CLASSID_PROJECT`], +/// The project-management domain classid: the V3 class +/// [`NodeGuid::CLASSID_PROJECT_V3`] in a normal build; the legacy V1 +/// [`NodeGuid::CLASSID_PROJECT`] only under `--no-default-features`. +#[cfg(feature = "guid-v3-tail")] +const PROJECT_CLASSID: u32 = NodeGuid::CLASSID_PROJECT_V3; +#[cfg(not(feature = "guid-v3-tail"))] +const PROJECT_CLASSID: u32 = NodeGuid::CLASSID_PROJECT; + +/// The **project-management** domain (V3 class [`NodeGuid::CLASSID_PROJECT_V3`], /// OGAR `0x01XX`): OpenProject ↔ Redmine work items. Family = project / version; /// `in_family` = relates-to, `out_family` = blocks (cross-project dependency). /// Anchor families are caller-supplied (the milestone / release hubs). pub const PROJECT: DomainSpec = DomainSpec { - classid: NodeGuid::CLASSID_PROJECT, + classid: PROJECT_CLASSID, name: "Project", anchor_families: &[], in_family_edge: "relates-to", @@ -130,12 +148,20 @@ pub const PROJECT: DomainSpec = DomainSpec { member_edge: "in-project", }; -/// The **commerce / ERP** domain (classid [`NodeGuid::CLASSID_ERP`], OGAR +/// The commerce/ERP domain classid: the V3 class [`NodeGuid::CLASSID_ERP_V3`] +/// in a normal build; the legacy V1 [`NodeGuid::CLASSID_ERP`] only under +/// `--no-default-features`. +#[cfg(feature = "guid-v3-tail")] +const ERP_CLASSID: u32 = NodeGuid::CLASSID_ERP_V3; +#[cfg(not(feature = "guid-v3-tail"))] +const ERP_CLASSID: u32 = NodeGuid::CLASSID_ERP; + +/// The **commerce / ERP** domain (V3 class [`NodeGuid::CLASSID_ERP_V3`], OGAR /// `0x02XX`): Odoo ↔ OSB invoices / partners / taxes. Family = partner / journal; /// `in_family` = line-of, `out_family` = paid-by (cross-partner settlement). /// Anchor families are caller-supplied (the key accounts / journals). pub const ERP: DomainSpec = DomainSpec { - classid: NodeGuid::CLASSID_ERP, + classid: ERP_CLASSID, name: "ERP", anchor_families: &[], in_family_edge: "line-of", @@ -622,9 +648,24 @@ mod tests { hops[2].hops, hops[3].hops ); - // The exact fixed-depth-16 values: 2·(16−7)=18 and 2·(16−4)=24. - assert_eq!(hops[2].hops, 18); - assert_eq!(hops[3].hops, 24); + // The exact fixed-depth-16 values depend on the fold: + // - V3 (default, `from_guid_prefix_v3`): folds HEEL·HIP·TWIG·LEAF, classid + // NOT folded, so lcp counts only the shared HEEL nibbles. node2 HEEL + // 0x1009 shares 0x100 (lcp 3) ⇒ 2·(16−3)=26; node3 HEEL 0xF000 diverges + // at nibble 0 (lcp 0) ⇒ 2·(16−0)=32. + // - V1 (`--no-default-features`, `from_guid_prefix`): folds + // classid_lo·HEEL·HIP·TWIG; the shared classid_lo adds 4 to the lcp ⇒ + // lcp 7 ⇒ 18 and lcp 4 ⇒ 24. + #[cfg(feature = "guid-v3-tail")] + { + assert_eq!(hops[2].hops, 26); + assert_eq!(hops[3].hops, 32); + } + #[cfg(not(feature = "guid-v3-tail"))] + { + assert_eq!(hops[2].hops, 18); + assert_eq!(hops[3].hops, 24); + } } #[test]