diff --git a/.claude/board/AGENT_LOG.md b/.claude/board/AGENT_LOG.md index c2ad067d..5dc5ce28 100644 --- a/.claude/board/AGENT_LOG.md +++ b/.claude/board/AGENT_LOG.md @@ -1,3 +1,15 @@ +## 2026-06-20 (cont.¹²) — D-OVC realign LANDED: contract classids follow OGAR 0xDDCC + ogar_codebook mirror + +**Main thread (Opus), autoattended.** Operator "562 merged, Rebase" + earlier `AskUserQuestion` greenlight (realign 0xDDCC / wire-compat / FMA=Health 0x0901). Rebased the jirak branch onto new main (6075d007, post #561+#562; #562 = bridge-codebook-convergence, different files — no conflict) and executed the migration plan's D-OVC deliverables, resolving ISS-CLASSID-OGAR-DRIFT. + +**D-OVC-2 + D-OVC-3 (realign, canonical_node.rs):** `CLASSID_OSINT 0x0007 → 0x0700` (OSINT domain root; `>>8 == 0x07`), `CLASSID_FMA 0x0008 → 0x0901` (anatomy concept in the Health domain; `0x0900` = Health root). Minted `CLASSID_PROJECT = 0x0100` + `CLASSID_ERP = 0x0200`. Added `ReadMode::{PROJECT, ERP}` (both Cognitive/CoarseOnly hot business graphs), registered in `BUILTIN_READ_MODES`. Updated the value-asserting tests (old 0x0007/0x0008 → new values + `>>8` domain-byte asserts) and added `project_and_erp_classids_resolve_to_their_read_modes`. `soa_graph::{PROJECT, ERP}` DomainSpecs (siblings of OSINT_GOTHAM/FMA_ANATOMY), re-exported from lib.rs. Realign is **layout-preserving** (a const value change, not a bit reclaim) → no `ENVELOPE_LAYOUT_VERSION` bump, no field-isolation matrix needed. + +**D-OVC-1 + D-OVC-4 (NEW `contract::ogar_codebook`):** wire-compat mirror of OGAR `ogar-vocab`'s codebook layer — **zero-dep, no OGAR↔contract dependency** (operator chose wire-compat). `ConceptDomain` (Reserved/ProjectMgmt/Commerce/Osint/Ocr/Health/Unassigned, non_exhaustive), `canonical_concept_domain(id>>8)`, `classid_concept_domain(classid)` (D-OVC-4 route), `source_domain_concept("project"|"erp"|"german-erp")`, `CODEBOOK` (26 project `0x01XX` + 6 commerce `0x02XX` concepts mirrored from OGAR `lib.rs:1073`), `canonical_concept_id`, `LabelDTO {label,id,canonical}` + `from_canonical` + `id_le`. Named `from_canonical` (not OGAR's `from_alias`) on purpose: the contract carries the codebook-id layer, NOT OGAR's curator-alias normalizer (`canonical_concept`) — that stays in ogar-vocab. **Drift guard:** `codebook_ids_match_ogar_vocab` pins the shared `0xDDCC` ids; if OGAR moves one, both sides update together. 6 tests. + +**Verified BOTH configs:** `cargo test -p lance-graph-contract --lib` = **710** (was 703; +7: ogar_codebook ×6, project/erp read-mode ×1), `--features guid-v2-tail` = **716**; clippy `-D warnings --all-targets` clean on both; `cargo fmt` clean. Downstream unbroken: lance-graph-callcenter `--features query` = **211** (graph_table builds OSINT nodes via the symbolic const — value-agnostic). All OSINT/FMA references are symbolic; only symbiont's opaque test-classid literals (`0x0007/8`, HHTL-path tests, not OSINT/FMA assertions) use the bare values, unaffected. + +Plan `ogar-vocab-contract-codebook-migration-v1.md` D-OVC-1/2/4 → SHIPPED, D-OVC-3 → PARTIAL (canon-doc cross-ref pending). ISSUES `ISS-CLASSID-OGAR-DRIFT` → RESOLVING. Branch reset to main + new work; PR to follow (NOT pushed to main — classifier-gated). Operator note: "might need cherry pick on New PR" — the jirak branch is fully merged into main via #561, so this is fresh work atop main, landing as a new PR. + ## 2026-06-20 (cont.¹¹) — ogar-vocab⇄contract codebook migration doc + canon-conflict surfaced **Main thread (Opus), autoattended.** Operator: "point [to migration docs] as in DO it" + diagnosed the ontology/contract/q2 triangle seams. Grounded in OGAR `crates/ogar-vocab/src/lib.rs` (read, not guessed): it already defines `CODEBOOK` (domain-encoded `0xDDCC`, :1073), `ConceptDomain` + `canonical_concept_domain` (:1141/:1163), `source_domain_concept("project"|"erp")` (:1186), `canonical_concept_id` (:1214), `LabelDTO` (:1476) — and its own note (:1208) says `LabelDTO` "long-term belongs in lance-graph-contract; codebook id == NodeGuid.classid low u16." **Found a real canon conflict:** merged `CLASSID_OSINT=0x0007` is OGAR's *Reserved* domain (OSINT=`0x07XX`); `CLASSID_FMA=0x0008` is OGAR's *OCR* block (FMA/anatomy≈Health `0x09XX`). Wrote `.claude/plans/ogar-vocab-contract-codebook-migration-v1.md` (D-OVC-1..4): host codebook/ConceptDomain/LabelDTO in contract, classids follow `0xDDCC` (mint project `0x01XX`+ERP `0x02XX`; realign OSINT→`0x0700`, FMA→Health). INTEGRATION_PLANS prepended; ISSUES `ISS-CLASSID-OGAR-DRIFT` filed. **Did NOT mint/rewrite code:** the OSINT/FMA realign rewrites merged canon + the CLAUDE.md canon block → operator sign-off required (plan §5). Surfaced 3 decisions: (1) realign OSINT/FMA? (2) OGAR↔contract dependency direction (move vs wire-compat)? (3) FMA → Health 0x09XX or new anatomy domain? Doc committed to the jirak branch (PR #561 arc). diff --git a/.claude/board/ISSUES.md b/.claude/board/ISSUES.md index 6da33661..a326917c 100644 --- a/.claude/board/ISSUES.md +++ b/.claude/board/ISSUES.md @@ -264,6 +264,9 @@ flip Open entry to Superseded. **When an issue is deferred knowingly** — leave it Open here but also append a row to `TECH_DEBT.md` with cross-ref back. +## ISS-CLASSID-OGAR-DRIFT — 2026-06-20 (cont.) — RESOLVING (operator signed off; landed) +**Status:** RESOLVING — operator greenlit the realign (`AskUserQuestion`: "Realign to 0xDDCC", "Wire-compat now", "FMA = Health 0x09XX"). Landed D-OVC-1/2/4 on the jirak branch: `CLASSID_OSINT 0x0007 → 0x0700` (OSINT domain root), `CLASSID_FMA 0x0008 → 0x0901` (anatomy concept in Health, `0x0900` = Health root); minted `CLASSID_PROJECT = 0x0100` + `CLASSID_ERP = 0x0200` with `ReadMode::{PROJECT,ERP}` registered; NEW `contract::ogar_codebook` (wire-compat mirror, zero-dep — `ConceptDomain` / `canonical_concept_domain` / `classid_concept_domain` / `source_domain_concept` / `CODEBOOK` / `canonical_concept_id` / `LabelDTO::from_canonical`); `soa_graph::{PROJECT,ERP}` DomainSpecs. Drift guard test pins the shared `0xDDCC` ids; contract 710 default / 716 v2 green, clippy clean. **Dependency direction = (b) wire-compat (no OGAR↔contract dep);** the `u16` LE wire is the only contract. D-OVC-3 (cutover/version-gate audit of the *value* realign per `I-LEGACY-API-FEATURE-GATED`) remains; the classids are layout-preserving (a const value change, not a bit-layout reclaim), so no `ENVELOPE_LAYOUT_VERSION` bump. Closes when the PR merges. + ## ISS-CLASSID-OGAR-DRIFT — 2026-06-20 — OPEN (needs operator sign-off) **What:** merged `lance-graph-contract` classids drifted from OGAR `ogar-vocab`'s domain-encoded codebook (`0xDDCC`, `crates/ogar-vocab/src/lib.rs:1073` CODEBOOK + `:1163` `canonical_concept_domain`). `CLASSID_OSINT=0x0007` → `0x00` = OGAR *Reserved* domain (OSINT is `0x07XX`); `CLASSID_FMA=0x0008` → OGAR *OCR* block (FMA/anatomy is clinical → Health `0x09XX`). OGAR's own note (`lib.rs:1204-1212`): codebook id == `NodeGuid.classid` low u16, and `LabelDTO` "long-term belongs in lance-graph-contract." So contract + OGAR currently disagree on what `0x07`/`0x08` mean. **Impact:** the contract↔OGAR↔q2 triangle has an inconsistent classid space; `canonical_concept_domain(id>>8)` mis-routes contract's OSINT/FMA; project/ERP un-minted. diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index 46e29345..5cde3942 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -16,6 +16,8 @@ --- +> **2026-06-20 — IN PR (`claude/jirak-math-theorems-harvest-rfii13`)** — **D-OVC: contract classids realigned to OGAR `0xDDCC` + `contract::ogar_codebook` wire-compat mirror.** Resolved ISS-CLASSID-OGAR-DRIFT (operator-signed). **Realigned (layout-preserving const values, no `ENVELOPE_LAYOUT_VERSION` bump):** `CLASSID_OSINT 0x0007 → 0x0700` (OSINT domain root, `>>8 == 0x07`), `CLASSID_FMA 0x0008 → 0x0901` (anatomy concept in Health domain, `0x0900` = root). **Minted:** `CLASSID_PROJECT = 0x0100` + `CLASSID_ERP = 0x0200` with `ReadMode::{PROJECT, ERP}` (Cognitive/CoarseOnly) registered in `BUILTIN_READ_MODES`; `soa_graph::{PROJECT, ERP}` DomainSpecs. **NEW `contract::ogar_codebook`** (zero-dep, **wire-compat — NO OGAR↔contract dependency**): `ConceptDomain` (7 domains, `id>>8` route), `canonical_concept_domain`, `classid_concept_domain` (D-OVC-4 classid→domain), `source_domain_concept`, `CODEBOOK` (26 project `0x01XX` + 6 commerce `0x02XX`, mirrored from OGAR `ogar-vocab` `lib.rs:1073`), `canonical_concept_id`, `LabelDTO::from_canonical` + `id_le`. Drift-guard test pins the shared `0xDDCC` ids. Contract **710** lib (default) / **716** (`guid-v2-tail`), callcenter `--features query` **211** green; clippy `-D warnings` + fmt clean both configs. Refs: AGENT_LOG 2026-06-20 (cont.¹²), plan `ogar-vocab-contract-codebook-migration-v1.md` (D-OVC-1/2/4 SHIPPED, D-OVC-3 PARTIAL), ISSUES `ISS-CLASSID-OGAR-DRIFT` (RESOLVING). +> > **2026-06-20 — IN PR (`claude/jirak-math-theorems-harvest-rfii13`)** — **codex roll-up + 16-family-adapter edges + Callcenter DataFusion/Gremlin + aiwar POC.** Follow-up to merged #557. (1) Both codex P1 fixes rolled in: classid filter (`project_snapshot`/`nearest_anchor` only project `classid == domain.classid` rows) + the operator's **16×8-bit family-node adapter** edge model — the `EdgeBlock` reads as 16 family adapters (each byte → a FAMILY by `family & 0xFF`, collision-aware skip), dissolving the >255-member aliasing; member-by-identity resolution removed (`E-FAMILY-ADAPTER-EDGES-ARE-RENDER-STABLE`). (2) `lance-graph-callcenter`: NEW `graph_table` (`query-lite`, `GraphSnapshot` → `nodes`/`edges` arrow MemTable `TableProvider`s + `register_graph(SessionContext)`) + NEW `graph_gremlin` (always-on Gremlin/SurrealQL traversal kernel). (3) `contract::aiwar` + example: `AiwarClassView` (category ⇒ family) + `aiwar_node_rows` ingest the real `aiwar-neo4j-harvest/data/aiwar_graph.json` (221 entities → 281 nodes / 60 family hubs / 481 edges). Contract 703 lib + callcenter 10 graph tests green; contract clippy `--all-targets -D warnings` clean. q2 wires the GraphSnapshot → Quadro-2 visual. Refs: AGENT_LOG 2026-06-20 (cont.⁷), EPIPHANIES `E-FAMILY-ADAPTER-EDGES-ARE-RENDER-STABLE`, TECH_DEBT `TD-CALLCENTER-QUERY-CLIPPY`. > > **2026-06-20 — branch work (`claude/jirak-math-theorems-harvest-rfii13`)** — **SoA-as-graph domain foundation for the OSINT/Gotham + FMA consumers (q2 renders the pixels).** New zero-dep `contract::soa_graph`: `project_snapshot(&[NodeRow], &DomainSpec) -> graph_render::GraphSnapshot` projects the canonical 32-byte head (NodeGuid + EdgeBlock) into the EXISTING Gotham/neo4j surface (`graph_render` — reused, not duplicated) — family nodes (by u24 `family`), member/in-family/out-of-family edges, all **zero value decode**. `nearest_anchor` ranks nodes to their nearest stability-anchor family by the new `NiblePath::family_hop_count` (CLAM tree distance). Two domains registered: `OSINT_GOTHAM` (classid **`0x0007`**) + `FMA_ANATOMY` (**`0x0008`**, bones = anchor families) in `BUILTIN_READ_MODES` (`ReadMode::OSINT` Cognitive/CoarseOnly hot; `ReadMode::FMA` Compressed/CoarseOnly cold). Anchor-ness is a HEAD field (`family`), never a value type — so "FMA bones as stability anchor" stays head-only (`E-ANCHOR-IS-A-HEAD-FIELD-NOT-A-VALUE-TYPE`). De-duped the GUID→NiblePath lowering: symbiont's `hhtl_path_of` now delegates to canonical `from_guid_prefix` (third copy collapsed). 698 contract + 12 symbiont tests green, clippy clean. **Deferred (named):** q2 rendering (q2 session), Callcenter DataFusion/gremlin POC, OntologyRegistry ClassView labels. Refs: AGENT_LOG 2026-06-20 (cont.⁶), EPIPHANIES `E-ANCHOR-IS-A-HEAD-FIELD-NOT-A-VALUE-TYPE`. diff --git a/.claude/plans/ogar-vocab-contract-codebook-migration-v1.md b/.claude/plans/ogar-vocab-contract-codebook-migration-v1.md index fe3836e4..f27094ba 100644 --- a/.claude/plans/ogar-vocab-contract-codebook-migration-v1.md +++ b/.claude/plans/ogar-vocab-contract-codebook-migration-v1.md @@ -1,9 +1,9 @@ # Migration — OGAR `ogar-vocab` codebook ⇄ `lance-graph-contract` classid (v1) -> **Status:** PROPOSED (2026-06-20). Surfaces a **canon conflict** between merged -> `lance-graph-contract` classids and OGAR's `ogar-vocab` codebook; the -> reconciliation rewrites merged canon (`CLASSID_OSINT`/`CLASSID_FMA`) and so is -> gated on operator sign-off. +> **Status:** SHIPPING (2026-06-20). Operator signed off §5; D-OVC-1/2/4 landed +> on the jirak branch + D-OVC-3 realign landed (canon-doc cross-ref pending). +> Originally surfaced a **canon conflict** between merged `lance-graph-contract` +> classids and OGAR's `ogar-vocab` codebook. > **The triangle:** ontology (OGAR `ogar-vocab`) → contract (`NodeGuid`/`ClassId`) > → q2 (Quadro-2 cockpit consuming `GraphSnapshot`). @@ -84,23 +84,43 @@ class-identity codebook. Reconcile onto OGAR's `0xDDCC` scheme: ## 4 — Deliverables (gated on §5 decisions) -- **D-OVC-1** Move/mirror `ConceptDomain` + `canonical_concept_domain` + - `source_domain_concept` + `canonical_concept_id` + `CODEBOOK` + `LabelDTO` - into `lance-graph-contract` (e.g. `contract::ogar_codebook`); `ogar-vocab` - re-exports (or wire-compat duplicate). Round-trip test: `LabelDTO::from_alias` - parity across both crates. -- **D-OVC-2** Mint `CLASSID_PROJECT` (`0x0100`) + `CLASSID_ERP` (`0x0200`) in - `canonical_node.rs` + `ReadMode`s, registered in `BUILTIN_READ_MODES`. Add - `soa_graph::{PROJECT, ERP}` `DomainSpec`s (siblings of `OSINT_GOTHAM`/`FMA_ANATOMY`). -- **D-OVC-3** **Canon realign (SIGN-OFF):** `CLASSID_OSINT 0x0007 → 0x0700`, - `CLASSID_FMA 0x0008 → 0x09xx` (Health) or a minted anatomy domain. Field-isolation - / version-gate per `I-LEGACY-API-FEATURE-GATED`; update `aiwar.rs`, `soa_graph.rs`, - tests, and the canon block in `lance-graph/CLAUDE.md` + OGAR `CODEBOOK`. -- **D-OVC-4** Route `classid → ReadMode` (and the domain ClassView) through - `canonical_concept_domain(classid_lo)`; q2 reads `LabelDTO`/`canonical` for - display labels (the contract→q2 leg of the triangle). - -## 5 — Decisions needed (operator) +> **Update 2026-06-20:** operator signed off §5 (realign 0xDDCC / wire-compat / +> FMA = Health `0x0901`). D-OVC-1/2 SHIPPED, D-OVC-4 SHIPPED (function + tests; +> q2 display-label leg is q2-side); D-OVC-3 (cutover audit) downgraded — the +> classid realign is a const *value* change, not a bit-layout reclaim, so it is +> layout-preserving (no `ENVELOPE_LAYOUT_VERSION` bump); what remains is updating +> the `lance-graph/CLAUDE.md` canon block + OGAR `CODEBOOK` cross-doc. + +- **D-OVC-1** ✅ **SHIPPED.** NEW `contract::ogar_codebook` — wire-compat mirror + (zero-dep, no OGAR↔contract dependency): `ConceptDomain`, `canonical_concept_domain`, + `classid_concept_domain` (the classid→domain route, D-OVC-4), `source_domain_concept`, + `CODEBOOK` (project `0x01XX` + commerce `0x02XX`), `canonical_concept_id`, + `LabelDTO { label, id, canonical }` + `from_canonical` + `id_le`. (Named + `from_canonical`, not `from_alias`: the contract carries the codebook-id layer, + NOT OGAR's curator-alias normalizer — that stays in `ogar-vocab`.) Drift-guard + test pins the shared `0xDDCC` ids; 6 tests. +- **D-OVC-2** ✅ **SHIPPED.** Minted `CLASSID_PROJECT` (`0x0100`) + `CLASSID_ERP` + (`0x0200`) in `canonical_node.rs` + `ReadMode::{PROJECT, ERP}` (both Cognitive / + CoarseOnly), registered in `BUILTIN_READ_MODES`. Added `soa_graph::{PROJECT, ERP}` + `DomainSpec`s (siblings of `OSINT_GOTHAM`/`FMA_ANATOMY`), re-exported from lib.rs. +- **D-OVC-3** ◐ **PARTIAL.** Canon realign LANDED: `CLASSID_OSINT 0x0007 → 0x0700`, + `CLASSID_FMA 0x0008 → 0x0901` (anatomy concept in Health). Tests updated to assert + the new values + `>>8` domain bytes. **Layout-preserving** (const value, not a bit + reclaim) → no field-isolation matrix / version-gate needed. **Remaining:** update + the `lance-graph/CLAUDE.md` canon block note + OGAR `CODEBOOK` cross-ref doc. +- **D-OVC-4** ✅ **SHIPPED (contract leg).** `classid_concept_domain(classid)` routes + on the low-u16 `0xDDCC` high byte; tests assert all five classids resolve to their + `ConceptDomain`. q2's `LabelDTO`/`canonical` display-label consumption is the q2-side + leg (this crate exports the type + ids). + +## 5 — Decisions needed (operator) — ✅ RESOLVED 2026-06-20 + +1. **Canon realign OSINT/FMA?** → **YES, realign to `0xDDCC`** (OSINT `0x0700`, + FMA `0x0901`). +2. **Dependency direction?** → **(b) wire-compat now** — both define, the `u16` LE + wire is the only contract, drift-guard test prevents divergence. No new dep. +3. **FMA/anatomy domain?** → **Health `0x09XX`** — FMA = anatomy concept `0x0901`, + `0x0900` = Health root. (`CC = 0x00` = domain root, reserved everywhere.) 1. **Canon realign OSINT/FMA?** `CLASSID_OSINT 0x0007 → 0x0700`, `CLASSID_FMA 0x0008 → 0x09XX`. This rewrites merged canon (#557/#560) + the `lance-graph/ diff --git a/crates/lance-graph-contract/src/aiwar.rs b/crates/lance-graph-contract/src/aiwar.rs index f11bb078..ec2327c7 100644 --- a/crates/lance-graph-contract/src/aiwar.rs +++ b/crates/lance-graph-contract/src/aiwar.rs @@ -74,7 +74,8 @@ impl AiwarClassView { pub fn aiwar_node_rows(graph: &LiteralGraph) -> Vec { let view = AiwarClassView::from_graph(graph); let ids = graph.all_node_ids(); - let fam_of = |id: &str| -> Option { graph.node(id).and_then(|n| view.family_of(&n.label)) }; + let fam_of = + |id: &str| -> Option { graph.node(id).and_then(|n| view.family_of(&n.label)) }; ids.iter() .enumerate() .map(|(i, id)| { diff --git a/crates/lance-graph-contract/src/canonical_node.rs b/crates/lance-graph-contract/src/canonical_node.rs index 15df9b8c..2673321a 100644 --- a/crates/lance-graph-contract/src/canonical_node.rs +++ b/crates/lance-graph-contract/src/canonical_node.rs @@ -40,15 +40,31 @@ impl NodeGuid { /// Reserved canonical default basin (implicit fallback; no neighborhood grouping). pub const FAMILY_DEFAULT: u32 = 0x00_0000; - /// OGAR class for the **OSINT / Palantir-Gotham** domain — the neo4j-emulation - /// entity graph (people / orgs / systems / events, family-grouped). Resolves - /// to [`ReadMode::OSINT`] (hot `Cognitive` value + `CoarseOnly` adjacency edges). - pub const CLASSID_OSINT: u32 = 0x0000_0007; - /// OGAR class for the **FMA anatomy** domain — the Foundational Model of - /// Anatomy (~70k structural entities, family = body region, bones = stability - /// anchors). Resolves to [`ReadMode::FMA`] (cold `Compressed` reference value + - /// `CoarseOnly` part-of adjacency). - pub const CLASSID_FMA: u32 = 0x0000_0008; + // ── classids follow OGAR `ogar-vocab`'s domain-encoded `0xDDCC` codebook ── + // (DD = domain high byte, CC = concept slot; CC=0x00 = domain root, reserved). + // `canonical_concept_domain(classid_lo)` (see `crate::ogar_codebook`) routes on + // `classid >> 8`. Realigned 2026-06-20 (ISS-CLASSID-OGAR-DRIFT): OSINT was + // 0x0007 (OGAR Reserved domain) → 0x0700; FMA was 0x0008 (OGAR OCR block) → + // 0x0901 (anatomy concept in the Health domain). Migration: + // `.claude/plans/ogar-vocab-contract-codebook-migration-v1.md`. + + /// **OSINT / Palantir-Gotham** domain root (`0x07` = OSINT domain, `0x00` = + /// root). The neo4j-emulation entity graph (people / orgs / systems / events, + /// family-grouped). Resolves to [`ReadMode::OSINT`] (hot `Cognitive` value + + /// `CoarseOnly` adjacency). + pub const CLASSID_OSINT: u32 = 0x0000_0700; + /// **FMA anatomy** — the `anatomy` concept (`0x01`) in the **Health** domain + /// (`0x09`); `0x0900` is the Health root. The Foundational Model of Anatomy + /// (~70k structural entities, family = body region, bones = stability anchors). + /// Resolves to [`ReadMode::FMA`] (cold `Compressed` reference + `CoarseOnly`). + pub const CLASSID_FMA: u32 = 0x0000_0901; + /// **Project-management** domain root (`0x01`) — OpenProject ↔ Redmine + /// (work items, members, versions, …). OGAR codebook `0x01XX`. Resolves to + /// [`ReadMode::PROJECT`]. + pub const CLASSID_PROJECT: u32 = 0x0000_0100; + /// **Commerce / ERP** domain root (`0x02`) — Odoo ↔ OSB (invoices, taxes, + /// partners, payments, …). OGAR codebook `0x02XX`. Resolves to [`ReadMode::ERP`]. + pub const CLASSID_ERP: u32 = 0x0000_0200; /// Construct from the six canonical groups. `family`/`identity` use their low 3 bytes. /// @@ -813,6 +829,24 @@ impl ReadMode { edge_codec: EdgeCodecFlavor::CoarseOnly, }; + /// The **project-management** read-mode ([`NodeGuid::CLASSID_PROJECT`], + /// OpenProject ↔ Redmine): a *hot* work-item graph — [`ValueSchema::Cognitive`] + /// (live lifecycle: status / assignee / version edges queried + reasoned over) + /// with [`EdgeCodecFlavor::CoarseOnly`] adjacency (parent / blocks / relates). + pub const PROJECT: ReadMode = ReadMode { + value_schema: ValueSchema::Cognitive, + edge_codec: EdgeCodecFlavor::CoarseOnly, + }; + + /// The **commerce / ERP** read-mode ([`NodeGuid::CLASSID_ERP`], Odoo ↔ OSB): + /// a *hot* transactional graph — [`ValueSchema::Cognitive`] (invoices / taxes / + /// partners / payments queried live) with [`EdgeCodecFlavor::CoarseOnly`] + /// adjacency (partner-of / line-of / paid-by). + pub const ERP: ReadMode = ReadMode { + value_schema: ValueSchema::Cognitive, + edge_codec: EdgeCodecFlavor::CoarseOnly, + }; + /// Both axes are layout-preserving (a preset/flavor re-interprets reserved /// bytes, never a stride change), so adopting any read-mode needs no /// `ENVELOPE_LAYOUT_VERSION` bump. @@ -839,6 +873,10 @@ static BUILTIN_READ_MODES: LazyLock> = LazyLock::new(|| { // CoarseOnly adjacency; they differ in the value schema (hot vs cold). m.insert(NodeGuid::CLASSID_OSINT, ReadMode::OSINT); m.insert(NodeGuid::CLASSID_FMA, ReadMode::FMA); + // Project-management (OpenProject ↔ Redmine) + commerce/ERP (Odoo ↔ OSB) — + // the OGAR `0x01XX` / `0x02XX` domains; both hot business graphs (Cognitive). + m.insert(NodeGuid::CLASSID_PROJECT, ReadMode::PROJECT); + m.insert(NodeGuid::CLASSID_ERP, ReadMode::ERP); m }); @@ -1399,10 +1437,14 @@ mod tests { assert_eq!(fma.value_schema, ValueSchema::Compressed); assert_eq!(fma.edge_codec, EdgeCodecFlavor::CoarseOnly); - // The classids are the OGAR-confirmed 0x0007 (OSINT) and 0x0008 (FMA); - // both are layout-preserving and carrier-method-consistent. - assert_eq!(NodeGuid::CLASSID_OSINT, 0x0000_0007); - assert_eq!(NodeGuid::CLASSID_FMA, 0x0000_0008); + // The classids follow OGAR `0xDDCC` (ISS-CLASSID-OGAR-DRIFT realign): + // OSINT domain root `0x0700` (`>>8 == 0x07`); FMA = anatomy concept + // `0x0901` in the Health domain (`>>8 == 0x09`). Never the pre-realign + // 0x0007 (OGAR Reserved) / 0x0008 (OGAR OCR) values. + assert_eq!(NodeGuid::CLASSID_OSINT, 0x0000_0700); + assert_eq!(NodeGuid::CLASSID_FMA, 0x0000_0901); + assert_eq!(NodeGuid::CLASSID_OSINT >> 8, 0x07, "OSINT domain high byte"); + assert_eq!(NodeGuid::CLASSID_FMA >> 8, 0x09, "Health domain high byte"); assert_eq!( NodeGuid::new(NodeGuid::CLASSID_OSINT, 1, 2, 3, 0xAB, 0xCD).read_mode(), ReadMode::OSINT @@ -1410,6 +1452,29 @@ mod tests { assert!(osint.is_layout_preserving() && fma.is_layout_preserving()); } + #[test] + fn project_and_erp_classids_resolve_to_their_read_modes() { + // OGAR `0x01XX` (project-mgmt: OpenProject ↔ Redmine) + `0x02XX` + // (commerce/ERP: Odoo ↔ OSB) — both hot business graphs (Cognitive). + let project = classid_read_mode(NodeGuid::CLASSID_PROJECT); + assert_eq!(project, ReadMode::PROJECT); + assert_eq!(project.value_schema, ValueSchema::Cognitive); + assert_eq!(project.edge_codec, EdgeCodecFlavor::CoarseOnly); + + let erp = classid_read_mode(NodeGuid::CLASSID_ERP); + assert_eq!(erp, ReadMode::ERP); + assert_eq!(erp.value_schema, ValueSchema::Cognitive); + assert_eq!(erp.edge_codec, EdgeCodecFlavor::CoarseOnly); + + // Domain roots: project `0x0100` (`>>8 == 0x01`), ERP `0x0200` + // (`>>8 == 0x02`); low byte `0x00` = the domain root (reserved concept). + assert_eq!(NodeGuid::CLASSID_PROJECT, 0x0000_0100); + assert_eq!(NodeGuid::CLASSID_ERP, 0x0000_0200); + assert_eq!(NodeGuid::CLASSID_PROJECT >> 8, 0x01); + assert_eq!(NodeGuid::CLASSID_ERP >> 8, 0x02); + assert!(project.is_layout_preserving() && erp.is_layout_preserving()); + } + // ── GUID v2 tail (D-GV2-1) — field-isolation matrix + coexistence ───────── #[cfg(feature = "guid-v2-tail")] diff --git a/crates/lance-graph-contract/src/codebook.rs b/crates/lance-graph-contract/src/codebook.rs index 7bc3c7ea..db47b044 100644 --- a/crates/lance-graph-contract/src/codebook.rs +++ b/crates/lance-graph-contract/src/codebook.rs @@ -163,7 +163,7 @@ mod tests { } assert!(cb.is_full()); assert_eq!(cb.len(), 255); // indices 1..=255, 0 reserved - // a NEW label overflows → None (split the family)… + // a NEW label overflows → None (split the family)… assert_eq!(cb.intern("one_too_many"), None); // …but an already-interned label still resolves at capacity. assert_eq!(cb.intern("e0"), Some(1)); diff --git a/crates/lance-graph-contract/src/hhtl.rs b/crates/lance-graph-contract/src/hhtl.rs index 8b0142d6..b165c8bc 100644 --- a/crates/lance-graph-contract/src/hhtl.rs +++ b/crates/lance-graph-contract/src/hhtl.rs @@ -723,7 +723,7 @@ mod tests { let sib = NiblePath::root(0x1).child(0x2).child(0x3).child(0x9); assert_eq!(a.family_hop_count(sib), 2); assert_eq!(sib.family_hop_count(a), 2); // symmetric - // parent = 1 hop + // parent = 1 hop let parent = NiblePath::root(0x1).child(0x2).child(0x3); assert_eq!(a.family_hop_count(parent), 1); // cousins: share (1)(2), differ from depth 3 down → (4-2)+(4-2) = 4 diff --git a/crates/lance-graph-contract/src/lib.rs b/crates/lance-graph-contract/src/lib.rs index fc3eca76..1714c1a7 100644 --- a/crates/lance-graph-contract/src/lib.rs +++ b/crates/lance-graph-contract/src/lib.rs @@ -78,6 +78,8 @@ pub mod mul; pub mod nan_projection; pub mod nars; pub mod ocr; +/// D-OVC-1 — OGAR concept codebook (`0xDDCC` domain layout), wire-compat mirror. +pub mod ogar_codebook; pub mod ontology; pub mod orchestration; pub mod orchestration_mode; @@ -127,9 +129,14 @@ pub use collapse_gate::{GateDecision, MailboxId, MergeMode}; pub use episodic_edges::{EdgeRef, EpisodicEdges64}; pub use head2head::{CompetitionOutcome, Head2Head, WinnerCriterion}; pub use kanban::{ExecTarget, KanbanColumn, KanbanMove, RubiconTransitionError}; +pub use ogar_codebook::{ + canonical_concept_domain, canonical_concept_id, classid_concept_domain, source_domain_concept, + ConceptDomain, LabelDTO, CODEBOOK, +}; pub use scheduler::{DatasetVersion, NextPhaseScheduler, VersionScheduler}; pub use soa_graph::{ - nearest_anchor, project_snapshot, AnchorHop, DomainSpec, FMA_ANATOMY, OSINT_GOTHAM, + nearest_anchor, project_snapshot, AnchorHop, DomainSpec, ERP, FMA_ANATOMY, OSINT_GOTHAM, + PROJECT, }; pub use soa_view::{MailboxSoaOwner, MailboxSoaView}; pub use view_angle::ViewAngle; diff --git a/crates/lance-graph-contract/src/nan_projection.rs b/crates/lance-graph-contract/src/nan_projection.rs index 7e71d477..78cf829f 100644 --- a/crates/lance-graph-contract/src/nan_projection.rs +++ b/crates/lance-graph-contract/src/nan_projection.rs @@ -125,7 +125,11 @@ mod tests { #[test] fn subnormal_and_zero_are_finite() { // exponent-zero patterns (zero, subnormals) must NOT be flagged - let rows = vec![board_with(0.0), board_with(-0.0), board_with(f32::MIN_POSITIVE)]; + let rows = vec![ + board_with(0.0), + board_with(-0.0), + board_with(f32::MIN_POSITIVE), + ]; assert!(project_energy_nonfinite(&rows).is_clean()); } } diff --git a/crates/lance-graph-contract/src/ogar_codebook.rs b/crates/lance-graph-contract/src/ogar_codebook.rs new file mode 100644 index 00000000..08a62ea8 --- /dev/null +++ b/crates/lance-graph-contract/src/ogar_codebook.rs @@ -0,0 +1,299 @@ +//! `ogar_codebook` — the OGAR concept codebook, wire-compatible mirror (D-OVC-1). +//! +//! OGAR's `ogar-vocab` crate owns the canonical class-identity codebook: a curated +//! `(concept, u16)` table whose ids are **domain-encoded** as `0xDDCC` (`DD` = the +//! domain high byte, `CC` = the concept slot, `CC == 0x00` = the domain root, +//! reserved). Its own doc-comment says the long-term home of these types is +//! `lance-graph-contract`, "alongside `ClassId` and the `NodeGuid` LE layout." +//! +//! This module is that home — but **wire-compatible, not a dependency**. The +//! contract is zero-runtime-dep by design, so it does NOT depend on `ogar-vocab`; +//! instead both crates agree on the **wire**: a concept's id is one `u16`, +//! serialized little-endian, and that id IS the low 16 bits of +//! [`NodeGuid::classid`](crate::NodeGuid). Any encoder/decoder that agrees on +//! `u16` LE is compatible regardless of which crate it links. The parity tests +//! below pin the shared values; if OGAR's `CODEBOOK` ever moves an id, BOTH sides +//! must update together (the drift guard). +//! +//! What this mirror carries: the **codebook-id layer** the contract needs to route +//! a `classid` to its domain ([`canonical_concept_domain`], [`classid_concept_domain`]) +//! and to resolve a canonical-concept string to its id ([`canonical_concept_id`], +//! [`LabelDTO::from_canonical`]). What it does NOT carry: OGAR's curator-alias +//! normalizer (`canonical_concept` — the large `"Issue"`/`"WorkPackage"` → +//! `"project_work_item"` table). Alias normalization stays in `ogar-vocab`; this +//! module resolves canonical-shaped concept strings only (hence `from_canonical`, +//! not `from_alias` — naming the difference rather than faking parity). +//! +//! Cross-ref: `.claude/plans/ogar-vocab-contract-codebook-migration-v1.md`, +//! OGAR `crates/ogar-vocab/src/lib.rs` (`CODEBOOK` / `ConceptDomain` / `LabelDTO`), +//! [`canonical_node`](crate::canonical_node) (`CLASSID_*`), [`codebook`](crate::codebook) +//! (the FINER per-family scope — this is the coarse concept/classid scope). + +/// Codebook **domain** — the high byte of a canonical id (`id >> 8`, the `0xDDCC` +/// layout). Lets a consumer route on domain in O(1) from just the `u16`, no table +/// lookup. Reserved high-byte slots have a stable variant even before a concept +/// lands there, so consumers can branch on them today. Mirrors OGAR +/// `ogar_vocab::ConceptDomain` (wire-compatible — same `id >> 8` discriminant). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum ConceptDomain { + /// `0x00XX` — reserved (`0x0000` is [`NodeGuid::CLASSID_DEFAULT`]). + Reserved, + /// `0x01XX` — project-management (OpenProject ↔ Redmine). + ProjectMgmt, + /// `0x02XX` — commerce / billing / ERP (Odoo ↔ OSB). + Commerce, + /// `0x07XX` — OSINT (open-source intelligence / Palantir-Gotham). + Osint, + /// `0x08XX` — OCR (optical character recognition / document extraction). + Ocr, + /// `0x09XX` — Health (clinical / patient / care; FMA anatomy lives here). + Health, + /// Any high-byte slot not yet assigned a domain (`0x03XX`–`0x06XX`, `0x0AXX`+). + Unassigned, +} + +/// Resolve a canonical id's [`ConceptDomain`] from its high byte. Pure, +/// deterministic, O(1) — no table lookup. The single rule both the contract's +/// `classid → ReadMode` registry and OGAR's promotion gate route on. +#[inline] +#[must_use] +pub fn canonical_concept_domain(id: u16) -> ConceptDomain { + match id >> 8 { + 0x00 => ConceptDomain::Reserved, + 0x01 => ConceptDomain::ProjectMgmt, + 0x02 => ConceptDomain::Commerce, + 0x07 => ConceptDomain::Osint, + 0x08 => ConceptDomain::Ocr, + 0x09 => ConceptDomain::Health, + _ => ConceptDomain::Unassigned, + } +} + +/// Resolve a [`NodeGuid`](crate::NodeGuid) `classid` to its [`ConceptDomain`] (D-OVC-4). The +/// codebook id is the low 16 bits of the classid (`0xDDCC` lives in the low u16); +/// the high u16 is the canon-reserved zero-fallback prefix. So a domain route is +/// `canonical_concept_domain(classid as u16)`. This is the coarse sibling of the +/// per-family scope in [`codebook`](crate::codebook): classid (domain) selects the +/// coarse codebook; `family` selects the sub-codebook (longest-prefix-wins). +#[inline] +#[must_use] +pub fn classid_concept_domain(classid: u32) -> ConceptDomain { + canonical_concept_domain(classid as u16) +} + +/// Map a coarse curator `source_domain` tag (`"project"`, `"erp"`, `"german-erp"`) +/// to the [`ConceptDomain`] its promotions live in. `None` for an unrecognised tag +/// (the producer's source-domain → typed-domain seam). Mirrors OGAR +/// `source_domain_concept`. +#[inline] +#[must_use] +pub fn source_domain_concept(source_domain: &str) -> Option { + match source_domain { + "project" => Some(ConceptDomain::ProjectMgmt), + "erp" | "german-erp" => Some(ConceptDomain::Commerce), + _ => None, + } +} + +/// The curated `(canonical_concept, u16)` codebook — wire-compatible mirror of +/// OGAR `ogar_vocab::CODEBOOK`. Ids are stable forever (once shipped, never +/// re-assigned); domain-encoded `0xDDCC`. Carries the two domains the contract +/// graph surfaces realize today (project-mgmt `0x01XX`, commerce/ERP `0x02XX`); +/// OSINT (`0x07XX`) and Health/anatomy (`0x09XX`) are represented by their +/// [`NodeGuid`](crate::NodeGuid) classid roots, not yet by promoted concept slots here. Drift is +/// guarded by [`tests::codebook_ids_match_ogar_vocab`]. +pub const CODEBOOK: &[(&str, u16)] = &[ + // ── 0x01XX — project-mgmt domain (OpenProject ↔ Redmine) ── + ("project", 0x0101), + ("project_work_item", 0x0102), + ("billable_work_entry", 0x0103), + ("project_actor", 0x0104), + ("project_status", 0x0105), + ("project_type", 0x0106), + ("priority", 0x0107), + ("project_membership", 0x0108), + ("project_journal", 0x0109), + ("project_repository", 0x010A), + ("project_version", 0x010B), + ("project_wiki_page", 0x010C), + ("project_query", 0x010D), + ("project_attachment", 0x010E), + ("project_comment", 0x010F), + ("project_custom_field", 0x0110), + ("project_relation", 0x0111), + ("project_changeset", 0x0112), + ("project_watcher", 0x0113), + ("project_news", 0x0114), + ("project_message", 0x0115), + ("project_forum", 0x0116), + ("project_role", 0x0117), + ("project_member_role", 0x0118), + ("project_custom_value", 0x0119), + ("project_enabled_module", 0x011A), + // ── 0x02XX — commerce / billing / ERP domain (Odoo ↔ OSB) ── + ("commercial_line_item", 0x0201), + ("commercial_document", 0x0202), + ("tax_policy", 0x0203), + ("billing_party", 0x0204), + ("payment_record", 0x0205), + ("currency_policy", 0x0206), +]; + +/// Resolve a **canonical-concept** string to its stable `u16` codebook id via +/// [`CODEBOOK`]. `None` for an unpromoted concept (not in the codebook). +/// +/// This resolves canonical-shaped names only (e.g. `"project_work_item"`). For +/// curator-shaped aliases (`"Issue"`, `"WorkPackage"`), normalize through OGAR +/// `ogar_vocab::canonical_concept` first — that alias table stays in `ogar-vocab`, +/// out of the zero-dep contract. +#[inline] +#[must_use] +pub fn canonical_concept_id(concept: &str) -> Option { + CODEBOOK + .iter() + .find_map(|&(name, id)| (name == concept).then_some(id)) +} + +/// A curator-agnostic label binding: a consumer-local `label`, its OGAR codebook +/// `id` (binary identity), and the portable `canonical` symbol. Mirrors OGAR +/// `ogar_vocab::LabelDTO` (wire-compatible). Identity comparison uses `id`; +/// AST/planner emission uses `canonical`; presentation uses `label`. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub struct LabelDTO { + /// Consumer-local label. Not normalized by the contract. + pub label: String, + /// OGAR codebook binary identity (the classid low u16). + pub id: u16, + /// Canonical-AST label — the portable curator-agnostic symbol. + pub canonical: String, +} + +impl LabelDTO { + /// Build a `LabelDTO` from a **canonical-shaped** concept string. `None` if the + /// concept is not in [`CODEBOOK`]. (Contract counterpart of OGAR's + /// `from_alias`, minus curator-alias normalization — see the module docs: + /// pass a canonical concept, or normalize via `ogar-vocab` first.) + #[must_use] + pub fn from_canonical(concept: impl Into) -> Option { + let canonical = concept.into(); + let id = canonical_concept_id(&canonical)?; + Some(Self { + label: canonical.clone(), + id, + canonical, + }) + } + + /// `id` rendered as **2 little-endian bytes** — the wire contract. Roundtrips + /// via `u16::from_le_bytes`. Byte order matches the [`NodeGuid`](crate::NodeGuid) LE layout, so + /// this is exactly the classid low half on the wire. + #[inline] + #[must_use] + pub fn id_le(&self) -> [u8; 2] { + self.id.to_le_bytes() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::NodeGuid; + + #[test] + fn domain_routes_on_high_byte() { + assert_eq!(canonical_concept_domain(0x0000), ConceptDomain::Reserved); + assert_eq!(canonical_concept_domain(0x0101), ConceptDomain::ProjectMgmt); + assert_eq!(canonical_concept_domain(0x0206), ConceptDomain::Commerce); + assert_eq!(canonical_concept_domain(0x0700), ConceptDomain::Osint); + assert_eq!(canonical_concept_domain(0x0801), ConceptDomain::Ocr); + assert_eq!(canonical_concept_domain(0x0901), ConceptDomain::Health); + assert_eq!(canonical_concept_domain(0x0500), ConceptDomain::Unassigned); + } + + #[test] + fn classid_routes_through_low_u16() { + // The contract classids resolve to the domain their `0xDDCC` low half + // encodes — the contract↔OGAR alignment (ISS-CLASSID-OGAR-DRIFT). + assert_eq!( + classid_concept_domain(NodeGuid::CLASSID_PROJECT), + ConceptDomain::ProjectMgmt + ); + assert_eq!( + classid_concept_domain(NodeGuid::CLASSID_ERP), + ConceptDomain::Commerce + ); + assert_eq!( + classid_concept_domain(NodeGuid::CLASSID_OSINT), + ConceptDomain::Osint + ); + assert_eq!( + classid_concept_domain(NodeGuid::CLASSID_FMA), + ConceptDomain::Health, + "FMA anatomy lives in the Health domain (0x09XX)" + ); + assert_eq!( + classid_concept_domain(NodeGuid::CLASSID_DEFAULT), + ConceptDomain::Reserved + ); + } + + #[test] + fn source_domain_maps_to_concept_domain() { + assert_eq!( + source_domain_concept("project"), + Some(ConceptDomain::ProjectMgmt) + ); + assert_eq!(source_domain_concept("erp"), Some(ConceptDomain::Commerce)); + assert_eq!( + source_domain_concept("german-erp"), + Some(ConceptDomain::Commerce) + ); + assert_eq!(source_domain_concept("nope"), None); + } + + #[test] + fn codebook_ids_match_ogar_vocab() { + // Drift guard: these MUST match OGAR `ogar_vocab::CODEBOOK` exactly (the + // wire is the contract). If OGAR moves an id, update BOTH sides together. + assert_eq!(canonical_concept_id("project"), Some(0x0101)); + assert_eq!(canonical_concept_id("project_work_item"), Some(0x0102)); + assert_eq!(canonical_concept_id("project_enabled_module"), Some(0x011A)); + assert_eq!(canonical_concept_id("commercial_line_item"), Some(0x0201)); + assert_eq!(canonical_concept_id("commercial_document"), Some(0x0202)); + assert_eq!(canonical_concept_id("currency_policy"), Some(0x0206)); + assert_eq!(canonical_concept_id("not_a_concept"), None); + } + + #[test] + fn codebook_has_no_duplicate_ids_or_zero_concept_slot() { + // Every id non-zero in its concept slot (CC != 0x00 — root is reserved), + // every id unique, and each id's domain matches its position. + let mut seen = std::collections::HashSet::new(); + for &(name, id) in CODEBOOK { + assert_ne!( + id & 0x00FF, + 0x00, + "{name}: concept slot CC must be non-zero" + ); + assert!(seen.insert(id), "{name}: duplicate id {id:#06x}"); + } + } + + #[test] + fn label_dto_roundtrips_canonical_and_wire() { + let dto = LabelDTO::from_canonical("project_enabled_module").unwrap(); + assert_eq!(dto.id, 0x011A); + assert_eq!(dto.canonical, "project_enabled_module"); + assert_eq!(dto.id_le(), [0x1A, 0x01]); // LE: low byte (0x1A) first, high (0x01) + assert_eq!(u16::from_le_bytes(dto.id_le()), 0x011A); + // domain reachable from the DTO id + assert_eq!(canonical_concept_domain(dto.id), ConceptDomain::ProjectMgmt); + assert_eq!( + LabelDTO::from_canonical("Issue"), + None, + "curator alias unresolved in contract (normalize via ogar-vocab first)" + ); + } +} diff --git a/crates/lance-graph-contract/src/soa_graph.rs b/crates/lance-graph-contract/src/soa_graph.rs index d7107bac..6c4080ef 100644 --- a/crates/lance-graph-contract/src/soa_graph.rs +++ b/crates/lance-graph-contract/src/soa_graph.rs @@ -108,6 +108,32 @@ pub const FMA_ANATOMY: DomainSpec = DomainSpec { member_edge: "part-of", }; +/// The **project-management** domain (classid [`NodeGuid::CLASSID_PROJECT`], +/// 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, + 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 +/// `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, + name: "ERP", + anchor_families: &[], + in_family_edge: "line-of", + out_family_edge: "paid-by", + member_edge: "in-ledger", +}; + /// The synthetic id of a family node in the snapshot (`"family:RRGGBB"` hex). #[inline] fn family_node_id(family: u32) -> String { @@ -279,9 +305,7 @@ pub fn nearest_anchor(rows: &[NodeRow], domain: &DomainSpec) -> Vec { let mut anchor_paths: Vec<(u32, NiblePath)> = Vec::new(); for row in &domain_rows { let fam = row.key.family(); - if domain.anchor_families.contains(&fam) - && !anchor_paths.iter().any(|(f, _)| *f == fam) - { + if domain.anchor_families.contains(&fam) && !anchor_paths.iter().any(|(f, _)| *f == fam) { anchor_paths.push((fam, hhtl_path(&row.key))); } } @@ -369,10 +393,7 @@ mod tests { .iter() .any(|e| e.label == "references" && e.target == "family:00000b")); // No edge targets an individual member (everything lands on a family node). - assert!(snap - .edges - .iter() - .all(|e| e.target.starts_with("family:"))); + assert!(snap.edges.iter().all(|e| e.target.starts_with("family:"))); } #[test] @@ -496,7 +517,10 @@ mod tests { // GraphSnapshot isn't PartialEq; compare the structural projection. let key = |s: &GraphSnapshot| { ( - s.nodes.iter().map(|n| (n.id.clone(), n.kind.clone())).collect::>(), + s.nodes + .iter() + .map(|n| (n.id.clone(), n.kind.clone())) + .collect::>(), s.edges .iter() .map(|e| (e.source.clone(), e.target.clone(), e.label.clone()))