diff --git a/.claude/board/AGENT_LOG.md b/.claude/board/AGENT_LOG.md index 7a5a81d0..b36ca559 100644 --- a/.claude/board/AGENT_LOG.md +++ b/.claude/board/AGENT_LOG.md @@ -1,3 +1,9 @@ +## [Main-thread → woa-rs HANDOFF] Odoo savant AXIS-B evidence-contract scaffold (carve-out request) + +Wrote `.claude/odoo/savants/_SCAFFOLD-EVIDENCE-CONTRACT.md` — a self-contained handover asking the **woa-rs session** (roster/evidence-schema owner) to carve out the **4 AXIS-B slots per savant** (Arrow `EvidenceRef` schema · odoo field→signal map · property-level OWL alignment · the decision in evidence terms) so lance-graph can implement the `Reasoner` impls (D-ODOO-2 / D-ODOO-SAV-4) in one pass without cross-session ping-pong. Includes the fixed dispatch tuple for all 25 (priority-tiered) + the target `Reasoner` shape + the open dispatch-shape question (N impls vs savant-config registry). Hand-back: fill per-savant docs + note here. No code; doc only. On branch `splat3d-cpu-simd-renderer-MAOO0` (PR #416). + +--- + ## [Agent-A / Sonnet] [SCAFFOLD ONLY — no implementation, no commit] D-ATOM-4 — counterfactual.rs split-resolution-via-counterfactual-mantissa scaffold **D-id:** D-ATOM-4 (`atom-mailbox-substrate-v1` pillar 5 — counterfactual mantissa v2 deposit + v3 mailbox+revision). @@ -83,6 +89,12 @@ --- +## [Main-thread] [DONE — green] D-ODOO-1 Odoo savant roster + integration plan + +Created the lance-graph side of the woa-rs Odoo savant delegation (material: `.claude/odoo/SAVANTS.md` + L1–L15, PR #413). **`contract::savants`** — the **25-savant roster as data**: `Savant { id, name, family: Option, kind, inference, semiring, style, lane, decides }` + `SAVANTS[25]` + `savant()`/`savant_by_name()`/`unaligned()` + `query_strategy()`. `other_kind` codes for the 6 `ReasoningKind::Other(u32)`. Rides the shipped `reasoning::{Reasoner,ReasoningKind}` / `nars` / `thinking::StyleCluster` (delegation surface already existed). **3 tests green** (roster=25 unique ids, id-16-absent, lookup+dispatch, 11 `unaligned()` need axioms). Plan `odoo-savant-roster-v1.md` + INTEGRATION_PLANS prepend (D-ODOO-1 done; D-ODOO-2 Reasoner impls / D-ODOO-3 OGIT families 0x63+0x90 / D-ODOO-4 alignment axioms / D-ODOO-5 conformance queued). Synced to `main` (incl. #412/#413); 452 contract tests green. + +--- + ## [Main-thread] [DONE — green] the 34 tactics as 34 working Rust kernels (Elixir-like behaviour) `crates/lance-graph-contract/src/recipe_kernels.rs` (new, wired in lib.rs). One uniform diff --git a/.claude/board/INTEGRATION_PLANS.md b/.claude/board/INTEGRATION_PLANS.md index f1550011..fb9c8fe6 100644 --- a/.claude/board/INTEGRATION_PLANS.md +++ b/.claude/board/INTEGRATION_PLANS.md @@ -17,6 +17,13 @@ D-ODOO-SAV-1/2/3 additive + low-risk → first PR (this session). D-ODOO-SAV-4 ### Invariants Option B (inherit existing slots; new families are genuine basins not per-class mints; `None` stays `None` w/o honest pivot) · public OWL pristine (axioms are NEW TTL) · savant = Layer-2 catalogue · reasoner output = suggestion (guard stays in woa-rs) · impls in callcenter behind contract `Reasoner` trait. +--- +## 2026-05-27 — odoo-savant-roster-v1 (the lance-graph side of the woa-rs Odoo savant delegation: 25 delegated reasoners) + +**Status:** PROPOSAL. **Plan file:** `.claude/plans/odoo-savant-roster-v1.md`. **Source:** `.claude/odoo/SAVANTS.md` + L1–L15 (PR #413). **Predecessor:** PR #412 (odoo→FIBO/SKR alignment + DOLCE classifier). +**Scope:** 25 Odoo savants = delegated reasoners (woa-rs keeps the AXIS-A deterministic guard; the ambiguous AXIS-B core delegates to lance-graph via `reasoning::Reasoner`). Each savant = a dispatch tuple (OGIT family · `ReasoningKind` · `InferenceType` · `SemiringChoice` · `StyleCluster`). +**Deliverables:** D-ODOO-1 roster-as-data (`contract::savants`, ✅ DONE) · D-ODOO-2 `Reasoner` impls per `ReasoningKind` · D-ODOO-3 new OGIT families `0x63 ProductCatalog` + `0x90 HRFoundation` + style wiring · D-ODOO-4 Layer-2 alignment axioms for the `None` classes (stock.*, analytic.distribution.model, account.account.tag) · D-ODOO-5 delegation call-site conformance (ReasoningContext + Arrow EvidenceRef schemas). +**Invariants:** suggestion-only never un-guarded write (Iron Rule 7) · deterministic guard stays woa-rs · BBB-allowed crates only · tuple fully determines dispatch · business = OGIT-inherited sidecar (odoo inherits FIBO/SKR slots; 0x63/0x90 are the only new families, need ratification). --- diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index 4c5009b7..3a12e841 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -14,6 +14,7 @@ | PR | Merged | Title | What it added | |---|---|---|---| +| **#411** | 2026-05-27 | Cognitive substrate: locked 33-TSV atom layer + 34-tactic recipes + escalation loop | **D-PERSONA-1** `contract::escalation` + `planner::mul::escalation` (CollapseHint/InnerCouncil/EpiphanyDetector/GhostEcho/WisdomMarker/Checklist, 13 tests). **`contract::atoms`** — LOCKED 33-dim TSV `CANONICAL_ATOMS` (3 Pearl + 9 Rung + 5 Σ + 8 Ops + 4 Presence + 4 Meta) + `I4x32` carrier. **`contract::recipes`** — 34-tactic metadata catalogue. **`contract::recipe_kernels`** — the 34 tactics as 34 `Tactic` impls + registry over a shared `ThoughtCtx`. Charter D0: **ladybug-rs has no relation, rewrite-not-port**; lattice is **SPOQ** (SPO 2³ causal + Q qualia overlay); business = OGIT sidecar; markers gate the datapath/control/gate partition. Green: escalation 13 / atoms 3 / recipes 4 / recipe_kernels 5 + 446 prior, no warnings. Branch `claude/splat3d-cpu-simd-renderer-MAOO0` (39 commits). See PR_ARC #411. | | **#389** | 2026-05-16 | fix(sprint-12/wave-F): codex P2 — AttentionMaskBackend impl for AttentionMaskSoA + canonical MailboxId import | Codex P2 follow-on to PR #388. Adds `AttentionMaskBackend` trait impl for `AttentionMaskSoA` (Wave-F surface coherence) and converges duplicate `MailboxId` imports onto the canonical contract definition. Merge commit `b526485`. | | **#388** | 2026-05-16 | impl(sprint-12/wave-F partial): D-CSV-10 sigma-tier-router + AttentionMask + splat ops + governance (6 of 12 workers landed) | Sprint-12 Wave F fleet partial landing. **D-CSV-10** `SigmaTierRouter` crate (Rubicon-resonance ΔF + threshold → Σ10 commit, hand-tuned threshold per OQ-CSV-6, tracked as TD-SIGMA-TIER-THRESHOLDS-1); **D-CSV-12** scalar splat op fleet on i4 (`splat_gaussian`, `score_hole_closure`, `replay_coherence`, `emit_if_epiphany`); **AttentionMask** SoA + actor + backend surface; W-F8 TYPE_DUPLICATION_MAP refresh (records two-`TrustTexture` coexistence as TD-TRUST-TEXTURE-DUPE-1); W-F10 sprint-11 Opus meta-review; W-F11 i4-substrate-decisions knowledge doc; W-F12 cognitive-substrate-convergence-v2 plan draft (608 lines). Merge commit `77f2d26`. | | **#387** | 2026-05-16 | impl(sprint-11/wave-E): D-CSV-8 MUL i4 SIMD evaluation + D-CSV-9 8ch↔SPO transcoder | **D-CSV-8** integer MUL evaluation on `QualiaI4_16D` + signed mantissa (scalar i4 path; AVX-512/NEON deferred → D-CSV-13 sprint-12). **D-CSV-9** 8-channel ↔ SPO-palette transcoder (Option R-3) at thinking-engine L3 commit boundary; 16-mapping bidirectional round-trip; renames `set_channel` → `set_channel_u8` to widen equivalence class. Merge commit `e042c70`. | diff --git a/.claude/board/PR_ARC_INVENTORY.md b/.claude/board/PR_ARC_INVENTORY.md index 80859888..f1e938b6 100644 --- a/.claude/board/PR_ARC_INVENTORY.md +++ b/.claude/board/PR_ARC_INVENTORY.md @@ -35,6 +35,35 @@ --- +## PR #411 — Cognitive substrate: locked 33-TSV atom layer + 34-tactic recipes + escalation loop (MERGED 2026-05-27 → main) + +**Status:** MERGED. Branch `claude/splat3d-cpu-simd-renderer-MAOO0` → `main`, 39 commits. + +**Added:** +- `contract::escalation` (D-PERSONA-1) + `planner::mul::escalation` — `CollapseHint` / `InnerCouncil` (3-archetype split) / `EpiphanyDetector` / `GhostEcho` (8) / `WisdomMarker` (0.1 floor) / `Checklist` (HARD/SOFT). 13 tests. +- `contract::atoms` — the **LOCKED 33-dim ThinkingStyleVector** `CANONICAL_ATOMS` (3 Pearl + 9 Rung + 5 Σ + 8 Operations + 4 Presence + 4 Meta) + `I4x32` bare-metal carrier. 3 tests. +- `contract::recipes` — the 34 reasoning tactics as a metadata catalogue (`Recipe{Tier,Mechanism,Bucket,spo2cubed,substrate}`, `RECIPES[34]`, lookups). 4 tests. +- `contract::recipe_kernels` — **the 34 tactics as 34 `Tactic` implementations** + registry (`kernel`/`all_kernels`) over a shared `ThoughtCtx`. 5 tests. +- Scaffolds (un-wired, `todo!()`): `recipe.rs`/`quorum.rs`/`counterfactual.rs` (contract), `graph/witness_tombstone.rs` (core). + +**Locked:** +- **D0 — ladybug-rs has NO relation and never will** (failed "empty cathedral"); rewrite-not-port; cross-repo docs (ladybug/ada-consciousness/neo4j-rs) are spec-references only, never deps/ports. +- Execution stack **atoms → cognitive-shader-driver → SIMD** (atoms are NOT SIMD). +- 3-layer: **atom = one pole** (bare-metal) → **style = one i4 vector** (molecule) → **persona = composition**; the OO style/persona objects are the metacognition. +- The lattice is **SPOQ**: SPO 2³ = the causal slice (Counterfactual=`SPO`/0b111, Intervention=`_PO`); **Q (Qualia) = the 4th, affective overlay**. +- **Business = OGIT-inherited sidecar**, not an atom. +- Markers gate implicitly (entropy=CollapseGate SD FLOW/HOLD/BLOCK, F-floor, rung, temperature, dissonance) — the CPU clock-gating partition: **datapath / control / gate**. +- The 34 tactics reduce to **3 mechanisms** (parallel-independence / truth-aware / structural-divergence) = the partition. +- One uniform `Tactic` behaviour; richer fingerprint substrate slots behind the same trait without changing the 34 call sites. + +**Deferred:** per-recipe real-substrate evaluators (kernels are deterministic over `ThoughtCtx` today); atom `pack/unpack` via cognitive-shader-driver; the un-wired scaffolds (D-ATOM-2..5 wiring); the SPO-2³-vs-SPOQ-2⁴ lattice decision; the substrate-Markov re-scope (awaits [FORMAL-SCAFFOLD] check); the `rung-persona`→mailbox rename. + +**Docs:** `ada-rewrite-charter`, `atom-basis-inventory`, `spo-2cubed-list-coverage`, `34-tactics-vs-ada`, `agi-stack-cross-repo` (knowledge); `atom-mailbox-substrate-v1` (plan + INTEGRATION_PLANS); `E-LADDER-SERVES-MAILBOX` + append-only correction (EPIPHANIES); `TD-GHOST-ECHO-DUP-1` (TECH_DEBT). + +**Confidence (2026-05-27):** working — `lance-graph-contract` green (escalation 13 / atoms 3 / recipes 4 / recipe_kernels 5 + 446 prior), zero warnings. The kernels are the uniform deterministic layer; real-substrate upgrade is the named follow-on. + +--- + ## sprint-13/W-I1 — impl(sprint-13): D-CSV-13b i4 batch SIMD dispatch + tests (in PR) **Status:** In PR (branch `claude/sprint-13-w-i1-salvage`, HEAD `c9c1c79`, awaiting user merge). 4 commits on the branch: `cdc84ec` salvage W-I1 i4_eval::batch impl + criterion scaffold (recovered from cleaned worktree) → `a356e64` SIMD-vs-scalar parity tests + repr(u8) enum invariant (5 new randomised tests over 10 sizes, criterion 0.5 dev-dep, dead-code warning fix) → `d8d1437` AVX-512 dim-extract sign-extend fix (the bug that made the salvage path silently produce wrong bytes on negative thresholds) → `c9c1c79` `scalar_impl` made `#[doc(hidden)] pub` for bench access. diff --git a/.claude/odoo/savants/_SCAFFOLD-EVIDENCE-CONTRACT.md b/.claude/odoo/savants/_SCAFFOLD-EVIDENCE-CONTRACT.md new file mode 100644 index 00000000..ea17436a --- /dev/null +++ b/.claude/odoo/savants/_SCAFFOLD-EVIDENCE-CONTRACT.md @@ -0,0 +1,89 @@ +# SCAFFOLD — Odoo savant AXIS-B evidence contract (carve-out request) + +> **Handover:** lance-graph session (`splat3d-cpu-simd-renderer-MAOO0`) → **woa-rs session** (the Odoo extractor, owner of the roster + evidence schemas per `SAVANTS.md` §"lance-graph handover boundary"). +> **Why this exists:** the `Reasoner` impls (D-ODOO-2 / D-ODOO-SAV-4) are blocked on the **AXIS-B input contract**, which the current `SAVANTS.md` + `L*.md` specify only on the AXIS-A (woa-rs guard) side. Rather than ping-pong (which loses context across sessions), **carve out the four slots below per savant** and I implement the Reasoners in one pass. +> **Date:** 2026-05-27. + +## What lance-graph already has (don't re-send) + +- **`contract::savants`** — the 25-savant roster as data: each `Savant { id, name, family, kind, inference, semiring, style, lane, decides }`. The dispatch tuple is **fixed**; `query_strategy()` rides `InferenceType::default_strategy()`. +- **`reasoning::{Reasoner, ReasoningContext, ReasoningKind, EvidenceRef, Budget}`** — the delegation surface (shipped). +- **#414**: OGIT families `0x64 ProductCatalog` / `0x90 HRFoundation` + Layer-2 alignment axioms (stock.* / analytic.distribution.model / account.account.tag) + StyleCluster wiring. +- FIBU subtree now inherits `fibofnd` (zugferd/fibobe/skr0x). + +## What I need carved out — 4 slots per savant + +For each savant create `.claude/odoo/savants/.md` (schema = `SAVANTS.md` §"Per-agent document schema") and fill **these four AXIS-B slots** (the rest of the tuple is in `contract::savants`): + +1. **Evidence (Arrow `EvidenceRef`)** — the concrete table(s) + **key columns** + dtype, and what signal each column carries. This is the typed input the Reasoner consumes. *(Without this the impl has no input.)* +2. **Odoo field → signal map** — which exact odoo model fields back each column (e.g. `res.partner.{payment_history, credit_limit}` for `PartnerTrustAdvisor`), with the L-doc `file:lines`. +3. **Property-level alignment** — the OWL property IRIs (not just class) the reasoner traverses (e.g. `odoo:amount_residual ≡ fibo:hasResidualAmount`), if the decision crosses the FIBO/SKR/ZUGFeRD seam. +4. **AXIS-B decision in evidence terms** — restate `decides` as: given evidence E, produce conclusion C with a NARS `(frequency, confidence)`; name the discriminating features. *(The AXIS-A guard is already in the L-docs; I only need the ambiguous core.)* + +## The target the carve-out feeds (so you know the shape) + +```rust +// lance-graph side will implement, one per ReasoningKind (or savant-config registry — TBD review): +impl Reasoner for Reasoner { + fn reason(&self, ctx: &ReasoningContext) -> SavantConclusion { + // ctx.evidence: &[EvidenceRef] ← slot 1 (your schema) + // dispatch by ctx.kind + family style; fuse via savant.semiring (NarsTruth common) + // → conclusion + (frequency, confidence) ← slot 4 shape + } +} +``` + +## The 25 savants to carve out (priority-ordered; fill in lane order) + +**Tier 1 — substrate most ready, do first (accounting / 0x62 / 0x81):** + +| id | savant | family | kind | infer | lane | decides (AXIS-B core) | +|---|---|---|---|---|---|---| +| 6 | SequenceGapAnomalyDetector | 0x62 | PostingAnomaly | Abduction | L11 | journal sequence gaps ⇒ deleted posted entries (GoBD) | +| 17 | AutopostRecommender | 0x81 | PostingAnomaly | Induction | L1 | auto-post bills after 3+ unmodified from a partner | +| 18 | LockDateAdvancer | 0x81 | PostingAnomaly | Abduction | L1 | next open period to advance a locked move into | +| 4 | AnalyticDistributionSuggester | 0x62 | NextBestAction | Induction | L10 | suggested cost-centre distribution for a move line | +| 15 | TaxExigibilitySuggestor | 0x62 | NextBestAction | Induction | L15 | tax exigibility (on-invoice vs on-payment / cash-basis) | + +**Tier 2 — partner / pricing (0x80 / 0x81 / 0x64):** + +| id | savant | family | kind | infer | lane | decides | +|---|---|---|---|---|---|---| +| 1 | FiscalPositionResolver | 0x80 | CustomerCategory | Deduction | L9 | which fiscal position (tax mapping) for a partner | +| 2 | PartnerTrustAdvisor | 0x80 | CustomerCategory | Revision | L9 | partner trust / dunning-risk from payment history | +| 3 | PricelistAssignmentAgent | 0x64 | Other(PRICELIST_ASSIGNMENT) | Revision | L8 | partner pricelist when no explicit property | +| 23 | PricelistRecommender | 0x81 | NextBestAction | Synthesis | L6 | which pricelist rule when multiple candidates apply | +| 22 | UpsellActivityTrigger | 0x81 | NextBestAction | Induction | L6 | qty_delivered>ordered ⇒ upsell TODO | +| 10 | UserCompanyAccessAdvisor | 0x80 | CustomerCategory | Induction | L12 | branch-access subset by user role/context | + +**Tier 3 — reconcile / FX / currency (None / 0x62):** + +| id | savant | family | kind | infer | lane | decides | +|---|---|---|---|---|---|---| +| 19 | ReconcileMatchSelector | None | Other(RECONCILE_MATCH) | Induction | L2 | open items to propose as reconciliation candidates | +| 20 | BankStatementMatcher | None | Other(BANK_STATEMENT_MATCH) | Induction | L5 | reconcile-model rule for a bank line + write-offs | +| 21 | PaymentToInvoiceMatcher | None | Other(RECONCILE_MATCH) | Induction | L5 | whether a payment fully reconciles open invoices | +| 5 | AnalyticModelScorer | None | CustomerCategory | Deduction | L10 | which analytic.distribution.model matches (priority) | +| 7 | ExchangeAccountSelector | 0x62 | Other(CHART_ACCOUNT_MAPPING) | Deduction | L12 | gain/loss account for FX diff | +| 8 | ReportRateTypeSelector | 0x62 | Other(CONSOLIDATION_RATE_POLICY) | Deduction | L12 | current/historical/average rate per report line | +| 9 | CurrencySelectionAdvisor | 0x62 | NextBestAction | Induction | L12 | which currencies to enable (geography signal) | + +**Tier 4 — stock / procurement (None, needs #414 axioms confirmed):** + +| id | savant | family | kind | infer | lane | decides | +|---|---|---|---|---|---|---| +| 11 | ProcurementRuleSelector | None | NextBestAction | Induction | L13 | route among equal-sequence rules | +| 12 | ReorderTimingAdvisor | None | NextBestAction | Induction | L13 | reorder timing under demand/supplier uncertainty | +| 13 | ReplenishmentReportAdvisor | None | NextBestAction | Induction | L13 | real shortfall vs demand noise | +| 14 | RouteTiebreaker | None | NextBestAction | Abduction | L13 | equal-sequence route tiebreak | +| 24 | RemovalStrategySelector | None | NextBestAction | Induction | L7 | quants to bind to a reservation (FIFO/FEFO/LIFO) | +| 25 | MoveAssignmentPrioritizer | None | NextBestAction | Induction | L7 | which confirmed moves to satisfy first | +| 26 | BackorderJudge | None | NextBestAction | Abduction | L7 | partial fulfilment ⇒ backorder vs cancel | + +## Open question for woa-rs to pin (drives the impl shape) + +**Reasoner dispatch shape:** one `Reasoner` impl per `ReasoningKind` (5–6 impls, dispatch on family+evidence inside), **or** a savant-config registry (data-driven, one generic engine reading the savant tuple)? #414 flagged this as gating D-ODOO-SAV-4. Your call — it determines whether the carve-out feeds N impls or one config table. + +## Hand-back + +Fill the per-savant docs (or a single table with the 4 slots × 25) and drop a note in `lance-graph/.claude/board/AGENT_LOG.md`. I'll then implement the Reasoners against the filled evidence contract in one pass — no re-derivation. diff --git a/.claude/plans/odoo-savant-roster-v1.md b/.claude/plans/odoo-savant-roster-v1.md new file mode 100644 index 00000000..04678b30 --- /dev/null +++ b/.claude/plans/odoo-savant-roster-v1.md @@ -0,0 +1,41 @@ +# odoo-savant-roster-v1 + +> **Status:** PROPOSAL (the lance-graph side of the woa-rs Odoo savant delegation). +> **Confidence:** HIGH on the roster + dispatch surface (it rides the shipped `reasoning`/`nars`/`thinking` contract enums); MED on the `Reasoner` impls + new OGIT families; the Layer-2 alignment axioms are net-new. +> **Plan file:** `.claude/plans/odoo-savant-roster-v1.md` +> **Source of truth:** `.claude/odoo/SAVANTS.md` + the L1–L15 lane drafts (`.claude/odoo/L*.md`), imported by PR #413. +> **Predecessors:** PR #412 (odoo→FIBO/SKR ontology alignment + DOLCE classifier), `lance-graph-ontology`, `ada-rewrite-charter` (D1: business = OGIT-inherited sidecar). + +## Scope + +woa-rs harvested 25 Odoo **savants** — delegated reasoners where woa-rs keeps the deterministic guard (AXIS-A Rust) and delegates the ambiguous, evidence-weighted core (AXIS-B) to lance-graph's thinking surface through the BBB-allowed contract crates. Each savant is a dispatch **tuple**: OGIT family · `reasoning::ReasoningKind` · `nars::InferenceType` · `nars::SemiringChoice` · `thinking::StyleCluster`. The tuple fully determines runtime dispatch (`InferenceType::default_strategy() → QueryStrategy`). + +This plan is the **lance-graph implementation** of the handover (SAVANTS.md §"lance-graph handover boundary"): (a) `Reasoner` impls per `ReasoningKind`, (b) two new OGIT families + style wiring, (c) Layer-2 alignment axioms for the `None`-family classes. + +## Deliverables + +| D-id | Scope | Crate | Status | +|---|---|---|---| +| **D-ODOO-1** | the 25-savant roster as data + dispatch tuple + lookups | `contract::savants` | ✅ **DONE this PR** (3 tests) | +| **D-ODOO-2** | `Reasoner` impls per `ReasoningKind` (CustomerCategory / PostingAnomaly / NextBestAction / InvoiceCompleteness / MailIntent + the 6 `Other` codes) — the AXIS-B experts | planner / a new reasoner crate | Queued | +| **D-ODOO-3** | two new OGIT families **`0x64 ProductCatalog`** + **`0x90 HRFoundation`** in `OgitFamilyTable` + inherited `StyleCluster` per family | `lance-graph-ontology` + `contract::build.rs` | Queued | +| **D-ODOO-4** | Layer-2 alignment axioms for the `None` classes (`stock.*`, `account.analytic.distribution.model`, `account.account.tag`) so the 11 `unaligned()` savants resolve a family | `lance-graph-ontology` `data/ontologies/odoo/alignment/` | Queued | +| **D-ODOO-5** | delegation call-site conformance: `ReasoningContext` + Arrow `EvidenceRef` schemas per savant; woa-rs↔lance-graph contract test | `contract` + conformance harness | Queued | + +## Invariants (the delegation contract) + +- **Suggestion-only, never an un-guarded write** (woa-rs Iron Rule 7, *verhaltens-bewahrend*) — the reasoner returns a truth-weighted conclusion; woa-rs applies it behind its AXIS-A guard. +- **Deterministic guard stays in woa-rs** (balance==0, residual, sign, prefix-match…); lance-graph only sees the ambiguous core. +- **BBB-allowed crates only** (`lance-graph-contract`, `-ontology`, `-callcenter`); no brain-crate in the customer binary (Iron Rule 1). +- The savant **tuple fully determines dispatch**; `SemiringChoice` selects evidence fusion (NarsTruth = NARS revision, the common case). +- **Business = OGIT-inherited sidecar** (charter D1): odoo classes inherit existing FIBO/SKR family slots (PR #412), they do not get a bespoke CAM family — `0x64`/`0x90` are the only *new* families and must be ratified. + +## Open questions + +- Ratify families `0x64 ProductCatalog` / `0x90 HRFoundation` (or fold into existing slots?). +- Where do the `Reasoner` impls live — `lance-graph-planner` (has NARS engine + MUL) or a dedicated reasoner crate? (AriGraph-circular-dep caveat applies; see CLAUDE.md p64 convergence note.) +- `ReasoningKind::Other(u32)` code registry — `contract::savants::other_kind` holds the 6 codes; promote to a named enum if the set stabilizes. + +## Cross-ref + +`.claude/odoo/SAVANTS.md` (roster + delegation contract), `.claude/odoo/L*.md` (per-lane porter specs), `contract::{savants, reasoning, nars, thinking}`, `lance-graph-ontology` (odoo alignment from PR #412), `ada-rewrite-charter.md` (business sidecar). diff --git a/crates/lance-graph-contract/examples/cognitive_cycle.rs b/crates/lance-graph-contract/examples/cognitive_cycle.rs new file mode 100644 index 00000000..f866ae76 --- /dev/null +++ b/crates/lance-graph-contract/examples/cognitive_cycle.rs @@ -0,0 +1,80 @@ +//! Orchestration deep-dive #1 — the active-inference cognitive cycle. +//! +//! Run it: `cargo run -p lance-graph-contract --example cognitive_cycle` +//! +//! THE IDEA (Elixir-style): +//! * every reasoning tactic implements ONE behaviour — `Tactic` (meta/gate/apply/run), +//! exactly like an Elixir `@behaviour` / GenServer callback. +//! * a "recipe" is a *pipeline* of tactics (the Elixir `|>` operator), applied in order. +//! * each tactic is **clock-gated** by markers: a Gate-bucket tactic only fires when there +//! is surprise to act on (CollapseGate SD not in FLOW). Most stay dark — like a CPU. +//! * the loop runs until free energy descends below the homeostasis floor: "the shader +//! can't resist the thinking… F descends, bits clear, shader rests." +//! +//! Nothing here is a toy: every call is a real `lance_graph_contract` kernel. + +use lance_graph_contract::recipe_kernels::{kernel, GateState, ThoughtCtx, SD_FLOW}; +use lance_graph_contract::recipes::recipe_by_code; + +/// A "deep-think" recipe = an ordered pipeline of tactics (by their catalogue code). +/// Read it top-to-bottom like an Elixir pipe: +/// ctx |> Expand |> Decompose |> Prune |> Contradiction |> Converge |> Meta |> Filter |> Reduce +const DEEP_THINK: [&str; 8] = ["RTE", "HTD", "TCP", "CR", "CDT", "MCP", "TCF", "CUR"]; + +fn gate_state(sd: f32) -> GateState { + if sd < SD_FLOW { GateState::Flow } else if sd <= 0.35 { GateState::Hold } else { GateState::Block } +} + +fn main() { + // A *surprised* starting state: high dispersion (SD=BLOCK), high free energy, + // four candidate interpretations, and a same-topic contradiction in the belief set. + let mut ctx = ThoughtCtx::new(vec![0.92, 0.61, 0.34, 0.12]); + ctx.sd = 0.42; // > 0.35 → BLOCK: the gate is wide open, deep tactics will fire + ctx.free_energy = 0.80; + ctx.confidence = 0.50; + ctx.beliefs = vec![(7, 0.90, 0.8), (7, 0.10, 0.7)]; // topic 7 asserted true AND false + + println!("== cognitive cycle: one Think, run to rest ==\n"); + println!("start: gate={:?} F={:.2} conf={:.2} candidates={} beliefs={}\n", + gate_state(ctx.sd), ctx.free_energy, ctx.confidence, ctx.candidates.len(), ctx.beliefs.len()); + + // The active-inference loop: keep thinking while there is surprise (gate != FLOW). + let mut round = 0; + while gate_state(ctx.sd) != GateState::Flow && round < 5 { + round += 1; + println!("── round {round} (gate {:?}) ───────────────────────", gate_state(ctx.sd)); + + // Pipe the context through the recipe. Each step is `ctx |> tactic`. + for code in DEEP_THINK { + let rec = recipe_by_code(code).expect("recipe in catalogue"); + let k = kernel(rec.id).expect("kernel for id"); + let out = k.run(&mut ctx); // gate + apply; confidence auto-clamped + println!( + " {:<5} [{:<12}] {:<22} {} (Δconf {:+.2})", + code, + format!("{:?}", rec.bucket), + rec.name, + if out.fired { out.note } else { "· gated off (FLOW)" }, + out.delta_conf, + ); + } + + // The cycle resolved some surprise: dispersion + free energy anneal toward rest. + // (In the wired system this falls out of the codec sweep; here we make it explicit.) + ctx.sd *= 0.55; + ctx.free_energy *= 0.5; + println!(" → after round: gate={:?} SD={:.3} F={:.3} conf={:.2}\n", + gate_state(ctx.sd), ctx.sd, ctx.free_energy, ctx.confidence); + } + + if gate_state(ctx.sd) == GateState::Flow { + println!("== rest == the shader stopped because gate reached Flow (SD={:.3} < FLOW {SD_FLOW}).", ctx.sd); + } else { + println!("== rest == round cap reached ({round} rounds) before FLOW; gate={:?}, SD={:.3}.", + gate_state(ctx.sd), ctx.sd); + } + println!("final: conf={:.2}, {} candidate(s) survived pruning, {} beliefs.", + ctx.confidence, ctx.candidates.len(), ctx.beliefs.len()); + println!("\nKey: Gate-bucket tactics (TCP/CDT/TCF/CUR) skip while in FLOW — the markers,"); + println!("not a scheduler, decide what fires. Same `Tactic` behaviour, 34 hot-swappable units."); +} diff --git a/crates/lance-graph-contract/examples/savant_dispatch.rs b/crates/lance-graph-contract/examples/savant_dispatch.rs new file mode 100644 index 00000000..0bdbfbfe --- /dev/null +++ b/crates/lance-graph-contract/examples/savant_dispatch.rs @@ -0,0 +1,74 @@ +//! Orchestration deep-dive #2 — Odoo savant delegation (woa-rs AXIS-A guard → lance-graph AXIS-B reason). +//! +//! Run it: `cargo run -p lance-graph-contract --example savant_dispatch` +//! +//! THE IDEA (Elixir-style): +//! * a Savant is a delegated reasoner declared as a *tuple* (family · ReasoningKind · +//! InferenceType · SemiringChoice · StyleCluster) — the tuple FULLY determines dispatch. +//! * dispatch is **pattern-matching** on `ReasoningKind` (like an Elixir `case`/multi-clause +//! function head), and the `InferenceType` resolves O(1) to a `QueryStrategy`. +//! * the deterministic guard (AXIS-A) stays in woa-rs; only the *ambiguous core* is delegated +//! here (AXIS-B), and the answer is a NARS-truth-weighted **suggestion**, never an +//! un-guarded write (Iron Rule 7). + +use lance_graph_contract::reasoning::ReasoningKind; +use lance_graph_contract::savants::{savant_by_name, Savant}; + +/// A concrete situation arriving from the ERP. +struct Situation { + headline: &'static str, + /// The AXIS-A guard already ran in woa-rs; `true` means it could NOT decide deterministically + /// and is delegating the ambiguous core to the savant. + ambiguous: bool, + savant: &'static str, +} + +/// Pattern-match the kind → the reasoning approach (the Elixir multi-clause `case`). +fn approach(kind: ReasoningKind) -> &'static str { + match kind { + ReasoningKind::CustomerCategory => "classify against the family codebook (deductive lookup)", + ReasoningKind::PostingAnomaly => "abduce the most likely cause from the evidence trail", + ReasoningKind::NextBestAction => "induce the action with the highest expected value", + ReasoningKind::InvoiceCompleteness => "check required-field coverage, score the gaps", + ReasoningKind::MailIntent => "resonate the message against intent prototypes", + ReasoningKind::Other(code) => match code { + 5 | 6 => "match open items / bank lines by evidence fusion (reconcile)", + _ => "domain-specific Other(code) reasoner", + }, + } +} + +fn dispatch(s: &Savant) { + println!(" savant {} (#{}, lane {})", s.name, s.id, s.lane); + println!(" family {}", s.family.map(|f| format!("0x{f:02X}")).unwrap_or_else(|| "None (needs alignment axiom)".into())); + println!(" tuple kind={:?} · infer={:?} · semiring={:?} · style={:?}", s.kind, s.inference, s.semiring, s.style); + // The InferenceType resolves O(1) to the runtime query strategy. + println!(" → strategy {:?} (InferenceType::default_strategy)", s.query_strategy()); + println!(" → approach {}", approach(s.kind)); + println!(" → output NARS (frequency, confidence) suggestion — woa-rs applies it behind its AXIS-A guard\n"); +} + +fn main() { + println!("== Odoo savant delegation: AXIS-A guard (woa-rs) → AXIS-B reason (lance-graph) ==\n"); + + let inbox = [ + Situation { headline: "€1,200 payment arrived — does it fully reconcile the partner's open invoices?", ambiguous: true, savant: "PaymentToInvoiceMatcher" }, + Situation { headline: "3rd identical bill from this vendor, unmodified — auto-post it?", ambiguous: true, savant: "AutopostRecommender" }, + Situation { headline: "new B2B partner in AT — which fiscal position (tax mapping)?", ambiguous: true, savant: "FiscalPositionResolver" }, + Situation { headline: "journal sequence jumps 1042 → 1044 — is 1043 a deleted posted entry?", ambiguous: true, savant: "SequenceGapAnomalyDetector" }, + Situation { headline: "invoice with a perfectly matching single open item", ambiguous: false, savant: "ReconcileMatchSelector" }, + ]; + + for s in &inbox { + println!("• {}", s.headline); + if !s.ambiguous { + println!(" AXIS-A (woa-rs): deterministic match — applied directly, NO delegation.\n"); + continue; + } + let sv = savant_by_name(s.savant).expect("savant in roster"); + dispatch(sv); + } + + println!("Same delegation tuple for all 25 savants; dispatch is data, not branches."); + println!("Pattern-match on ReasoningKind picks the approach; the family's StyleCluster colours it."); +} diff --git a/crates/lance-graph-contract/src/lib.rs b/crates/lance-graph-contract/src/lib.rs index 21329b3e..6c88b7fe 100644 --- a/crates/lance-graph-contract/src/lib.rs +++ b/crates/lance-graph-contract/src/lib.rs @@ -75,6 +75,7 @@ pub mod reasoning; pub mod recipe_kernels; pub mod recipes; pub mod repository; +pub mod savants; pub mod scenario; pub mod sensorium; pub mod sigma_propagation; diff --git a/crates/lance-graph-contract/src/savants.rs b/crates/lance-graph-contract/src/savants.rs new file mode 100644 index 00000000..647fbebd --- /dev/null +++ b/crates/lance-graph-contract/src/savants.rs @@ -0,0 +1,146 @@ +//! The Odoo savant roster — 25 delegated reasoners (lanes L1–L15). +//! +//! Source of truth: `.claude/odoo/SAVANTS.md` (+ the L1–L15 lane drafts), the +//! woa-rs → lance-graph delegation harvest. Each **Savant** is a delegated +//! reasoner: woa-rs keeps the deterministic guard (AXIS-A); the ambiguous, +//! evidence-weighted core (AXIS-B) is delegated here via +//! [`crate::reasoning::Reasoner`] + [`crate::reasoning::ReasoningContext`]. +//! +//! This module is the **roster spine**: the 25 savants as data + their dispatch +//! tuple (OGIT family · [`ReasoningKind`] · [`InferenceType`] · [`SemiringChoice`] +//! · [`StyleCluster`]). The tuple fully determines runtime dispatch +//! (`InferenceType::default_strategy()` → `QueryStrategy`). The actual +//! `Reasoner` impls per `ReasoningKind`, the two new OGIT families +//! (`0x63 ProductCatalog`, `0x90 HRFoundation`), and the Layer-2 alignment +//! axioms for the `None` classes are the follow-on deliverables tracked in +//! `.claude/plans/odoo-savant-roster-v1.md`. + +use crate::nars::{InferenceType, SemiringChoice}; +use crate::reasoning::ReasoningKind; +use crate::thinking::StyleCluster; + +/// Stable codes for the `ReasoningKind::Other(u32)` savants (SAVANTS.md). +pub mod other_kind { + pub const PRICELIST_ASSIGNMENT: u32 = 1; + pub const CHART_ACCOUNT_MAPPING: u32 = 3; + pub const CONSOLIDATION_RATE_POLICY: u32 = 4; + pub const RECONCILE_MATCH: u32 = 5; + pub const BANK_STATEMENT_MATCH: u32 = 6; +} + +/// One delegated Odoo reasoner. +#[derive(Debug, Clone, Copy)] +pub struct Savant { + /// Roster id (SAVANTS.md numbering; 16 is intentionally absent). + pub id: u8, + /// Savant name. + pub name: &'static str, + /// OGIT family (8-bit), or `None` until a Layer-2 alignment axiom lands. + pub family: Option, + /// The use-case the reasoner serves. + pub kind: ReasoningKind, + /// Inference type — selects the query strategy via `default_strategy()`. + pub inference: InferenceType, + /// How evidence fuses across paths. + pub semiring: SemiringChoice, + /// Thinking style cluster (inherited from the family). + pub style: StyleCluster, + /// Source lane (`.claude/odoo/L*.md`). + pub lane: &'static str, + /// The AXIS-B decision it makes (one line). + pub decides: &'static str, +} + +use InferenceType::*; +use ReasoningKind::*; +use SemiringChoice::*; +use StyleCluster::*; + +/// The 25 Odoo savants (SAVANTS.md roster). `family = None` = needs an alignment axiom. +pub const SAVANTS: [Savant; 25] = [ + // ── L8–L15 gap lanes (15) ── + Savant { id: 1, name: "FiscalPositionResolver", family: Some(0x80), kind: CustomerCategory, inference: Deduction, semiring: NarsTruth, style: Analytical, lane: "L9", decides: "which fiscal position (tax mapping) applies to a partner" }, + Savant { id: 2, name: "PartnerTrustAdvisor", family: Some(0x80), kind: CustomerCategory, inference: Revision, semiring: NarsTruth, style: Empathic, lane: "L9", decides: "partner trust / dunning-risk from payment history" }, + Savant { id: 3, name: "PricelistAssignmentAgent", family: Some(0x64), kind: Other(other_kind::PRICELIST_ASSIGNMENT), inference: Revision, semiring: NarsTruth, style: Analytical, lane: "L8", decides: "partner pricelist when no explicit property (country-group/config fallback)" }, + Savant { id: 4, name: "AnalyticDistributionSuggester", family: Some(0x62), kind: NextBestAction, inference: Induction, semiring: NarsTruth, style: Analytical, lane: "L10", decides: "suggested cost-centre distribution for a move line" }, + Savant { id: 5, name: "AnalyticModelScorer", family: None, kind: CustomerCategory, inference: Deduction, semiring: HammingMin, style: Analytical, lane: "L10", decides: "which analytic.distribution.model matches (priority-scored)" }, + Savant { id: 6, name: "SequenceGapAnomalyDetector", family: Some(0x62), kind: PostingAnomaly, inference: Abduction, semiring: NarsTruth, style: Analytical, lane: "L11", decides: "journal sequence gaps ⇒ deleted posted entries (GoBD)" }, + Savant { id: 7, name: "ExchangeAccountSelector", family: Some(0x62), kind: Other(other_kind::CHART_ACCOUNT_MAPPING), inference: Deduction, semiring: Boolean, style: Analytical, lane: "L12", decides: "gain/loss account for FX diff (sign-driven; config-assist)" }, + Savant { id: 8, name: "ReportRateTypeSelector", family: Some(0x62), kind: Other(other_kind::CONSOLIDATION_RATE_POLICY), inference: Deduction, semiring: Boolean, style: Analytical, lane: "L12", decides: "current/historical/average rate per report line (IFRS vs HGB)" }, + Savant { id: 9, name: "CurrencySelectionAdvisor", family: Some(0x62), kind: NextBestAction, inference: Induction, semiring: NarsTruth, style: Analytical, lane: "L12", decides: "which currencies to enable (geography signal)" }, + Savant { id: 10, name: "UserCompanyAccessAdvisor", family: Some(0x80), kind: CustomerCategory, inference: Induction, semiring: NarsTruth, style: Empathic, lane: "L12", decides: "branch-access subset by user role/context" }, + Savant { id: 11, name: "ProcurementRuleSelector", family: None, kind: NextBestAction, inference: Induction, semiring: NarsTruth, style: Analytical, lane: "L13", decides: "route among equal-sequence rules (lead/availability/reliability)" }, + Savant { id: 12, name: "ReorderTimingAdvisor", family: None, kind: NextBestAction, inference: Induction, semiring: NarsTruth, style: Analytical, lane: "L13", decides: "reorder timing under demand/supplier uncertainty" }, + Savant { id: 13, name: "ReplenishmentReportAdvisor", family: None, kind: NextBestAction, inference: Induction, semiring: NarsTruth, style: Analytical, lane: "L13", decides: "real shortfall vs demand noise in the replenishment report" }, + Savant { id: 14, name: "RouteTiebreaker", family: None, kind: NextBestAction, inference: Abduction, semiring: NarsTruth, style: Analytical, lane: "L13", decides: "equal-sequence route tiebreak (supplier lead/cost/capacity)" }, + Savant { id: 15, name: "TaxExigibilitySuggestor", family: Some(0x62), kind: NextBestAction, inference: Induction, semiring: NarsTruth, style: Analytical, lane: "L15", decides: "tax exigibility (on-invoice vs on-payment / cash-basis)" }, + // ── L1–L7 original lanes (10; id 16 intentionally absent per SAVANTS.md) ── + Savant { id: 17, name: "AutopostRecommender", family: Some(0x81), kind: PostingAnomaly, inference: Induction, semiring: NarsTruth, style: Analytical, lane: "L1", decides: "recommend auto-posting bills after 3+ unmodified from a partner" }, + Savant { id: 18, name: "LockDateAdvancer", family: Some(0x81), kind: PostingAnomaly, inference: Abduction, semiring: NarsTruth, style: Analytical, lane: "L1", decides: "which next open period to advance a move into when date is locked" }, + Savant { id: 19, name: "ReconcileMatchSelector", family: None, kind: Other(other_kind::RECONCILE_MATCH), inference: Induction, semiring: NarsTruth, style: Analytical, lane: "L2", decides: "which open items to propose as reconciliation candidates" }, + Savant { id: 20, name: "BankStatementMatcher", family: None, kind: Other(other_kind::BANK_STATEMENT_MATCH), inference: Induction, semiring: NarsTruth, style: Analytical, lane: "L5", decides: "which reconcile-model rule matches a bank line + write-offs" }, + Savant { id: 21, name: "PaymentToInvoiceMatcher", family: None, kind: Other(other_kind::RECONCILE_MATCH), inference: Induction, semiring: NarsTruth, style: Analytical, lane: "L5", decides: "whether a payment fully reconciles open invoices (Mahnwesen gate)" }, + Savant { id: 22, name: "UpsellActivityTrigger", family: Some(0x81), kind: NextBestAction, inference: Induction, semiring: NarsTruth, style: Exploratory, lane: "L6", decides: "qty_delivered>ordered ⇒ upsell TODO for salesperson" }, + Savant { id: 23, name: "PricelistRecommender", family: Some(0x81), kind: NextBestAction, inference: Synthesis, semiring: NarsTruth, style: Exploratory, lane: "L6", decides: "which pricelist rule when multiple candidates apply" }, + Savant { id: 24, name: "RemovalStrategySelector", family: None, kind: NextBestAction, inference: Induction, semiring: XorBundle, style: Exploratory, lane: "L7", decides: "which quants to bind to a reservation (FIFO/FEFO/LIFO/closest)" }, + Savant { id: 25, name: "MoveAssignmentPrioritizer", family: None, kind: NextBestAction, inference: Induction, semiring: NarsTruth, style: Exploratory, lane: "L7", decides: "which confirmed moves to satisfy first (priority/deadline/quants)" }, + Savant { id: 26, name: "BackorderJudge", family: None, kind: NextBestAction, inference: Abduction, semiring: NarsTruth, style: Exploratory, lane: "L7", decides: "partial fulfilment ⇒ backorder vs cancel remainder" }, +]; + +/// Look up a savant by roster id. +#[inline] +pub fn savant(id: u8) -> Option<&'static Savant> { + SAVANTS.iter().find(|s| s.id == id) +} + +/// Look up a savant by name. +#[inline] +pub fn savant_by_name(name: &str) -> Option<&'static Savant> { + SAVANTS.iter().find(|s| s.name == name) +} + +/// All savants still awaiting a Layer-2 alignment axiom (`family = None`). +pub fn unaligned() -> impl Iterator { + SAVANTS.iter().filter(|s| s.family.is_none()) +} + +impl Savant { + /// The runtime query strategy this savant dispatches to. + pub fn query_strategy(&self) -> crate::nars::QueryStrategy { + self.inference.default_strategy() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn roster_is_25_with_unique_ids() { + assert_eq!(SAVANTS.len(), 25); + for s in &SAVANTS { + assert!(!s.name.is_empty() && !s.decides.is_empty()); + assert_eq!(savant(s.id).map(|x| x.name), Some(s.name), "id lookup round-trips"); + } + assert!(savant(16).is_none(), "id 16 intentionally absent"); + } + + #[test] + fn lookups_and_dispatch() { + let fp = savant_by_name("FiscalPositionResolver").unwrap(); + assert_eq!(fp.id, 1); + assert_eq!(fp.family, Some(0x80)); + // Deduction → CamExact (per InferenceType::default_strategy). + assert_eq!(fp.query_strategy(), crate::nars::QueryStrategy::CamExact); + } + + #[test] + fn unaligned_savants_need_axioms() { + // The stock.* / analytic.model / reconcile savants carry family=None. + let names: Vec<&str> = unaligned().map(|s| s.name).collect(); + assert!(names.contains(&"ProcurementRuleSelector")); + assert!(names.contains(&"ReconcileMatchSelector")); + assert!(!names.contains(&"FiscalPositionResolver")); // 0x80, aligned + assert!(unaligned().count() >= 9); + } +} diff --git a/crates/lance-graph-ontology/src/hydrators/fibo.rs b/crates/lance-graph-ontology/src/hydrators/fibo.rs index f4d7e23c..ee223f2f 100644 --- a/crates/lance-graph-ontology/src/hydrators/fibo.rs +++ b/crates/lance-graph-ontology/src/hydrators/fibo.rs @@ -130,7 +130,7 @@ pub fn hydrate_fibo_be_from( g: OGIT::FIBOBE_V1.0, version: OGIT::FIBOBE_V1.1, domain_name: "fibobe".to_string(), - inherits_from: Some(OGIT::DOLCE_V1.0), + inherits_from: Some(OGIT::FIBOFND_V1.0), starting_entity_id: 100, }; hydrator.hydrate_many(&path_refs, registry)?; diff --git a/crates/lance-graph-ontology/src/hydrators/skr_datev.rs b/crates/lance-graph-ontology/src/hydrators/skr_datev.rs index f223ff79..e02f5ac5 100644 --- a/crates/lance-graph-ontology/src/hydrators/skr_datev.rs +++ b/crates/lance-graph-ontology/src/hydrators/skr_datev.rs @@ -46,7 +46,7 @@ pub fn hydrate_skr03_from( g: OGIT::SKR03_V1.0, version: OGIT::SKR03_V1.1, domain_name: "skr03".to_string(), - inherits_from: Some(OGIT::DOLCE_V1.0), + inherits_from: Some(OGIT::FIBOFND_V1.0), starting_entity_id: 100, iri_prefix: SKR03_IRI_PREFIX.to_string(), }; @@ -92,7 +92,7 @@ pub fn hydrate_skr04_from( g: OGIT::SKR04_V1.0, version: OGIT::SKR04_V1.1, domain_name: "skr04".to_string(), - inherits_from: Some(OGIT::DOLCE_V1.0), + inherits_from: Some(OGIT::FIBOFND_V1.0), starting_entity_id: 100, iri_prefix: SKR04_IRI_PREFIX.to_string(), }; diff --git a/crates/lance-graph-ontology/src/hydrators/zugferd.rs b/crates/lance-graph-ontology/src/hydrators/zugferd.rs index 2db64f02..b8b6df9b 100644 --- a/crates/lance-graph-ontology/src/hydrators/zugferd.rs +++ b/crates/lance-graph-ontology/src/hydrators/zugferd.rs @@ -83,7 +83,7 @@ pub fn hydrate_zugferd_from( g: OGIT::ZUGFERD_V1.0, version: OGIT::ZUGFERD_V1.1, domain_name: "zugferd".to_string(), - inherits_from: Some(OGIT::DOLCE_V1.0), + inherits_from: Some(OGIT::FIBOFND_V1.0), starting_entity_id: 100, }; hydrator.hydrate_many(&path_refs, registry)?; diff --git a/crates/lance-graph-ontology/tests/fibo_be_hydrator_smoke.rs b/crates/lance-graph-ontology/tests/fibo_be_hydrator_smoke.rs index d2e92249..82559ff9 100644 --- a/crates/lance-graph-ontology/tests/fibo_be_hydrator_smoke.rs +++ b/crates/lance-graph-ontology/tests/fibo_be_hydrator_smoke.rs @@ -21,8 +21,8 @@ fn fibo_be_hydrator_smoke() { assert_eq!(bundle.domain_name, "fibobe"); assert_eq!( bundle.inherits_from, - Some(OGIT::DOLCE_V1.0), - "FIBO-BE inherits_from must be DOLCE" + Some(OGIT::FIBOFND_V1.0), + "FIBO-BE inherits_from must be FIBOFND (BE → FND → DOLCE, PR #416)" ); let entity_count = bundle.entity_count(); @@ -69,13 +69,13 @@ fn fibo_be_canonical_iris_resolve() { } #[test] -fn fibo_be_inherits_from_dolce() { - // BE declares inherits_from: dolce directly today. A future PR may - // chain BE → FND → DOLCE once multi-parent inheritance lands. +fn fibo_be_inherits_from_fibofnd() { + // BE now chains BE → FND → DOLCE (FND itself inherits DOLCE); re-parented + // from dolce-direct in PR #416 (FIBU subtree under fibofnd). let registry = OntologyRegistry::new_in_memory(); hydrate_fibo_be(®istry).expect("FIBO-BE hydrates"); let bundle = registry .bundle_for(OGIT::FIBOBE_V1.0) .expect("bundle registered"); - assert_eq!(bundle.inherits_from, Some(OGIT::DOLCE_V1.0)); + assert_eq!(bundle.inherits_from, Some(OGIT::FIBOFND_V1.0)); } diff --git a/crates/lance-graph-ontology/tests/skr_hydrator_smoke.rs b/crates/lance-graph-ontology/tests/skr_hydrator_smoke.rs index 46a5f500..7f865832 100644 --- a/crates/lance-graph-ontology/tests/skr_hydrator_smoke.rs +++ b/crates/lance-graph-ontology/tests/skr_hydrator_smoke.rs @@ -27,7 +27,7 @@ fn skr03_hydrator_smoke() { .expect("ContextBundle registered at SKR03_V1"); assert_eq!(bundle.g, OGIT::SKR03_V1.0); assert_eq!(bundle.domain_name, "skr03"); - assert_eq!(bundle.inherits_from, Some(OGIT::DOLCE_V1.0)); + assert_eq!(bundle.inherits_from, Some(OGIT::FIBOFND_V1.0)); assert!( bundle.entity_count() >= 1400, "expected >= 1400 canonical SKR 03 accounts, got {}", @@ -46,7 +46,7 @@ fn skr04_hydrator_smoke() { .expect("ContextBundle registered at SKR04_V1"); assert_eq!(bundle.g, OGIT::SKR04_V1.0); assert_eq!(bundle.domain_name, "skr04"); - assert_eq!(bundle.inherits_from, Some(OGIT::DOLCE_V1.0)); + assert_eq!(bundle.inherits_from, Some(OGIT::FIBOFND_V1.0)); assert!( bundle.entity_count() >= 1200, "expected >= 1200 SKR 04 accounts, got {}", diff --git a/crates/lance-graph-ontology/tests/zugferd_hydrator_smoke.rs b/crates/lance-graph-ontology/tests/zugferd_hydrator_smoke.rs index 12783635..ced2ca5b 100644 --- a/crates/lance-graph-ontology/tests/zugferd_hydrator_smoke.rs +++ b/crates/lance-graph-ontology/tests/zugferd_hydrator_smoke.rs @@ -26,8 +26,8 @@ fn zugferd_hydrator_smoke() { assert_eq!(bundle.domain_name, "zugferd"); assert_eq!( bundle.inherits_from, - Some(OGIT::DOLCE_V1.0), - "ZUGFeRD inherits_from must be DOLCE" + Some(OGIT::FIBOFND_V1.0), + "ZUGFeRD inherits_from must be FIBOFND (PR #416)" ); // EN16931 profile covers 4 XSD files: top-level CrossIndustryInvoice, diff --git a/modules/fibobe/manifest.yaml b/modules/fibobe/manifest.yaml index 0f3e5c58..e200e0ef 100644 --- a/modules/fibobe/manifest.yaml +++ b/modules/fibobe/manifest.yaml @@ -20,4 +20,4 @@ rbac_policy: ~ stack_profile: ~ action_capabilities: {} actor: ~ -inherits_from: dolce +inherits_from: fibofnd diff --git a/modules/skr03/manifest.yaml b/modules/skr03/manifest.yaml index f4d21491..3955489c 100644 --- a/modules/skr03/manifest.yaml +++ b/modules/skr03/manifest.yaml @@ -30,4 +30,4 @@ rbac_policy: ~ stack_profile: ~ action_capabilities: {} actor: ~ -inherits_from: dolce +inherits_from: fibofnd diff --git a/modules/skr04/manifest.yaml b/modules/skr04/manifest.yaml index 84776d75..17fa9519 100644 --- a/modules/skr04/manifest.yaml +++ b/modules/skr04/manifest.yaml @@ -31,4 +31,4 @@ rbac_policy: ~ stack_profile: ~ action_capabilities: {} actor: ~ -inherits_from: dolce +inherits_from: fibofnd diff --git a/modules/zugferd/manifest.yaml b/modules/zugferd/manifest.yaml index 6bd8670a..96a818d3 100644 --- a/modules/zugferd/manifest.yaml +++ b/modules/zugferd/manifest.yaml @@ -21,4 +21,4 @@ rbac_policy: ~ stack_profile: ~ action_capabilities: {} actor: ~ -inherits_from: dolce +inherits_from: fibofnd