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
35 changes: 35 additions & 0 deletions .claude/board/ISSUES.md
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
14 changes: 13 additions & 1 deletion crates/causal-edge/src/edge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()))
Expand Down
4 changes: 4 additions & 0 deletions crates/causal-edge/src/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
60 changes: 60 additions & 0 deletions crates/lance-graph-contract/src/canonical_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -1218,9 +1274,13 @@ static BUILTIN_READ_MODES: LazyLock<HashMap<u32, ReadMode>> = 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
});
Expand Down
57 changes: 49 additions & 8 deletions crates/lance-graph-contract/src/soa_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,34 +108,60 @@ 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",
out_family_edge: "part-of",
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",
out_family_edge: "blocks",
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",
Expand Down Expand Up @@ -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]
Expand Down
Loading