diff --git a/.claude/board/AGENT_LOG.md b/.claude/board/AGENT_LOG.md index 5dc5ce28..5098df77 100644 --- a/.claude/board/AGENT_LOG.md +++ b/.claude/board/AGENT_LOG.md @@ -1,3 +1,31 @@ +## 2026-06-20 (cont.¹⁵) — PR #564 codex P2 fixes (contract-source unification + build-time parity fuse) + +**Main thread (Opus), autoattended.** Two codex P2 review comments on #564, both correct, both fixed: +1. **One contract source at the integration boundary** (Cargo.toml): `lance-graph` + `symbiont` path-dep `lance-graph-contract`; `lance-graph-ogar` git-dep'd it → two distinct crates when composed → `OgarClassView` would impl the git `ClassView`, not the path one. Fix: `lance-graph-ogar` now **path-deps** `../lance-graph-contract` (the canonical in-repo copy) + a `[patch."…/lance-graph"] lance-graph-contract = { path = … }` folds ogar-class-view's transitive git contract onto the SAME path copy = ONE source. Documented the cargo limitation: an in-repo workspace root (symbiont) adding this crate MUST repeat the patch (`[patch]` only applies at the root). +2. **Parity guard only ran under `#[cfg(test)]`** (lib.rs) → a downstream `cargo build` never executed it, contradicting the "fails the build on drift" claim. Fix: added a **compile-time length fuse** `parity::COUNT_FUSE` (`const _ = assert!(mirror::CODEBOOK.len() == ogar_vocab::class_ids::ALL.len())`) that fires in ANY build (cargo build included) on add/remove drift; kept the runtime `assert_codebook_parity` (full id/domain bijection, tested + callable at startup); corrected the docs to state both depths precisely (no overclaim). + +Re-verified: `cargo test --manifest-path crates/lance-graph-ogar/Cargo.toml` 3/3 (the `ogar_class_view_implements_contract_class_view` test compiling PROVES one-source unification), clippy `-D warnings` + fmt clean, no "patch not used". Both threads replied + resolved. Pushed to #564. + +## 2026-06-20 (cont.¹⁴) — zero-copy SoA read contract (`node_rows_from_le_bytes`) — the surrealdb "second brain" primitive + +**Main thread (Opus), autoattended.** Operator: "create a contract … that ensures LE contract to the lance-graph SoA view → zero-copy symbiont; surrealdb becomes a second brain inside lance-graph." Brutal feasibility pass against real code on both sides (see EPIPHANY `E-SURREALDB-SECOND-BRAIN-IS-ZERO-COPY-IFF-FIXEDSIZEBINARY`): +- lance-graph side already zero-copy-ready: `NodeRow` `#[repr(C, align(64))]` 512B LE; `NodeRowPacket::as_le_bytes` is the WRITE cast. **Shipped the missing READ inverse:** `canonical_node::node_rows_from_le_bytes(&[u8]) -> Option<&[NodeRow]>` — checked (`len % 512`, `ptr % 64`), `None` on violation (caller copies, no UB), empty→Some(empty). Re-exported from lib.rs; +`NodeRowPacket` re-export. 2 tests (zero-copy round-trip with ptr-identity assert; rejects non-multiple + misaligned-but-correct-length window). 712 lib green, clippy `-D warnings` both configs + fmt clean. +- surrealdb side does NOT yet qualify: `.claude/lance-backend/lance/schema.rs` stores `val: DataType::Binary` (variable BinaryArray, no fixed stride / no align) → not castable. Needs `FixedSizeBinary(512)` SoA value column + deps the zero-dep contract + reads through `node_rows_from_le_bytes`. Caveat: value zero-copy iff stored UNcompressed (compressed = one decode-copy; key always zero-copy). That's the surrealdb-side plan (the lance-backend wiring), not done here. + +Rides on the jirak branch (PR #564 arc — the symbiont contract surface: OGAR activation + SoA zero-copy reader). Next: the surrealdb-side FixedSizeBinary(512) SoA path plan. + +## 2026-06-20 (cont.¹³) — clean separation: NEW `lance-graph-ogar` activation crate (OGAR AR surface), #563 merged + +**Main thread (Opus), autoattended.** Operator: "what about clean separation — lance-graph-ontology OGIT / lance-graph-ogar OGAR" + correction "OGAR isn't just vocab, it's classes, ClassView, active-record shape" + "563 merged". Rebased jirak onto new main (ff1a3452 = merged #563, so `contract::ogar_codebook` is now ON main). + +**Discovery (consult-don't-guess):** OGAR is the Active-Record Core and ALREADY speaks the contract — `ogar-class-view::OgarClassView` already `impl lance_graph_contract::ClassView` (32 promoted concepts → ObjectView/render_rows), git-depping `lance-graph-contract@main`. `ogar-vocab::Class` = the AR shape (attrs + family Associations); `canonical_concept_id == ClassId == NodeGuid.classid` low u16. So the AR bridge already exists OGAR-side; the lance-graph side just needs ACTIVATION, not a new bridge. Also: `ogar-ontology` is zero-dep; `ogar-adapter-surrealql` default = light `emit()` DDL (no surrealdb); its `surrealdb-parser` feature (the `unmap()` half, rust 1.95+) is the only heavy part. + +**NEW `crates/lance-graph-ogar`** (operator chose "Full AR surface"): re-exports ogar-vocab + ogar-class-view + ogar-ontology + ogar-adapter-surrealql under stable names + `OgarClassView`/`Class` + `contract` passthrough; hosts the **parity-guard** (`parity::assert_codebook_parity`: bijective `contract::ogar_codebook::CODEBOOK ⇄ ogar_vocab::class_ids::ALL` + domain agreement) that FAILS THE BUILD on codebook drift. Features: `default = []` (light: all four crates, emit-only), `surrealql-parser` (the surrealdb parser half), `serde` passthrough. **EXCLUDED** from the workspace with its own `[workspace]` root; **git-deps OGAR@main + lance-graph-contract@main = ONE contract source** (same as ogar-class-view's) so the `impl ClassView` matches the trait the guard checks — NO `[patch]` (the symbiont alignment; a path+git mix would be two distinct contract types). This is the clean separation: `lance-graph-ontology = OGIT` (TTL spine), `lance-graph-ogar = OGAR` (AR surface), `contract` = zero-dep traits + the OGAR-absent `ogar_codebook` mirror. + +**Auto-activation = Cargo presence** (no runtime detection): a build pulling `lance-graph-ogar` (golden image via symbiont, or q2/medcare) gets the REAL OGAR Class/ClassView/codebook + full `from_alias` normalizer + the drift fuse; a build without it carries the contract's zero-dep mirror + the bare `ClassView` trait (OGAR stays headless-capable — depending on the zero-dep contract is the compile-time handshake, not "needing lance-graph"). + +Verified: `cargo test --manifest-path crates/lance-graph-ogar/Cargo.toml` **3/3** (parity bijection ≥32 concepts, classid↔codebook-id identity, OgarClassView-is-a-ClassView); `lance-graph-contract` resolved to ONE source (git main #ff1a3452); clippy `-D warnings --all-targets` + fmt clean. Added to workspace `exclude` (next to symbiont). New PR off main (jirak). EPIPHANY `E-OGAR-IS-AR-CORE-AUTOACTIVATED-BY-CARGO-PRESENCE`; plan D-OVC-5. + ## 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. diff --git a/.claude/board/EPIPHANIES.md b/.claude/board/EPIPHANIES.md index 9758a3ef..2f121dcc 100644 --- a/.claude/board/EPIPHANIES.md +++ b/.claude/board/EPIPHANIES.md @@ -1,3 +1,37 @@ +## 2026-06-20 — E-SURREALDB-SECOND-BRAIN-IS-ZERO-COPY-IFF-FIXEDSIZEBINARY — surrealdb (kv-lance) can become a zero-copy "second brain" inside lance-graph ONLY if it stores each node as an uncompressed `FixedSizeBinary(512)` LE blob; the contract a store satisfies is `node_rows_from_le_bytes(&[u8]) -> Option<&[NodeRow]>` (the inverse of `NodeRowPacket::as_le_bytes`), and a variable-length `Binary` column does NOT qualify + +**Status:** FINDING (brutal feasibility pass, 2026-06-20; contract primitive shipped, surrealdb side planned). + +Operator proposal: "a contract in surrealdb that ensures LE contract to the lance-graph SoA view → zero-copy symbiont; surrealdb becomes a second brain inside lance-graph." Verified against the real code on both sides: + +**lance-graph side — already zero-copy-ready:** `NodeRow` is `#[repr(C, align(64))]`, 512 bytes (const-asserted), key(16)+edges(16)+value(480), all canon-LE. `NodeRowPacket::as_le_bytes` already casts `&[NodeRow] → &[u8]` (the WRITE path). The missing piece — the READ path — is now shipped: **`node_rows_from_le_bytes(&[u8]) -> Option<&[NodeRow]>`** (checked: `len % 512 == 0` AND `ptr % 64 == 0`, else `None` → caller copies rather than risk UB). That function IS the LE contract a backing store satisfies. + +**surrealdb side — NOT zero-copy as scaffolded:** the `.claude/lance-backend` Arrow schema stores `val: DataType::Binary` (variable-length `BinaryArray` = offsets + an unaligned, non-fixed-stride value buffer). That **cannot** be cast to `&[NodeRow]`. The SoA value path needs `DataType::FixedSizeBinary(512)` (a single contiguous N×512 buffer, which arrow-rs allocates 64-byte aligned) so the column buffer IS a `&[NodeRow]` in place. + +**The two honest caveats (brutal):** +1. **Zero-copy on the VALUE requires the column be UNcompressed.** A Lance-compressed `FixedSizeBinary` decodes to a contiguous buffer first — that's ONE copy (still no per-field deserialize, far better than serde, but not literally zero). The KEY (16 bytes) is always addressable zero-copy (OGAR canon: "the key is never compressed"). So: zero-copy address always; zero-copy value iff stored uncompressed. +2. **Direction of the contract:** the LE layout is OWNED by `lance-graph-contract` (`NodeRow` + `node_rows_from_le_bytes`); surrealdb SATISFIES it by depending on the **zero-dep** contract (the handshake, not the engine — OGAR-pattern) and storing `FixedSizeBinary(512)`. "Second brain" = surrealdb's kv-lance bytes ARE the SoA the cognitive shader reads in place; surrealdb adds SurrealQL + MVCC + Lance versioning over the SAME bytes, no serialize boundary. + +Shipped: `node_rows_from_le_bytes` + round-trip/reject tests (712 lib green, clippy+fmt clean). Planned: surrealdb-side `FixedSizeBinary(512)` SoA value path + read-through (the `.claude/lance-backend` 12-day wiring). Cross-ref: AGENT_LOG 2026-06-20 (cont.¹⁴); `soa_envelope::SoaEnvelope`; surrealdb `.claude/lance-backend/lance/schema.rs`; OGAR canon "the GUID is the key of key-value … the key is never compressed." + +## 2026-06-20 — E-OGAR-IS-AR-CORE-AUTOACTIVATED-BY-CARGO-PRESENCE — OGAR is the Active-Record Core (Class + ClassView), not "just vocab"; it already `impl`s the contract's `ClassView`, so a lance-graph-side `lance-graph-ogar` crate AUTO-ACTIVATES the real AR surface wherever it is compiled in (Cargo presence = the switch, no runtime detection), guarded against drift by a parity fuse — while OGAR stays headless-capable and the contract stays zero-dep + +**Status:** FINDING (operator clean-separation lock, 2026-06-20; shipped `crates/lance-graph-ogar`). + +Two corrections crystallised the design: + +1. **OGAR ≠ codebook.** OGAR = *Open Graph of Active Record*: the unit is the **`Class`** (canonical concept + typed attributes + family-edge `Association`s) and its **`ClassView`**. The `u16` codebook id is ONE facet of a Class's identity (`canonical_concept_id == ClassId == NodeGuid.classid` low u16). A bridge that only carried the id would be exactly the "make OGAR stupid" failure the operator warned against. +2. **The AR bridge already exists, OGAR-side.** `ogar-class-view::OgarClassView` already `impl lance_graph_contract::ClassView` (32 promoted concepts → `ObjectView`/`render_rows`), git-depping `lance-graph-contract@main`. So OGAR already *speaks the contract*; depending on the **zero-dep** contract is the compile-time handshake, NOT "needing lance-graph" (contracts compile types, never serialize). OGAR stays fully headless. + +**The clean separation (two crates, two responsibilities):** +- `lance-graph-ontology` = **OGIT** (TTL/RDF hydration — the ontology SOURCE). +- `lance-graph-ogar` = **OGAR** (re-export + activation of the full AR surface: ogar-vocab Class/codebook + ogar-class-view ClassView + ogar-ontology + ogar-adapter-surrealql). +- `lance-graph-contract` keeps the **zero-dep `ogar_codebook` mirror** as the OGAR-ABSENT baseline + the `ClassView`/`ObjectView` traits OGAR implements. + +**Auto-activation = Cargo presence (no runtime detection).** A build graph that pulls `lance-graph-ogar` (golden image via symbiont, or q2/medcare) gets the REAL OGAR Class/ClassView/codebook + full `from_alias` normalizer + the **parity-guard** (`assert_codebook_parity`: bijective `mirror ⇄ ogar_vocab::class_ids::ALL` + domain agreement) that FAILS THE BUILD on drift. A build without it carries the lean mirror + the bare `ClassView` trait and never knows the difference. **One contract source** is the load-bearing constraint: `lance-graph-ogar` AND `ogar-class-view` both resolve `lance-graph-contract` to git `#main` (no `[patch]`, the symbiont alignment), so the `impl ClassView` matches the trait the guard checks (path + git copies would be two distinct types). The crate is `exclude`d from the workspace (git OGAR deps), built via `--manifest-path`. + +Verified: `cargo test --manifest-path crates/lance-graph-ogar/Cargo.toml` 3/3 (parity bijection over ≥32 concepts + classid↔codebook-id identity + OgarClassView-is-a-ClassView), clippy `-D warnings` + fmt clean; resolved `lance-graph-contract` to ONE source (git main #ff1a3452 = merged #563). Cross-ref: AGENT_LOG 2026-06-20 (cont.¹³), plan `ogar-vocab-contract-codebook-migration-v1` D-OVC-5; the `from_alias`-vs-`from_canonical` split (E-... ogar_codebook mirror, #563). + ## 2026-06-20 — E-UNIFORM-MORTON-TILE-PYRAMID — making every GUID tier the same size (8×u16) makes the KEY, the per-family CODEBOOK, the VALUE tile, and the PERTURBATION pyramid all the SAME 2bit×2bit 4×4 Morton-tile primitive — so one kernel (Morton + AMX 4×4 BF16 GEMM), one distance (Morton common-prefix = HHTL hop), and one codebook shape (256×256 per tier) govern the whole substrate **Status:** FINDING (operator design lock, GUID-v2-tail, 2026-06-20). diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index 5cde3942..6146c965 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -16,7 +16,11 @@ --- -> **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`)** — **Zero-copy SoA read contract: `node_rows_from_le_bytes` (the surrealdb "second brain" primitive).** The inverse of `NodeRowPacket::as_le_bytes` (WRITE) — `canonical_node::node_rows_from_le_bytes(&[u8]) -> Option<&[NodeRow]>`, a CHECKED zero-copy cast (`len % 512 == 0` AND `ptr % 64 == 0`, else `None` → caller copies, no UB; empty→Some(empty)). This IS the LE contract a backing store satisfies so its bytes ARE the SoA the cognitive shader reads in place. **Brutal verdict:** lance-graph side now zero-copy-ready end-to-end; surrealdb's kv-lance does NOT qualify as scaffolded (`val: DataType::Binary` variable-length → needs `FixedSizeBinary(512)`), and value zero-copy holds only if stored UNcompressed (key/address always zero-copy). 712 contract lib green, clippy `-D warnings` both configs + fmt clean. Refs: AGENT_LOG 2026-06-20 (cont.¹⁴), EPIPHANIES `E-SURREALDB-SECOND-BRAIN-IS-ZERO-COPY-IFF-FIXEDSIZEBINARY`. +> +> **2026-06-20 — IN PR (`claude/jirak-math-theorems-harvest-rfii13`)** — **Clean separation: NEW `lance-graph-ogar` activation crate (OGAR Active-Record surface).** The OGAR half of `ontology=OGIT / ogar=OGAR`. OGAR is the AR Core and ALREADY `impl`s the contract: `ogar-class-view::OgarClassView impl lance_graph_contract::ClassView` (32 concepts), `ogar-vocab::Class` = AR shape, `canonical_concept_id == ClassId`. NEW `crates/lance-graph-ogar` (EXCLUDED, own `[workspace]`, git-deps OGAR@main + lance-graph-contract@main = ONE source, no `[patch]`) re-exports the full AR surface (ogar-vocab + ogar-class-view + ogar-ontology + ogar-adapter-surrealql) + a **parity-guard** (`assert_codebook_parity`: bijective `ogar_codebook::CODEBOOK ⇄ ogar_vocab::class_ids::ALL` + domain agreement, FAILS build on drift). Features: `default` (light, emit-only), `surrealql-parser` (parser half), `serde`. **Auto-activation = Cargo presence**: pull the crate → real OGAR AR + drift fuse; don't → contract's zero-dep mirror + bare ClassView trait (OGAR stays headless). `cargo test --manifest-path crates/lance-graph-ogar/Cargo.toml` **3/3** green, clippy + fmt clean, contract = ONE source (git main #ff1a3452). Refs: AGENT_LOG 2026-06-20 (cont.¹³), EPIPHANIES `E-OGAR-IS-AR-CORE-AUTOACTIVATED-BY-CARGO-PRESENCE`, plan D-OVC-5. **(#563 D-OVC contract realign now MERGED to main.)** +> +> **2026-06-20 — MERGED #563 (`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`. > diff --git a/.claude/plans/ogar-vocab-contract-codebook-migration-v1.md b/.claude/plans/ogar-vocab-contract-codebook-migration-v1.md index f27094ba..935ed24c 100644 --- a/.claude/plans/ogar-vocab-contract-codebook-migration-v1.md +++ b/.claude/plans/ogar-vocab-contract-codebook-migration-v1.md @@ -112,6 +112,22 @@ class-identity codebook. Reconcile onto OGAR's `0xDDCC` scheme: 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). +- **D-OVC-5** ✅ **SHIPPED — clean separation `ontology=OGIT / ogar=OGAR`.** NEW + `crates/lance-graph-ogar` (EXCLUDED, own `[workspace]`, git-deps OGAR@main + + lance-graph-contract@main = ONE source, no `[patch]`): re-exports the **full OGAR + Active-Record surface** (`ogar-vocab` Class/codebook + `ogar-class-view` + `OgarClassView impl ClassView` + `ogar-ontology` + `ogar-adapter-surrealql`) + + the **codebook parity-guard** (`parity::assert_codebook_parity` — bijective + `ogar_codebook::CODEBOOK ⇄ ogar_vocab::class_ids::ALL` + domain agreement, fails + the build on drift). **Auto-activation = Cargo presence** (no runtime detection): + pulling the crate (golden image via symbiont, or q2/medcare) lights up the real + OGAR AR surface + the drift fuse; OGAR stays headless-capable (its only + lance-graph dep is the zero-dep contract — the compile-time handshake). Features + `default` (light emit-only) / `surrealql-parser` (the heavy `unmap()` parser + half, rust 1.95+) / `serde`. Verified 3/3 + clippy + fmt clean. This realizes the + §5-decision-2 "(a) consolidation" path the wire-compat baseline deferred — both + now coexist: contract mirror = OGAR-absent baseline, `lance-graph-ogar` = the + OGAR-present activation. EPIPHANY `E-OGAR-IS-AR-CORE-AUTOACTIVATED-BY-CARGO-PRESENCE`. ## 5 — Decisions needed (operator) — ✅ RESOLVED 2026-06-20 diff --git a/Cargo.toml b/Cargo.toml index 6663e7d0..268d546f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,6 +80,14 @@ exclude = [ # the default build). Verify via `cargo build --manifest-path crates/symbiont/Cargo.toml` # or `docker build -f crates/symbiont/Dockerfile -t symbiont .`. "crates/symbiont", + # OGAR (Open Graph of Active Record) activation crate — re-exports OGAR's full + # AR surface (ogar-vocab Class/codebook + ogar-class-view impl ClassView + + # ogar-ontology + ogar-adapter-surrealql) and hosts the codebook parity-guard. + # EXCLUDED (own [workspace], git-deps OGAR + lance-graph-contract@main = one + # source, no [patch]) so OGAR never enters the default build; the lance-graph + # half of the OGIT(ontology)/OGAR(ogar) clean separation. Verify via + # `cargo test --manifest-path crates/lance-graph-ogar/Cargo.toml`. + "crates/lance-graph-ogar", ] resolver = "2" diff --git a/crates/lance-graph-contract/src/canonical_node.rs b/crates/lance-graph-contract/src/canonical_node.rs index 2673321a..daeeba41 100644 --- a/crates/lance-graph-contract/src/canonical_node.rs +++ b/crates/lance-graph-contract/src/canonical_node.rs @@ -968,10 +968,114 @@ impl<'a> SoaEnvelope for NodeRowPacket<'a> { } } +/// Zero-copy **read** of a [`NodeRow`] slice out of an external LE byte buffer — +/// the inverse of [`NodeRowPacket::as_le_bytes`] and the load-bearing primitive +/// for "a store hands lance-graph its SoA view without a copy." +/// +/// This is the **LE contract a backing store satisfies**: hand a byte slice that +/// is (a) a whole number of 512-byte rows and (b) aligned to +/// `align_of::()` (64), and you get back a `&[NodeRow]` viewing the SAME +/// bytes — no allocation, no deserialize. Returns `None` if either invariant +/// fails (a sliced/offset buffer that lost 64-byte alignment, or a length that +/// isn't a multiple of the stride), so the caller can fall back to a copy rather +/// than risk UB. +/// +/// Intended consumer: a Lance-backed key-value store (e.g. surrealdb's `kv-lance`) +/// that persists each node as a fixed-size 512-byte LE blob +/// (`arrow::FixedSizeBinary(512)`, whose value buffer arrow-rs allocates 64-byte +/// aligned). The store's value buffer is then directly a `&[NodeRow]` the +/// cognitive shader reads in place — surrealdb's bytes ARE the SoA. (A +/// *variable-length* `Binary` column does NOT qualify: it has no fixed stride and +/// no alignment guarantee; the store must use `FixedSizeBinary(512)` for the SoA +/// value path. And the buffer must be uncompressed for the read to be literally +/// zero-copy — a Lance-compressed column decodes to a contiguous buffer first, +/// which is one copy, still no per-field deserialize.) +/// +/// The bytes are interpreted in canon-LE order exactly as [`NodeGuid`]/[`EdgeBlock`] +/// wrote them, so no endianness translation happens at the boundary. +#[inline] +#[must_use] +pub fn node_rows_from_le_bytes(bytes: &[u8]) -> Option<&[NodeRow]> { + if bytes.is_empty() { + return Some(&[]); + } + if !bytes.len().is_multiple_of(NODE_ROW_STRIDE) { + return None; + } + if !(bytes.as_ptr() as usize).is_multiple_of(core::mem::align_of::()) { + return None; + } + let n = bytes.len() / NODE_ROW_STRIDE; + // SAFETY: NodeRow is #[repr(C, align(64))], size_of == 512 == NODE_ROW_STRIDE + // (const-asserted above). We checked (1) bytes.len() is an exact multiple of + // the stride, so n rows span the whole slice with no trailing bytes, and (2) + // the pointer is aligned to align_of::() (64). Every bit pattern in + // the 512 bytes is a valid NodeRow (NodeGuid is bytes, EdgeBlock is [u8;16], + // value is [u8;480] — no niche/enum to invalidate), so the reinterpretation + // is sound. The returned slice borrows `bytes` for its lifetime (no copy). + Some(unsafe { core::slice::from_raw_parts(bytes.as_ptr().cast::(), n) }) +} + #[cfg(test)] mod tests { use super::*; + #[test] + fn node_rows_le_bytes_round_trip_zero_copy() { + // Build a small SoA, view it as LE bytes (the write path), then read it + // back as &[NodeRow] (the inverse) — same bytes, no copy. + let rows = vec![ + NodeRow { + key: NodeGuid::new(NodeGuid::CLASSID_OSINT, 1, 2, 3, 0xAB, 0xCD), + edges: EdgeBlock::default(), + value: [7u8; 480], + }, + NodeRow { + key: NodeGuid::new(NodeGuid::CLASSID_PROJECT, 4, 5, 6, 0x11, 0x22), + edges: EdgeBlock::default(), + value: [9u8; 480], + }, + ]; + let packet = NodeRowPacket::new(&rows, 0); + let bytes = packet.as_le_bytes(); + assert_eq!(bytes.len(), 2 * NODE_ROW_STRIDE); + + let view = node_rows_from_le_bytes(bytes).expect("aligned, 512-multiple"); + assert_eq!(view.len(), 2); + assert_eq!(view[0].key.classid(), NodeGuid::CLASSID_OSINT); + assert_eq!(view[1].key.classid(), NodeGuid::CLASSID_PROJECT); + assert_eq!(view[0].value, [7u8; 480]); + // Truly zero-copy: the view aliases the SAME backing store as `rows`. + assert_eq!(view.as_ptr().cast::(), rows.as_ptr().cast::()); + } + + #[test] + fn node_rows_from_le_bytes_rejects_bad_inputs() { + let rows = vec![ + NodeRow { + key: NodeGuid::local(1), + edges: EdgeBlock::default(), + value: [0u8; 480], + }, + NodeRow { + key: NodeGuid::local(2), + edges: EdgeBlock::default(), + value: [0u8; 480], + }, + ]; + let packet = NodeRowPacket::new(&rows, 0); + let bytes = packet.as_le_bytes(); // 1024 bytes, 64-aligned + // empty → Some(empty) + assert_eq!(node_rows_from_le_bytes(&[]).map(<[_]>::len), Some(0)); + // not a whole number of rows → None (length check) + assert!(node_rows_from_le_bytes(&bytes[..NODE_ROW_STRIDE - 1]).is_none()); + // a 512-length window offset by 1 off the 64-aligned base: correct length + // but misaligned → None via the alignment check (no UB cast). + let misaligned = &bytes[1..1 + NODE_ROW_STRIDE]; + assert_eq!(misaligned.len(), NODE_ROW_STRIDE); + assert!(node_rows_from_le_bytes(misaligned).is_none()); + } + #[test] fn defaults_are_zero_and_bootstrap() { let g = NodeGuid::local(0x00_00CD); diff --git a/crates/lance-graph-contract/src/lib.rs b/crates/lance-graph-contract/src/lib.rs index 1714c1a7..4b2ce8c2 100644 --- a/crates/lance-graph-contract/src/lib.rs +++ b/crates/lance-graph-contract/src/lib.rs @@ -121,8 +121,8 @@ pub mod world_model; // Re-exports for the most commonly used collapse_gate types. pub use canonical_node::{ - classid_read_mode, EdgeBlock, EdgeCodecFlavor, GuidParts, NodeGuid, NodeRow, ReadMode, - ValueSchema, ValueTenant, VALUE_TENANTS, + classid_read_mode, node_rows_from_le_bytes, EdgeBlock, EdgeCodecFlavor, GuidParts, NodeGuid, + NodeRow, NodeRowPacket, ReadMode, ValueSchema, ValueTenant, VALUE_TENANTS, }; pub use class_view::{ClassId, ClassProjection, ClassView, FieldMask, RenderRow}; pub use collapse_gate::{GateDecision, MailboxId, MergeMode}; diff --git a/crates/lance-graph-ogar/.gitignore b/crates/lance-graph-ogar/.gitignore new file mode 100644 index 00000000..1f1ecc31 --- /dev/null +++ b/crates/lance-graph-ogar/.gitignore @@ -0,0 +1,7 @@ +# Activation crate (re-resolves OGAR + lance-graph-contract to branch `main` on +# every build) — do NOT commit Cargo.lock. Pinning would freeze the git-deps to +# specific revs (the snapshot anti-pattern); the activation must reflect the +# CURRENT OGAR AR surface + contract codebook so the parity-guard checks live +# state. Same rationale as crates/symbiont/.gitignore (E-GOLDEN-IMAGE-IS-A-LIVING-HARNESS). +/Cargo.lock +/target diff --git a/crates/lance-graph-ogar/Cargo.toml b/crates/lance-graph-ogar/Cargo.toml new file mode 100644 index 00000000..3713c4c3 --- /dev/null +++ b/crates/lance-graph-ogar/Cargo.toml @@ -0,0 +1,91 @@ +# lance-graph-ogar — the OGAR (Open Graph of Active Record) activation crate. +# EXCLUDED from the lance-graph workspace (own [workspace] root below). +# +# Clean separation (operator, 2026-06-20): +# lance-graph-ontology = OGIT (TTL/RDF hydration spine) +# lance-graph-ogar = OGAR (Active-Record Class / ClassView / adapters) +# +# This crate is the lance-graph-side ACTIVATION + re-export of OGAR's full AR +# surface. OGAR is the Active-Record Core and already speaks the contract: +# ogar-vocab::Class = the calcified AR shape (attributes + family +# Associations); `canonical_concept_id` == ClassId +# ogar-class-view::OgarClassView impl lance_graph_contract::ClassView +# (32 promoted concepts → ObjectView/render_rows) +# ogar-ontology = prefix conventions + NiblePath identity routing +# ogar-adapter-surrealql = emit(Class) -> SurrealQL DDL (the DO arm) +# +# Auto-activation = Cargo presence: a build graph that pulls THIS crate gets the +# real OGAR Class/ClassView/codebook + the parity-guard (lib.rs): a COMPILE-TIME +# length fuse (`const _` assert that the mirror and `ogar_vocab::class_ids::ALL` +# have equal count — fires in ANY build, `cargo build` included) plus a runtime +# `parity::assert_codebook_parity()` for the full id/domain bijection (call it at +# consumer startup; also asserted by this crate's tests = the CI gate). A build +# WITHOUT this crate uses the contract's zero-dep mirror + the bare ClassView +# trait. OGAR never depends on the lance-graph ENGINE — only the zero-dep contract. +# +# ── ONE contract source (codex P2, PR #564) ── +# The canonical `lance-graph-contract` in-repo is the PATH copy (`crates/ +# lance-graph-contract`) — what `lance-graph` and `symbiont` already path-dep. +# `ogar-class-view` (in the OGAR repo) hard-codes a `git` dep on +# `AdaWorldAPI/lance-graph#main` for ITS standalone build; Cargo treats path and +# git as DISTINCT crates even at the same version, so `OgarClassView` would impl a +# DIFFERENT `ClassView` than the engine expects. The `[patch]` below redirects +# that transitive git contract onto the SAME path copy, so this crate's standalone +# build has exactly one contract source = the path copy. +# +# CONSUMER REQUIREMENT (cargo limitation): `[patch]` only applies in the WORKSPACE +# ROOT. When an in-repo workspace root (e.g. `crates/symbiont`) adds +# `lance-graph-ogar`, THIS crate's `[patch]` is ignored — that root MUST repeat: +# [patch."https://github.com/AdaWorldAPI/lance-graph"] +# lance-graph-contract = { path = "../lance-graph-contract" } +# to unify ogar-class-view's transitive git contract onto its path copy. Without +# it the OgarClassView `impl ClassView` won't typecheck against the engine's. +# +# Build/verify: cargo test --manifest-path crates/lance-graph-ogar/Cargo.toml +# Full DO arm : cargo test --manifest-path crates/lance-graph-ogar/Cargo.toml \ +# --features surrealql-parser (pulls surrealdb-ast/parser, rust 1.95+) + +[package] +name = "lance-graph-ogar" +version = "0.1.0" +edition = "2021" +publish = false +description = "OGAR (Open Graph of Active Record) activation crate: re-exports ogar-vocab Class/codebook + ogar-class-view (impl lance_graph_contract::ClassView) + ogar-ontology + ogar-adapter-surrealql, with a codebook parity-guard against the contract's zero-dep ogar_codebook mirror. Auto-activates the real OGAR AR surface wherever it is compiled into the build (golden image / AR-aware consumers)." +license = "Apache-2.0" + +# Own workspace root — keeps this crate out of the parent lance-graph workspace. +[workspace] + +[features] +default = [] +# The unmap(SurrealQL) -> Class half of the DO arm: pulls surrealdb-ast + +# surrealdb-parser (heavy; rust-version 1.95+). The emit() DDL formatter half is +# always available (no surrealdb deps). +surrealql-parser = ["ogar-adapter-surrealql/surrealdb-parser"] +# serde passthrough for the OGAR IR types. +serde = ["ogar-vocab/serde", "ogar-adapter-surrealql/serde"] + +[dependencies] +# Contract surface (zero-dep): ClassView / ObjectView / ClassId / NodeGuid + the +# ogar_codebook wire-compat mirror the parity-guard checks. PATH copy = the +# canonical in-repo source (same as lance-graph / symbiont); the [patch] below +# folds ogar-class-view's transitive git contract onto it = ONE source. +lance-graph-contract = { path = "../lance-graph-contract" } + +# ── OGAR Active-Record forks: git deps @ main (the canonical superset; matches +# symbiont's pins so they resolve to ONE source in the golden image) ── +ogar-vocab = { git = "https://github.com/AdaWorldAPI/OGAR", branch = "main" } +ogar-class-view = { git = "https://github.com/AdaWorldAPI/OGAR", branch = "main" } +ogar-ontology = { git = "https://github.com/AdaWorldAPI/OGAR", branch = "main" } +# default features only = the light emit() DDL formatter (no surrealdb tree); +# the parser half is opt-in via this crate's `surrealql-parser` feature. +ogar-adapter-surrealql = { git = "https://github.com/AdaWorldAPI/OGAR", branch = "main" } + +# Fold ogar-class-view's transitive `lance-graph-contract` (git +# AdaWorldAPI/lance-graph#main) onto the SAME path copy this crate uses, so there +# is exactly ONE contract crate and `OgarClassView`'s `impl ClassView` is for it +# (codex P2, PR #564). Applies in THIS crate's standalone build (its own +# [workspace] root); an in-repo workspace root that adds this crate must repeat +# the patch (see the CONSUMER REQUIREMENT note in the header). +[patch."https://github.com/AdaWorldAPI/lance-graph"] +lance-graph-contract = { path = "../lance-graph-contract" } diff --git a/crates/lance-graph-ogar/src/lib.rs b/crates/lance-graph-ogar/src/lib.rs new file mode 100644 index 00000000..c2278fd4 --- /dev/null +++ b/crates/lance-graph-ogar/src/lib.rs @@ -0,0 +1,188 @@ +//! `lance-graph-ogar` — the OGAR (Open Graph of Active Record) activation crate. +//! +//! The lance-graph-side **re-export + activation** of OGAR's full Active-Record +//! surface, the OGAR half of the clean separation (operator, 2026-06-20): +//! +//! ```text +//! lance-graph-ontology = OGIT (TTL/RDF hydration spine — the ontology SOURCE) +//! lance-graph-ogar = OGAR (Active-Record Class / ClassView / adapters) +//! ``` +//! +//! # OGAR is the Active-Record Core, and it already speaks the contract +//! +//! OGAR is **not** "just a codebook" — the unit is the **`Class`** and its +//! **`ClassView`** (the active-record shape: identity, state, relations, +//! composition). The codebook `u16` is one *facet* of a `Class`'s identity. +//! +//! - [`ogar_vocab::Class`] — the calcified AR shape: canonical concept + typed +//! attributes + family-edge `Association`s. `canonical_concept_id` == the +//! contract [`ClassId`](lance_graph_contract::class_view::ClassId). +//! - [`ogar_class_view::OgarClassView`] — **`impl lance_graph_contract::ClassView`**: +//! builds an `ObjectView` per promoted concept, keyed by `ClassId`, exposing the +//! whole 32-concept AR set through the contract's runtime projection trait +//! (`render_rows(id, mask)`). +//! - [`ogar_ontology`] — prefix conventions + NiblePath identity routing. +//! - [`ogar_adapter_surrealql`] — `emit(Class) -> SurrealQL DDL` (the DO arm); +//! the `unmap(SurrealQL) -> Class` parser half is behind `surrealql-parser`. +//! +//! OGAR depending on `lance-graph-contract` (the **zero-dep** trait crate) is +//! *not* "needing lance-graph" — contract is the compile-time handshake (the +//! "contracts compile types, never serialize" principle). OGAR stays fully +//! **headless-capable**: a build without this crate uses the contract's zero-dep +//! [`ogar_codebook`](lance_graph_contract::ogar_codebook) mirror + the bare +//! `ClassView` trait; OGAR's own crates never depend on the lance-graph engine. +//! +//! # Auto-activation = Cargo presence (no runtime detection) +//! +//! A build graph that pulls THIS crate (the golden image via `symbiont`, or any +//! AR-aware consumer — q2, medcare, …) gets the **real** OGAR `Class`/`ClassView`/ +//! codebook (including [`ogar_vocab`]'s full curator-alias normalizer, so OGAR is +//! never dumbed down) **plus** the [`parity`] guard. The guard fires at two depths +//! so it cannot be silently bypassed (codex P2, PR #564): +//! - a **compile-time length fuse** ([`parity`] `const _`) that fails ANY build +//! (`cargo build` included) if the mirror and `ogar_vocab::class_ids::ALL` have +//! a different concept count — the most common drift (add/remove a concept); +//! - a **runtime full-bijection** check ([`parity::assert_codebook_parity`]) for +//! id + domain agreement, asserted by this crate's tests (the CI gate) and +//! callable at consumer startup. +//! +//! One contract source: this crate path-deps `lance-graph-contract` (the canonical +//! in-repo copy) and a `[patch]` folds `ogar-class-view`'s transitive *git* +//! contract onto the SAME path copy, so the `OgarClassView` `impl ClassView` is for +//! the contract the guard checks (an in-repo workspace root adding this crate must +//! repeat that patch — see the manifest CONSUMER REQUIREMENT note). +//! +//! # The OGIT ↔ OGAR seam +//! +//! `lance-graph-ontology` (OGIT) hydrates classes from TTL; OGAR mints the +//! calcified canonical concepts (`class_ids::ALL`) keyed by the same `ClassId` +//! space. They meet at the codebook id == `NodeGuid.classid` low u16 — the +//! `0xDDCC` domain layout the [`parity`] guard pins. Reconciling an OGIT-hydrated +//! TTL class against an OGAR-promoted concept is a `ClassId` lookup, not a parse. + +#![forbid(unsafe_code)] +#![warn(missing_docs)] + +// ── Full re-export of the OGAR Active-Record crates under stable names ── +pub use ogar_adapter_surrealql; +pub use ogar_class_view; +pub use ogar_ontology; +pub use ogar_vocab; + +// ── The contract surface OGAR implements + the wire mirror the guard checks ── +pub use lance_graph_contract as contract; + +/// The OGAR active-record `ClassView` projection (`impl +/// lance_graph_contract::ClassView`) — the one-stop entry point a renderer holds. +pub use ogar_class_view::OgarClassView; +/// The calcified canonical AR shape (attributes + family `Association`s). +pub use ogar_vocab::Class; + +/// Codebook parity-guard — the drift fuse between OGAR's authoritative codebook +/// (`ogar_vocab::class_ids::ALL`) and the contract's zero-dep wire mirror +/// (`lance_graph_contract::ogar_codebook::CODEBOOK`). Two depths so it cannot be +/// silently bypassed (codex P2, PR #564): a [`COUNT_FUSE`] **compile-time** assert +/// that fires in ANY build, plus [`assert_codebook_parity`] for the runtime full +/// id/domain bijection (tested here = CI gate; call at consumer startup too). When +/// this crate is absent, the contract's mirror stands alone and needs no check. +pub mod parity { + use lance_graph_contract::ogar_codebook as mirror; + + /// **Compile-time length fuse.** Fails the build — `cargo build`, not just + /// `cargo test` — if the contract mirror and OGAR's authoritative + /// `class_ids::ALL` carry a different number of concepts (add/remove drift). + /// The full id/domain bijection is the runtime [`assert_codebook_parity`]. + pub const COUNT_FUSE: () = assert!( + mirror::CODEBOOK.len() == ogar_vocab::class_ids::ALL.len(), + "ogar_codebook mirror drifted from ogar_vocab::class_ids::ALL (concept count mismatch) — \ + update lance_graph_contract::ogar_codebook::CODEBOOK to match OGAR", + ); + + /// Whether OGAR's domain for `id` agrees with the contract mirror's. Both + /// enums are structurally identical (`id >> 8` discriminant); compared by a + /// total match so a new OGAR domain variant trips this (`#[non_exhaustive]`). + #[must_use] + pub fn domains_agree(id: u16) -> bool { + use lance_graph_contract::ogar_codebook::ConceptDomain as C; + use ogar_vocab::ConceptDomain as O; + matches!( + ( + ogar_vocab::canonical_concept_domain(id), + mirror::canonical_concept_domain(id) + ), + (O::Reserved, C::Reserved) + | (O::ProjectMgmt, C::ProjectMgmt) + | (O::Commerce, C::Commerce) + | (O::Osint, C::Osint) + | (O::Ocr, C::Ocr) + | (O::Health, C::Health) + | (O::Unassigned, C::Unassigned) + ) + } + + /// Assert the mirror is a faithful, complete copy of OGAR's codebook — + /// forward (mirror ⊆ OGAR), reverse (OGAR ⊆ mirror), and domain agreement. + /// Returns the number of concepts checked. Panics on any divergence. + pub fn assert_codebook_parity() -> usize { + // Forward: every mirror entry resolves identically through OGAR's API. + for &(concept, id) in mirror::CODEBOOK { + assert_eq!( + ogar_vocab::canonical_concept_id(concept), + Some(id), + "contract mirror has {concept}={id:#06x} but OGAR disagrees", + ); + assert!( + domains_agree(id), + "domain disagreement for {concept} ({id:#06x})" + ); + } + // Reverse: every OGAR canonical concept is present in the mirror with the + // same id (no OGAR concept silently missing from the wire mirror). + for &(concept, id) in ogar_vocab::class_ids::ALL { + assert_eq!( + mirror::canonical_concept_id(concept), + Some(id), + "OGAR has {concept}={id:#06x} but contract mirror is missing/wrong", + ); + } + ogar_vocab::class_ids::ALL.len() + } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn mirror_is_a_faithful_copy_of_ogar_codebook() { + let n = assert_codebook_parity(); + assert!(n >= 32, "expected ≥32 promoted concepts, got {n}"); + } + + #[test] + fn classid_low_u16_is_the_codebook_id() { + // The contract NodeGuid.classid low u16 IS the OGAR codebook id — the + // wire identity the whole separation rests on. + use lance_graph_contract::NodeGuid; + let project_id = ogar_vocab::canonical_concept_id("project").unwrap(); + let guid = NodeGuid::new(u32::from(project_id), 0, 0, 0, 0, 0); + assert_eq!(guid.classid() as u16, project_id); + // and it routes to the ProjectMgmt domain on both sides + assert!(domains_agree(project_id)); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ogar_class_view_implements_contract_class_view() { + // The activation in one line: an OgarClassView IS a contract ClassView, + // so a consumer holding `&dyn ClassView` can be handed the real OGAR AR + // surface. (Compile-time proof; constructing it walks the 32 class fns.) + use lance_graph_contract::class_view::ClassView; + let view = OgarClassView::new(); + let _as_trait: &dyn ClassView = &view; + } +}