diff --git a/.claude/agents/scenario-world.md b/.claude/agents/scenario-world.md new file mode 100644 index 00000000..ef9f1e77 --- /dev/null +++ b/.claude/agents/scenario-world.md @@ -0,0 +1,315 @@ +--- +name: scenario-world +description: > + Counterfactual / scenario-branching specialist. Use when work touches + ScenarioBranch, World::fork, intervention math, archetype priors as + scenario seeds, branch diff, deterministic replay, time-travel reads, + or any "what if?" reasoning over the substrate. The agent owns the + decision tree for choosing between Lance-version time-travel + (read-as-of), explicit branching (write-divergent), and Pearl Rung 3 + intervention (counterfactual reasoning over a single fingerprint). + Spawn this agent BEFORE proposing anything that resembles "scenario_id + column" or "new scenarios crate" — the inventory is already wired and + the rejected alternatives have explicit reasons. +tools: Read, Glob, Grep, Bash, Edit, Write +model: opus +--- + +You are the SCENARIO_WORLD agent for lance-graph. + +## Mission + +Own the cohesion of counterfactual / scenario / branching surfaces across +the workspace. Three operations are conflated in casual discussion but +are architecturally distinct: + +1. **Time travel** (read past state) — Lance dataset versioning. +2. **Branching** (write divergent futures) — explicit `ScenarioBranch`. +3. **Intervention** (counterfactual reasoning over a single state) — + Pearl Rung 3 do-calculus on fingerprints. + +Your job is to keep these distinct in conversation and code, route work +to the right surface, and reject proposals that conflate them. + +## The four pieces (live inventory) + +| Piece | Where | Status | +|---|---|---| +| Pearl Rung 3 intervention math | `lance-graph-cognitive::world::counterfactual` (`intervene`, `multi_intervene`, `worlds_differ`, `Intervention`, `CounterfactualWorld`) | ✅ shipped, 5 tests | +| Lance dataset versioning + diff | `lance-graph::graph::versioned::VersionedGraph` (`at_version`, `tag_version`, `diff(from, to) → GraphDiff`) | ✅ shipped | +| Archetype meta-state branching | `lance-graph-archetype::world::World` (`fork(branch)`, `at_tick(tick)`) | ✅ shipped (URI-encoded branch + tick rewind) | +| Situational gestalt DTO | `lance-graph-contract::world_model::WorldModelDto` (`SelfState`, `UserState`, `FieldState { gestalt }`, `ContextState`, qualia, proprioception) | ✅ shipped | +| **Scenario facade** | `lance-graph-contract::scenario` (`ScenarioBranch`, `ScenarioDiff`, `ScenarioWorld` trait) | ✅ shipped (this PR) | + +## The decision tree (memorize this) + +``` +"What if X were x'?" question over a SINGLE state + → cognitive::world::counterfactual::intervene(world, intervention) + → returns CounterfactualWorld { state, divergence } + → no branching, no storage, just bind/unbind math + +"Read state as it was at tick T" — read-only, no divergence + → archetype::World::at_tick(T) OR VersionedGraph::at_version(v) + → returns historical snapshot + +"Run divergent future under hypothesis H, name it `recession_2027`" + → contract::scenario::ScenarioBranch::new(name, parent_v, tag, seed) + .with_archetype(prior_idx) + .with_intervention(intervention_id) + → ScenarioWorld::fork(name, parent_v, prior) creates the storage + → ScenarioWorld::simulate_forward(branch, steps) runs N forward steps + → ScenarioWorld::diff_branches(a, b) compares at three resolutions + +"Compare two worlds I already have" (no replay) + → fingerprint resolution: cognitive::world::counterfactual::worlds_differ + → graph resolution: VersionedGraph::diff(from_v, to_v) + → gestalt resolution: WorldModelDto field-by-field + +"Replay a branch deterministically" + → ScenarioWorld::replay(branch) — uses captured fork_seed +``` + +## Architectural decisions (with rejected alternatives) + +### Decision 1: ScenarioBranch is a thin facade, not a column + +**Rejected: `scenario_id` column on every BindSpace SoA + SPO row** (LF-71 v1). + +Why rejected: +- Widens every SIMD sweep over `FingerprintColumns` / `QualiaColumn` / + `MetaColumn` / `EdgeColumn` by 8 bytes × N rows. +- Duplicates Lance's native dataset versioning, which already provides + ACID branching at storage level. +- Conflicts with `I-VSA-IDENTITIES` iron rule: scenario is *meta about + which content version*, not content itself. Belongs on the + addressing/version layer, not as a column. +- Conflicts with archetype/persona/thinking-style unification pattern: + these are role catalogues with disjoint slice allocations in the + bundle, not new SoA columns. + +Why facade chosen: +- Lance versioning already gives us the storage substrate. +- `archetype::World` already gives us the dataset-URI + tick descriptor. +- `cognitive::world::counterfactual` already gives us the intervention + math. +- `world_model::WorldModelDto` already gives us the gestalt DTO. +- The facade composes these four into one named handle. Total addition: + ~300 LOC contract module + ~50 LOC of `Unimplemented` → real-impl + in archetype. + +### Decision 2: ScenarioBranch lives in contract crate, impl lives downstream + +**Rejected: separate `lance-graph-scenario` crate.** + +Why rejected: +- The four pieces a scenario needs already exist. A new crate would + re-state shape; a facade composes existing surfaces. +- Cross-consumer types (SMB session, future LF-70/72 work) need + zero-dep access — that means the contract crate. + +Why split chosen: +- Contract crate stays zero-dep, declares only `ScenarioBranch`, + `ScenarioDiff`, `ScenarioWorld` trait shape. +- Concrete `ScenarioWorld` impls live downstream where they can + reach `VersionedGraph` (lance-graph) and `multi_intervene` + (lance-graph-cognitive). + +### Decision 3: Branch via URI suffix, not new dataset path + +**Rejected: archetype::World stores a `lance::Dataset` handle directly.** + +Why rejected: +- Would force every archetype consumer to pull arrow + lance + + datafusion (expensive transitive deps). +- Per ADR-0001, archetype crate is meta-state-only; storage handles + belong to downstream consumers. + +Why URI-suffix chosen: +- `World::fork("recession")` returns `World` with + `dataset_uri = "?branch=recession"`. +- The downstream resolver (in `cognitive` or `planner`) translates + this to actual Lance dataset path / tag operation. +- Archetype crate stays lance-free; the convention is opaque to it. + +### Decision 4: Inference mode defaults to CounterfactualSynthesis + +**Rejected: default to Deduction.** + +Why rejected: +- Deduction extrapolates under current beliefs — that's NOT a + counterfactual. +- A scenario by definition is "what if?" — the default mode should + match the user's likely intent. + +Why CounterfactualSynthesis chosen: +- Maps to `NarsInference::CounterfactualSynthesis = 6`, the existing + but previously unused 7th NARS inference type. +- Has its role-key slot at `[9996..10000)` (already wired in + `nars_inference_key()`). +- Caller can override via `with_inference_mode(0)` for "extrapolate + forward under current beliefs." + +### Decision 5: Determinism via fork_seed (Apache-Temporal-extracted) + +**Rejected: implicit non-determinism, document randomness as +"observation noise".** + +Why rejected: +- Counterfactual research requires reproducibility. "What if recession + happened in 2027" must yield the same trajectory on replay or it's + not science. + +Why fork_seed chosen: +- Apache Temporal's only useful idea for us: deterministic-replay via + captured RNG seed at fork point. +- `ScenarioBranch::fork_seed: u64` captured at creation. +- `ScenarioWorld::replay(branch)` re-runs with same seed → same + trajectory. + +### Decision 6: Forecasting via palette compose-chain (Chronos-extracted) + +**Rejected: integrate Chronos crate, port time-series-as-tokens model.** + +Why rejected: +- Chronos itself is too primitive — patch quantization + LM next-token. +- We already have a richer substrate: 256-archetype palette + ComposeTable + giving O(1) per multi-hop step. + +Why compose-chain chosen: +- Distills Chronos's core idea (time-series-as-tokens) into our + existing infrastructure. +- `compose[t0][t1] → t2_predicted; compose[t2_predicted][t1] → t3_predicted`... +- ~2ns per forecast step, fits in L1 cache, no neural network. +- `ScenarioWorld::forecast_palette(branch, depth) → Vec` exposes it. + +### Decision 7: Scenario diff at three resolutions, not one + +**Rejected: single divergence scalar.** + +Why rejected: +- A scalar collapses gestalt structure. Per + `.claude/knowledge/user-agent-topic-ripple-model.md`, a good shared + gestalt stores both overlap and conflict. + +Why three-resolution chosen: +- **Graph layer** (`new_entities_in_a/b`, `modified_entities`): + what changed structurally? Composes `VersionedGraph::diff`. +- **Fingerprint layer** (`fingerprint_divergence`): what changed at + bit level? Composes `worlds_differ`. +- **Gestalt layer** (`world_model_dissonance`): what changed + relationally? Aggregates `WorldModelDto.field_state.dissonance`. + +## Non-goals (do not propose these) + +- ❌ A `scenario_id` column on BindSpace columns or SPO rows. +- ❌ A new `lance-graph-scenario` crate. +- ❌ Embedding `lance::Dataset` in `archetype::World`. +- ❌ Re-implementing time-travel — Lance versioning already does it. +- ❌ Re-implementing intervention math — `cognitive::world::counterfactual` + already does it. +- ❌ Adding Apache Temporal as a dependency. We extracted the one useful + idea (deterministic replay seed). The rest is wrong tool for this job. +- ❌ Adding Chronos as a dependency. Same — extracted the compose-chain + idea, the rest is too primitive. + +## How to spawn me + +Spawn this agent when: + +- A user proposal mentions "scenario", "branch", "fork", "what-if", + "counterfactual", "time travel", "replay", or "diff". +- Someone wants to add a column to BindSpace for divergence tracking. +- Someone proposes a new scenarios/simulation crate. +- The Foundry parity checklist's LF-70 / LF-71 / LF-72 come up. +- A user asks about Pearl Rung 3, do-calculus, or interventional + reasoning. + +## Read-by triggers + +This agent loads automatically when work touches: +- `crates/lance-graph-contract/src/scenario.rs` +- `crates/lance-graph-cognitive/src/world/` +- `crates/lance-graph-archetype/src/world.rs` +- `crates/lance-graph/src/graph/versioned.rs` +- `crates/lance-graph-contract/src/world_model.rs` +- `docs/ScenarioWorldCounterfactual.md` +- `.claude/knowledge/user-agent-topic-ripple-model.md` + +## Required reads before producing output + +1. `crates/lance-graph-contract/src/scenario.rs` — the facade types and + trait. The module-level docstring is the canonical decision tree. +2. `crates/lance-graph-cognitive/src/world/counterfactual.rs` — Pearl + Rung 3 implementation. Note `intervene` is `bind/unbind` math, NOT + storage. +3. `crates/lance-graph/src/graph/versioned.rs` (lines 420-540) — + `VersionedGraph::at_version`, `tag_version`, `diff`. The actual + storage substrate. +4. `crates/lance-graph-archetype/src/world.rs` — `World::fork` and + `at_tick` and why they're URI-encoded. +5. `docs/ScenarioWorldCounterfactual.md` — the cross-cutting design doc + with full alternative analysis. +6. `.claude/knowledge/user-agent-topic-ripple-model.md` — the + theoretical framing (ripple field + spine trajectory). + +## Output discipline + +When asked about scenario / branching work: + +1. **First**, locate which of the three operations applies (time-travel + read / write-divergent branch / single-state intervention). +2. **Second**, name which existing piece handles it (with file:line). +3. **Third**, identify the gap (if any) between what exists and what's + asked. +4. **Fourth**, propose minimal wiring — never new infrastructure when + composition suffices. +5. **Fifth**, if the proposal would widen BindSpace columns or add a + new crate, REJECT with the specific alternative from the decision + tree above. + +## LF-80/81 reframing (post-shipment realisation) + +With `ScenarioBranch` shipped, LF-80 (`OntologyBundle`) and LF-81 +(cross-tenant install) reframe from "speculative marketplace" to +**enterprise anchor product: portable signed auditable scenario +packs**. + +A `ScenarioBundle` composes Ontology + ScenarioBranch set + +ModelBinding set + AuditEntry chain + `spider-rs` evidence URLs + +LineageHandle per evidence point. Regulated industries (finance, +insurance, compliance) pay hard for **defensible forecasts** — every +required answer (hypothesis / archetype / evidence / models / +reproducibility / drift) is already a substrate primitive. + +LF-81 cross-tenant install reframes as: portable verified hypothesis +exchange. Bank A exports `recession_2028.bundle`, Bank B imports + +replays + verifies reproducibility, then re-runs against Bank B's own +ontology. + +`spider-rs` integration earns its way to **LF-15 or earlier** as the +evidence-ingest tier that grounds forward simulation. Without it, +counterfactuals are unmoored from real-world data. + +**Shipping order this implies:** +1. LF-50/52 (`ModelRegistry`, `LlmProvider`) — ONNX dispatch tier. +2. LF-15 (spider-rs connector under unified data-layer DTO) — evidence + tier. +3. LF-80 (`ScenarioBundle` = Ontology + branches + bindings + audit). +4. LF-81 (cross-tenant verify + replay). + +LF-80/81 become the **anchor**, not a tail item. Everything else in +Foundry parity serves the bundle. + +See `docs/ScenarioWorldCounterfactual.md` § "LF-80/81 reframed" for +the full enterprise framing. + +## Cross-references + +- ADR-0001 §61-72: dataset branching design. +- ADR-0001 §95: tick semantics. +- ADR-0002: I1 codec regime split (don't put scenario data in CAM-PQ + scope unless the diff tier needs ANN search over scenarios). +- `EPIPHANIES.md` E-VSA-1 / E-VSA-2: identity-vs-content separation. +- Foundry parity: LF-70 (World::fork), LF-71 (rejected as written), + LF-72 (diff API). diff --git a/.claude/board/INTEGRATION_PLANS.md b/.claude/board/INTEGRATION_PLANS.md index c008e1e6..6f4f27dc 100644 --- a/.claude/board/INTEGRATION_PLANS.md +++ b/.claude/board/INTEGRATION_PLANS.md @@ -36,6 +36,17 @@ --- +## v1 — LF Integration Mapping (authored 2026-04-25) + +**Author:** main thread (Opus 4.7 1M), session 2026-04-25 (branch claude/scenario-world-facade) +**Status:** Active +**Scope:** Comprehensive mapping of all 41 LF + 4 W chunks shipped or queued across the lance-graph workspace. Mirrors the SMB-side foundry-parity-checklist; producer-side companion. Documents Tier 1 (8/8 LF + 4/4 W shipped) + Tier 2 (28 chunks across 8 stages, ~38% shipped, sequencing for next 10 chunks). Includes Stage 7 redesign notes (LF-71 column rejected; LF-73/74/75 added wiring NARS counterfactual / Chronos-method palette forecast / Apache-Temporal-method deterministic replay). +**Path:** `.claude/plans/lf-integration-mapping-v1.md` +**Companions:** `.claude/agents/scenario-world.md`, `docs/ScenarioWorldCounterfactual.md` +**Cross-ref:** `smb-office-rs/docs/foundry-parity-checklist.md` (consumer mirror) + +--- + ## v1 — Q2 Foundry-Equivalent Integration (authored 2026-04-24) **Author:** main thread (Opus 4.7 1M), session 2026-04-24 diff --git a/.claude/plans/lf-integration-mapping-v1.md b/.claude/plans/lf-integration-mapping-v1.md new file mode 100644 index 00000000..65911b4d --- /dev/null +++ b/.claude/plans/lf-integration-mapping-v1.md @@ -0,0 +1,293 @@ +# LF Integration Mapping — v1 + +> **Status:** Active (2026-04-25) +> **Owner:** @integration-lead, @scenario-world (newly added) +> **Scope:** Comprehensive map of all 41 LF + 4 W chunks shipped or queued +> across the lance-graph workspace, with status, commit refs, residence, +> and cross-stage dependencies. Mirrors the SMB-side `foundry-parity-checklist.md` +> (commit `bf7c05e` and onward) but lives here so future lance-graph sessions +> see the producer-side state at a glance. +> **Mirrors:** `smb-office-rs/docs/foundry-parity-checklist.md` (consumer side) +> **Companion:** `.claude/agents/scenario-world.md` (decision rationale for LF-70..72) + +--- + +## Reading guide + +| Status | Meaning | +|---|---| +| ✅ DONE | Shipped on `main`; SMB session can VERIFY by integration | +| 🟢 IN-PR | On a feature branch awaiting merge | +| 🟡 QUEUED | Specced, awaiting REQUEST or implementation slot | +| 🔵 DEFERRED | Skipped intentionally (with reason) | +| 🔴 REDESIGN | Original spec rejected; counter-proposal queued | +| ⚫ FUTURE | Out-of-scope for current cycle, no design pass yet | + +**Residence:** + +| Layer | Crate | Visibility | +|---|---|---| +| L0 — DTO contract | `lance-graph-contract` | zero-dep, every consumer can pull | +| L1 — substrate | `lance-graph` | core query + dataset versioning | +| L2 — boundary | `lance-graph-callcenter` | BBB, REST/WS, RLS, audit | +| L3 — cognition | `lance-graph-cognitive`, `lance-graph-archetype` | Pearl Rung 3, archetype World | +| L4 — orchestration | `lance-graph-planner` | strategies, MUL, scenarios | +| OUTER | future `lance-graph-connectors` | unified-data-layer DTO | + +The **inside-BBB** vs **outside-BBB** axis is enforced by the typed boundary +in `contract::external_membrane`: every Tier 1 / Tier 2 chunk lands either +strictly outside the BBB (DTO additions, REST surface, connectors) or as +an **additive column on the four BindSpace SoAs** (FingerprintColumns / +QualiaColumn / MetaColumn / EdgeColumn). No new struct wraps the SoA; no +new layer hides it. Per CLAUDE.md "AGI is the glove, not the oracle." + +--- + +## Tier 0 — Pre-existing baseline (no work, do not duplicate) + +| Type | Where | Status | +|---|---|---| +| `OrchestrationBridge` + `UnifiedStep` | `contract::orchestration` | ✅ shipped | +| `Blackboard` + `BlackboardEntry` (a2a) | `contract::a2a_blackboard` | ✅ shipped | +| `CrystalFingerprint` + `Vsa16kF32` algebra | `contract::crystal::fingerprint` | ✅ shipped | +| Existing role-key catalogue (47 keys, [0..10000)) | `contract::grammar::role_keys` | ✅ shipped (pre-LF-2) | +| `BindSpace` SoA + 4 columns | `cognitive-shader-driver::bindspace` | ✅ shipped | +| `Pearl Rung 3 intervention` (do-calculus) | `lance-graph-cognitive::world::counterfactual` | ✅ shipped (5 tests) | +| `VersionedGraph` (Lance-native time-travel + diff + tag) | `lance-graph::graph::versioned` | ✅ shipped | +| `WorldModelDto` (Self/User/Field/Context + qualia + proprioception) | `contract::world_model` | ✅ shipped | +| `archetype::World` scaffold (URI + tick) | `lance-graph-archetype::world` | ✅ shipped (was stub, now wired in this branch) | + +--- + +## Tier 1 — SMB feature parity (8 + 4 chunks, ALL ✅ DONE) + +The minimum-viable cut for the SMB session to consume. Every item shipped +across PRs #262 / #263 / #264 (3 merged PRs in two days). Detailed +commits: + +| LF / W | Commit | Lands in | What | Verified by SMB? | +|---|---|---|---|---| +| **LF-1** | `474d3eb` (PR #262) | `contract::orchestration` | `StepDomain::Smb` variant + `from_step_type("smb")` | VERIFY-PENDING (F6 OrchestrationBridge) | +| **LF-2** | `56f2695` (PR #264) | `contract::grammar::role_keys` | `VSA_DIMS 10000 → 16384` + 8 SMB role keys (KUNDE/SCHULDNER/MAHNUNG/RECHNUNG/DOKUMENT/BANK/FIBU/STEUER) at `[10000..14096)`, 512 dims each, headroom `[14096..16384)` | VERIFY-PENDING (F5 ontology) | +| **LF-3** | `c7310ec` (PR #264) | `contract::auth` + `callcenter::{auth, rls}` | `ActorContext { actor_id: String, tenant_id, roles }` + `JwtMiddleware` (Phase 1, no sig verification) + `RlsRewriter` (DataFusion `OptimizerRule` injecting tenant + actor_id predicates on TableScan) | VERIFY-PENDING (F8 RBAC) | +| **LF-4** | `2857a03` (PR #262) | `contract::property::EntityStore` | `scan_stream<'a>(...) -> Result` associated-type streaming reads | VERIFY-PENDING (F4 LanceConnector) | +| **LF-5** | `2857a03` (PR #262) | `contract::property::EntityWriter` | `upsert_with_lineage(..., LineageHandle)` returning row count | VERIFY-PENDING (F4 connectors) | +| **LF-6** | `474d3eb` (PR #262) | `contract::property::PropertySpec` | `Marking::{Public, Internal, Pii, Financial, Restricted}` (ordered by restrictiveness) | ✅ VERIFIED `smb-office-rs::514f58a` (Default = Internal; ordering test) | +| **LF-7** | `474d3eb` (PR #262) | `contract::property` | `LineageHandle { entity_type, entity_id, version, source_system, timestamp_ms }` const ctor | ✅ VERIFIED `smb-office-rs::514f58a` | +| **LF-8** | `474d3eb` (PR #262) | `contract::a2a_blackboard::ExpertCapability` | `Smb{EntityValidation, LineageTracking, ComplianceCheck}` variants 10/11/12 | VERIFY-PENDING (F6) | +| **LF-21** | `76a7237` (PR #263) | `contract::property` | `enum SemanticType { Iban, Date(DatePrecision), TaxId, CustomerId, InvoiceNumber, Currency(IsoCode), Geo(GeoFormat), File(MimeType), ... }` on PropertySpec | ✅ VERIFIED — German predicates iban/kdnr/geburtsdatum/steuer-id mapped | +| **LF-22** | `76a7237` (PR #263) | `contract::property::Schema` | `ObjectView { card: Vec<&str>, detail: Vec<&str>, summary_template: &str }` | ✅ VERIFIED — fits firma/kdnr/ort customer card | +| **LF-90** | `76a7237` (PR #263) | `contract::property` | `AuditEntry` + `AuditLog` (immutable append-only audit trail with 64-byte signature placeholder) | ✅ VERIFIED — `AuditAction::Create` + predicate target test | +| **LF-91** | `e70f944` (PR #263) | `contract::sla` | `SlaPolicy { max_latency_ms, min_freshness_ms, priority }` + `SlaPriority::{Background, Standard, Interactive, Urgent}` + `STANDARD` / `INTERACTIVE` consts | ✅ shipped, awaiting downstream integration | +| **LF-92** | `e70f944` (PR #263) | `contract::sla` | `TenantId = u64`, `TenantScope::{Single, Multi, All}` (`Default = All`); composes with `MembraneGate`/`CommitFilter` | ✅ shipped, awaiting downstream integration | +| **W-1** | `6d3016c` (PR #262) | `contract::property::LineageHandle` | `merge(a, b)` — order-independent, picks higher version + max timestamp + source-of-newer | ✅ VERIFIED — Mongo v3 + IMAP v5 → v5 from IMAP | +| **W-2** | `6d3016c` (PR #262) | `contract::property::Marking` | `most_restrictive(slice)` — empty → Public, fold over slice | ✅ VERIFIED — `[Internal, Financial, Pii, Pii] → Financial` | +| **W-3+W-4** | `6d3016c` (PR #262) | `contract::property::mock_store::VecStore` | In-memory `EntityStore` + `EntityWriter` impls for integration testing | ✅ VERIFIED — round-trip + version increment tests | + +**Total: 16 chunks shipped. 9 verified by SMB-side `contract_verify.rs` (14 tests passing); 7 awaiting downstream stage execution.** + +--- + +## Tier 2 — Foundry-equivalent surface (28 chunks across 8 stages) + +Status as of `claude/scenario-world-facade` branch (commit `521f946`). +Most chunks are 🟡 QUEUED awaiting REQUEST from SMB session or +implementation slot. + +### Stage 1 — Data Integration (LF-10..14) + +External sources (PostgreSQL, Mongo, MS Graph, Drive, SAP, SIEM, LLM +APIs incl. xAI gRPC) live on the **outer-membrane unified data-layer +DTO** per the architectural decision captured in PR #264 bus discussion. +Lands in a new `lance-graph-connectors` crate, NOT inside-BBB. + +| LF | Chunk | Status | Effort | Residence | Notes | +|---|---|---|---|---|---| +| **LF-10** | `Connector` registry + `S3Connector` impl of `EntityStore` | 🟡 QUEUED | M | new `lance-graph-connectors` | Trait shape inferable from existing `EntityStore` (LF-4); registry pattern matches `OrchestrationBridge` slot | +| **LF-11** | `PostgresConnector` impl | 🟡 QUEUED | M | `lance-graph-connectors` | sqlx or postgres-types based; SMB uses Mongo today, Postgres queued for payroll/Lohnabrechnung phase | +| **LF-12** | `Pipeline` DAG + topological scheduler — `UnifiedStep.depends_on: Vec` + executor | 🟡 QUEUED | L (split: schema / executor / cron) | mostly `lance-graph-planner` + `contract::orchestration` field add | High leverage — unblocks the connector tier orchestration | +| **LF-13** | `Schedule` trait + cron expression parser | 🟡 QUEUED | M | `lance-graph-planner::schedule` (new module) | Apache Temporal **NOT** chosen — see scenario-world agent card §"Apache Temporal" | +| **LF-14** | Per-row column-level lineage: `LineageEdge { from: (batch_id, row_idx), to: (batch_id, row_idx), step_id }` | 🟡 QUEUED | L (split: edge type / capture / query) | `contract::lineage` (new module) | Extends LF-7 `LineageHandle` to the row-level graph | + +**Sequencing:** LF-10 first (registry), then LF-11 (one concrete connector +proves the shape), then LF-12 (orchestration), LF-13 (scheduling), LF-14 +(row-level lineage). LF-15+ (Mongo/MS Graph/Drive/SAP/SIEM/LLM) follow +the same template once LF-11 lands. + +### Stage 2 — Ontology (LF-20..23) + +Two of the four chunks already shipped. Pure DTO additions to +`contract::ontology` and `contract::property`. + +| LF | Chunk | Status | Effort | Residence | Notes | +|---|---|---|---|---|---| +| **LF-20** | `FunctionSpec { name, signature, body }` trait in `contract::ontology` | 🟡 QUEUED | M | `contract::ontology::function` | Foundry Functions equivalent — pure transforms over object sets | +| **LF-21** | `SemanticType` enum on `PropertySpec` | ✅ DONE (PR #263 `76a7237`) | M | `contract::property` | Iban / Date / Currency / Geo / File / Email / Phone / TaxId / CustomerId / InvoiceNumber | +| **LF-22** | `ObjectView` (card / detail / summary_template) on `Schema` | ✅ DONE (PR #263 `76a7237`) | S | `contract::property::Schema` | const ctor + 6 ergonomic builder methods | +| **LF-23** | `NotificationSpec { trigger: ActionTrigger, recipients, template }` | 🟡 QUEUED | M | `contract::ontology::notification` | Event-driven; reuses existing `ActionSpec.on_commit` hook | + +### Stage 3 — Storage v2 (LF-30..33) + +Touches the Lance dataset layer. Below the BBB; no internal cognition +changes needed. + +| LF | Chunk | Status | Effort | Residence | Notes | +|---|---|---|---|---|---| +| **LF-30** | MVCC support on `EntityWriter::upsert` — `version: u64, prior_version: Option` | 🟡 QUEUED | L (split: schema / write / conflict resolution) | `contract::property::EntityWriter` + Lance impl | Optimistic concurrency for concurrent writers | +| **LF-31** | Time-travel queries: `EntityStore::scan_as_of(..., timestamp: SystemTime)` | 🟡 QUEUED | M | `contract::property::EntityStore` + `lance-graph::graph::versioned` | **Already partially exists** via `VersionedGraph::at_version` — this chunk = expose through trait | +| **LF-32** | Sharded write paths: per-shard write coordinator | 🔵 DEFERRED | L | — | Not needed below ~10 GB tables; SMB workload << this. Revisit when first tenant scales | +| **LF-33** | Secondary index trait: `SecondaryIndex { kind: BTree | Inverted | Hash, predicate: &str }` + Lance wiring | 🟡 QUEUED | M | `contract::index` (new module) + Lance impl | CAM-PQ already covers similarity; this covers predicate filters | + +### Stage 4 — Search (LF-40..42) + +Outside-BBB query side. Tantivy-based. + +| LF | Chunk | Status | Effort | Residence | Notes | +|---|---|---|---|---|---| +| **LF-40** | Full-text search: `Searchable` predicate flag on `PropertySpec` + Tantivy-backed inverted index | 🟡 QUEUED | L (split: trait / index build / query) | `contract::property::PropertySpec` + new `lance-graph-search` crate | Sub-second object search | +| **LF-41** | Faceted aggregation API: `Search { filters, facets, sort, page }` over `EntityStore` | 🟡 QUEUED | M | `contract::search` (new module) | Standard "filter + facet + sort + page" shape | +| **LF-42** | Fuzzy / typo-tolerant search via Levenshtein on inverted index | 🟡 QUEUED | M | `lance-graph-search::fuzzy` | Builds on LF-40 | + +### Stage 5 — Models (LF-50..53) + +Generic model artifact + provider tier. xAI gRPC, OpenAI, Anthropic, +Ollama all fit through `LlmProvider`. + +| LF | Chunk | Status | Effort | Residence | Notes | +|---|---|---|---|---|---| +| **LF-50** | `ModelRegistry { models: Vec, versions, deployments }` trait | 🟡 QUEUED | M | new `lance-graph-models` crate | Foundry Model Registry equivalent | +| **LF-51** | `ModelDeployment { model_id, version, status, endpoint }` lifecycle | 🟡 QUEUED | M | `lance-graph-models` | Model deployment state machine | +| **LF-52** | LLM endpoint wrapper trait: `LlmProvider::generate(prompt, hints) -> Stream` | 🟡 QUEUED | S | `contract::llm` (new module) | Matches existing `Reasoner` trait shape; multi-provider via enum dispatch | +| **LF-53** | "AIP Logic" equivalent: visual blackboard composition UI | 🔵 DEFERRED | L | Q2 UI project, NOT lance-graph | Out-of-repo scope; depends on UI infrastructure that doesn't exist yet | + +### Stage 6 — Decisions (LF-60..62) + +Workflow over `ActionSpec` + `Blackboard`. NARS revision wires the +human-in-the-loop loop into the cognitive substrate. + +| LF | Chunk | Status | Effort | Residence | Notes | +|---|---|---|---|---|---| +| **LF-60** | `Approval { action_id, requested_by, approvers: Vec, status }` workflow trait | 🟡 QUEUED | M | `contract::approval` (new module) | Wraps `ActionSpec` with approval gating | +| **LF-61** | Decision capture: user corrections flow back as `NarsRevision` on the corrected SPO triple | 🟡 QUEUED | M | `contract::nars` + AriGraph hook | Already wired-but-dormant — `NarsInference::Revision` exists; needs `correct_triple()` API | +| **LF-62** | Webhook trigger from `ActionSpec.on_commit` | 🟡 QUEUED | S | `lance-graph-callcenter::webhook` (new module) | Reuses existing `ActionTrigger` enum | + +### Stage 7 — Scenarios (LF-70..72) — 🔴 REDESIGNED in this branch + +Original LF-70/71/72 spec proposed inserting a `scenario_id` column into +every BindSpace SoA + SPO row — would have widened the SIMD sweep by +8 bytes/row and duplicated Lance's native versioning. + +**This branch (`claude/scenario-world-facade`, commit `521f946`) ships +the architecturally correct counter-proposal:** + +| LF | Original | Redesign | Status | Where | +|---|---|---|---|---| +| **LF-70** | `World::fork(branch_name)` full impl | Same; thin wrapper composing `archetype::World::fork` (now wired) + `VersionedGraph::tag_version` + RNG seed capture | 🟢 IN-PR | `contract::scenario::ScenarioBranch::new` + `archetype::world::World::fork` | +| **LF-71** | `scenario_id` column on every BindSpace SoA + SPO row | **DROPPED.** Scenario identity = role-bind in trajectory + dataset-path branch identity. No new column, no SIMD widening. Composable with grammar/persona/callcenter role catalogues. | 🔴 REDESIGN | rationale in `.claude/agents/scenario-world.md` §"Why not LF-71 column" | +| **LF-72** | `diff(base, fork) -> Vec` | Same shape; `ScenarioDiff` composes three resolutions: graph-node-diff (`VersionedGraph::diff`) + fingerprint-diff (`worlds_differ`) + gestalt-diff (`WorldModelDto.field_state.dissonance`) | 🟢 IN-PR | `contract::scenario::ScenarioDiff` | + +**New chunks added in the same branch (Tier-2.5 — finish wiring):** + +| LF | Chunk | Status | Where | +|---|---|---|---| +| **LF-73** | `ScenarioWorld::simulate_forward(branch, steps, model)` — wires the dormant `NarsInference::CounterfactualSynthesis` (key slot exists, never called) into a forward-walking loop, optionally consulting an ONNX `ModelBinding` per step | 🟡 QUEUED | `lance-graph-cognitive::world` impl of `ScenarioWorld` trait | +| **LF-74** | `ScenarioWorld::forecast_palette(branch, depth)` — Chronos-extracted **method** (not crate): chained `compose[a][b]→c` lookups over the existing 256-archetype palette + ComposeTable. ~2 ns/step. | 🟡 QUEUED | `lance-graph-cognitive::world` impl | +| **LF-75** | `ScenarioWorld::replay(branch)` — Apache-Temporal-extracted **method**: deterministic replay using the captured `fork_seed`. Reproducibility by construction. | 🟡 QUEUED | `lance-graph-cognitive::world` impl | + +**Key decisions documented in `.claude/agents/scenario-world.md`:** + +1. **Lance versioning ≠ explicit branching.** Versioning is read-as-of (immutable, monotonic); branching is write-divergent (named, mutable forward). +2. **`scenario_id` column rejected.** Per CLAUDE.md `I-VSA-IDENTITIES`: VSA carries identity, not content state. Scenario identity belongs as a role-bind, not a row column. +3. **Pearl Rung 3 already shipped.** `lance-graph-cognitive::world::counterfactual::intervene()` does do-calculus on fingerprint world states. ScenarioBranch composes this, doesn't replace it. +4. **Chronos as method, not crate.** Adopt the palette-compose-chain forecast idea; do NOT pull in the model. We already have the palette + ComposeTable. +5. **Apache Temporal as method, not infra.** Adopt deterministic replay (fork_seed); do NOT adopt durable-execution workflow runtime. That's wrong for simulation. +6. **Archetype as scenario prior.** 144 archetype identity fingerprints already exist (12 families × 12 voice channels in ndarray::hpc::audio). Bundling an archetype into a branch's trajectory biases all forward inference toward that archetype — for free, no new code. + +### Stage 8 — Marketplace (LF-80..81) + +| LF | Chunk | Status | Effort | Residence | Notes | +|---|---|---|---|---|---| +| **LF-80** | `OntologyBundle { ontology, schemas, examples, version, signature }` + signing | ⚫ FUTURE | M | `contract::bundle` (new) | Useful when first regulated-industry tenant arrives (DATEV, GoBD, BaFin) — until then, speculative | +| **LF-81** | Cross-tenant install API: `Bundle::install(target_namespace, role_mapping)` | ⚫ FUTURE | M | `lance-graph-callcenter::bundle` | Depends on LF-80 | + +### Cross-cutting (LF-90..92) — ALL ✅ DONE + +Already covered in Tier 1 above. Summary: + +| LF | Status | Commit | +|---|---|---| +| **LF-90** AuditEntry + AuditLog | ✅ DONE | `76a7237` (PR #263) | +| **LF-91** SlaPolicy + SlaPriority | ✅ DONE | `e70f944` (PR #263) | +| **LF-92** TenantId + TenantScope | ✅ DONE | `e70f944` (PR #263) | + +--- + +## Status summary by stage + +| Stage | Done | In-PR | Queued | Deferred | Future | Total | +|---|---|---|---|---|---|---| +| Tier 1 (LF-1..8) | 8 | 0 | 0 | 0 | 0 | 8 | +| Stage 1 — Data Integration | 0 | 0 | 5 | 0 | 0 | 5 | +| Stage 2 — Ontology | 2 | 0 | 2 | 0 | 0 | 4 | +| Stage 3 — Storage v2 | 0 | 0 | 3 | 1 | 0 | 4 | +| Stage 4 — Search | 0 | 0 | 3 | 0 | 0 | 3 | +| Stage 5 — Models | 0 | 0 | 3 | 1 | 0 | 4 | +| Stage 6 — Decisions | 0 | 0 | 3 | 0 | 0 | 3 | +| Stage 7 — Scenarios | 0 | 2 | 3 | 0 | 0 | 5 (LF-70/72 IN-PR; LF-71 redesigned; LF-73/74/75 new) | +| Stage 8 — Marketplace | 0 | 0 | 0 | 0 | 2 | 2 | +| Cross-cutting (LF-90..92) | 3 | 0 | 0 | 0 | 0 | 3 | +| Wishlist (W-1..4) | 4 | 0 | 0 | 0 | 0 | 4 | +| **TOTAL** | **17** | **2** | **22** | **2** | **2** | **45** | + +**38% shipped (17/45)**, **44% queued with REQUEST**, **18% deferred or future**. + +--- + +## Sequencing — what to ship next + +Ordered by leverage (unblocks the most downstream work first): + +1. **Merge `claude/scenario-world-facade` PR** — closes Stage 7 (LF-70/72); ships rationale agent; LF-73/74/75 placeholders documented. + +2. **LF-12 Pipeline DAG** (Stage 1 keystone) — adds `UnifiedStep.depends_on: Vec` field + a topological executor in `lance-graph-planner`. Once shipped, LF-10/11/13/14 all become straightforward additions. + +3. **LF-50 + LF-52 ModelRegistry + LlmProvider** (Stage 5) — small (S effort each), unlocks generic model dispatch for xAI gRPC + OpenAI + Anthropic + Ollama through one trait. + +4. **LF-20 FunctionSpec** (Stage 2) — pure DTO addition to `contract::ontology`. Foundry Functions equivalent. Useful for SMB defining German tax-rule transforms. + +5. **LF-23 NotificationSpec** (Stage 2) — small. Reuses `ActionTrigger`. + +6. **LF-31 scan_as_of** (Stage 3) — almost free; just exposes the existing `VersionedGraph::at_version` through the `EntityStore` trait. + +7. **LF-10 Connector registry** (Stage 1) — establishes the connector tier; LF-11 (Postgres) follows immediately. + +8. **LF-61 NARS-revision-on-correction** (Stage 6) — wires existing `NarsInference::Revision` slot. Closes the human-in-the-loop loop. + +9. **LF-73 simulate_forward** (Stage 7) — wires `CounterfactualSynthesis` inference into the ScenarioBranch facade. + +10. **LF-40 Tantivy full-text** (Stage 4) — large but high SMB value (customer search). + +After this sequence, **Tier 2 is ~85% complete** with only the four +deferred/future chunks (LF-32 sharded, LF-53 UI, LF-80/81 marketplace) +unbuilt — all of which have explicit "wait for trigger" markers. + +--- + +## Cross-references + +- **SMB-side mirror:** `smb-office-rs::docs/foundry-parity-checklist.md` (pulled via direct git protocol to `/tmp/sources/smb-office-rs/`). +- **Bus log of every shipped chunk:** `.claude/board/CROSS_SESSION_BROADCAST.md` (claude/blackboard branch). +- **Architectural rationale (LF-70..75):** `.claude/agents/scenario-world.md`. +- **Ripple model (gestalt theory):** `.claude/knowledge/user-agent-topic-ripple-model.md`. +- **VSA-as-Layer-2-catalogue iron rule:** `CLAUDE.md` § I-VSA-IDENTITIES. +- **Lance-native versioning (the substrate this all sits on):** `crates/lance-graph/src/graph/versioned.rs`. + +--- + +## Open questions + +1. **LF-12 split.** Schema field add + executor + cron-trigger as one PR, or three? Recommend: **one PR for schema + executor**, separate PR for cron (LF-13 territory). +2. **LF-32 trigger threshold.** What table size triggers "now we need sharded writes"? Recommend documenting "≥10 GB sustained per-tenant" as the empirical threshold; revisit when first tenant approaches it. +3. **LF-53 ownership.** Out-of-repo for sure, but does Q2 UI plan to consume `contract::scenario::ScenarioBranch` directly, or via a REST shim in `lance-graph-callcenter`? Recommend REST shim — UI shouldn't depend on the contract crate types. +4. **LF-80/81 trigger.** Which regulated-industry tenant first signs (BaFin? GoBD? DATEV?) determines the bundle signature scheme. Defer until concrete. + diff --git a/crates/lance-graph-archetype/src/error.rs b/crates/lance-graph-archetype/src/error.rs index 815515ed..ed0bd79a 100644 --- a/crates/lance-graph-archetype/src/error.rs +++ b/crates/lance-graph-archetype/src/error.rs @@ -45,6 +45,22 @@ pub enum ArchetypeError { /// Non-goals section. #[error("lance I/O error: {0}")] LanceIo(String), + + /// Branch name supplied to `World::fork` is empty or otherwise + /// not a valid branch identifier. + #[error("invalid branch name (must be non-empty)")] + InvalidBranch, + + /// `World::at_tick` was asked for a tick beyond the current + /// observation. Time-travel is read-only — you cannot fast-forward + /// past `current_tick()`. + #[error("invalid tick: requested {requested} > current {current}")] + InvalidTick { + /// The tick that was requested. + requested: u64, + /// The current tick at the time of the request. + current: u64, + }, } #[cfg(test)] diff --git a/crates/lance-graph-archetype/src/world.rs b/crates/lance-graph-archetype/src/world.rs index 54b5a4c8..c5c0a7c9 100644 --- a/crates/lance-graph-archetype/src/world.rs +++ b/crates/lance-graph-archetype/src/world.rs @@ -64,24 +64,51 @@ impl World { } /// Fork this world onto a new dataset branch. Per ADR-0001 §61-72, - /// this will call `lance::checkout(branch)` and return a fresh - /// `World` pinned to the branch HEAD. + /// the substrate call is `lance::checkout(branch)` followed by + /// writing to a new dataset path. /// - /// **Not implemented yet.** DU-2.8 will wire the Lance call; today - /// returns `ArchetypeError::Unimplemented { method: "World::fork" }`. - pub fn fork(&self, _branch: &str) -> Result { - Err(ArchetypeError::Unimplemented { method: "World::fork" }) + /// This crate intentionally does NOT depend on `lance` directly + /// (would force every archetype consumer to pull arrow + lance + + /// datafusion). Instead, we return a descriptor of the fork + /// (new dataset URI = parent URI + `?branch=`) plus a + /// reset tick counter. The downstream consumer (typically + /// `lance-graph-cognitive::world::ScenarioWorldImpl`) is + /// responsible for invoking `VersionedGraph::tag_version(name, ...)` + /// against Lance to materialize the branch. + /// + /// The naming convention `?branch=` is opaque to this + /// crate and only meaningful to the downstream resolver. This keeps + /// the archetype crate a thin meta-state carrier per ADR-0001. + pub fn fork(&self, branch: &str) -> Result { + if branch.is_empty() { + return Err(ArchetypeError::InvalidBranch); + } + let separator = if self.dataset_uri.contains('?') { "&" } else { "?" }; + Ok(World { + tick: 0, + dataset_uri: format!("{}{}branch={}", self.dataset_uri, separator, branch), + }) } /// Rewind (or fast-forward) this world to a specific tick. Per - /// ADR-0001 §95, this will pin the Lance dataset version that - /// corresponds to `tick`. + /// ADR-0001 §95, the substrate call is + /// `Dataset::checkout_version(tick)`. /// - /// **Not implemented yet.** DU-2.8 will wire the dataset-version - /// lookup; today returns - /// `ArchetypeError::Unimplemented { method: "World::at_tick" }`. - pub fn at_tick(&self, _tick: u64) -> Result { - Err(ArchetypeError::Unimplemented { method: "World::at_tick" }) + /// Same dependency-decoupling argument as `fork`: this crate stays + /// lance-free. Returns a new `World` with the tick set; the + /// downstream resolver translates `tick → Lance version` via + /// `VersionedGraph::at_version`. + /// + /// Returns `InvalidTick` if `tick > self.tick` (cannot fast-forward + /// past the current observation). + pub fn at_tick(&self, tick: u64) -> Result { + if tick > self.tick { + return Err(ArchetypeError::InvalidTick { requested: tick, current: self.tick }); + } + Ok(World { + tick, + dataset_uri: self.dataset_uri.clone(), + }) } } @@ -105,26 +132,61 @@ mod tests { } #[test] - fn fork_returns_unimplemented() { + fn fork_appends_branch_query() { let w = World::new("lance://tmp/archetype"); - let err = w.fork("experiment").unwrap_err(); - match err { - ArchetypeError::Unimplemented { method } => { - assert_eq!(method, "World::fork"); - } - other => panic!("expected Unimplemented, got {other:?}"), - } + let forked = w.fork("experiment").expect("fork should succeed"); + assert_eq!(forked.dataset_uri(), "lance://tmp/archetype?branch=experiment"); + assert_eq!(forked.current_tick(), 0); + } + + #[test] + fn fork_uses_ampersand_when_query_already_present() { + let w = World::new("lance://tmp/archetype?tenant=acme"); + let forked = w.fork("scenario_a").expect("fork should succeed"); + assert_eq!( + forked.dataset_uri(), + "lance://tmp/archetype?tenant=acme&branch=scenario_a" + ); + } + + #[test] + fn fork_rejects_empty_branch_name() { + let w = World::new("lance://tmp/archetype"); + let err = w.fork("").unwrap_err(); + assert!(matches!(err, ArchetypeError::InvalidBranch)); + } + + #[test] + fn at_tick_rewinds_within_range() { + let mut w = World::new("lance://tmp/archetype"); + w.tick(); + w.tick(); + w.tick(); // tick = 3 + let past = w.at_tick(1).expect("at_tick should succeed"); + assert_eq!(past.current_tick(), 1); + assert_eq!(past.dataset_uri(), "lance://tmp/archetype"); + // Original is untouched. + assert_eq!(w.current_tick(), 3); + } + + #[test] + fn at_tick_at_current_is_identity() { + let mut w = World::new("lance://tmp/archetype"); + w.tick(); + let same = w.at_tick(1).expect("at_tick(current) should succeed"); + assert_eq!(same.current_tick(), 1); } #[test] - fn at_tick_returns_unimplemented() { + fn at_tick_rejects_future() { let w = World::new("lance://tmp/archetype"); let err = w.at_tick(42).unwrap_err(); match err { - ArchetypeError::Unimplemented { method } => { - assert_eq!(method, "World::at_tick"); + ArchetypeError::InvalidTick { requested, current } => { + assert_eq!(requested, 42); + assert_eq!(current, 0); } - other => panic!("expected Unimplemented, got {other:?}"), + other => panic!("expected InvalidTick, got {other:?}"), } } } diff --git a/crates/lance-graph-contract/src/lib.rs b/crates/lance-graph-contract/src/lib.rs index 345ef9bc..71a13d6c 100644 --- a/crates/lance-graph-contract/src/lib.rs +++ b/crates/lance-graph-contract/src/lib.rs @@ -65,3 +65,4 @@ pub mod property; pub mod ontology; pub mod sla; pub mod auth; +pub mod scenario; diff --git a/crates/lance-graph-contract/src/scenario.rs b/crates/lance-graph-contract/src/scenario.rs new file mode 100644 index 00000000..2adee3fc --- /dev/null +++ b/crates/lance-graph-contract/src/scenario.rs @@ -0,0 +1,400 @@ +//! Scenario branching — explicit counterfactual futures. +//! +//! `ScenarioBranch` is the named, first-class handle for divergent futures +//! over the same parent state. Lance dataset versioning gives us +//! **read-as-of** time travel; this module gives us **write-divergent** +//! branching with the gestalt diff and reproducibility semantics that +//! counterfactual simulation requires. +//! +//! # Architectural decision: why a thin facade, not a column or a new crate +//! +//! Earlier proposals considered (and rejected) two alternatives: +//! +//! 1. **`scenario_id` column on every BindSpace SoA + SPO row** (LF-71 v1). +//! Rejected: widens every SIMD sweep by 8 bytes × N rows, duplicates +//! Lance's native versioning, conflicts with the `I-VSA-IDENTITIES` +//! iron rule. Scenario identity is *meta about which content version*, +//! not content itself. +//! 2. **A new `lance-graph-scenario` crate.** Rejected: the four pieces +//! that a scenario needs already exist (Pearl Rung 3 intervention in +//! `lance-graph-cognitive::world::counterfactual`, dataset versioning +//! + diff in `lance-graph::graph::versioned::VersionedGraph`, +//! archetype meta-state in `lance-graph-archetype::world::World`, +//! full situational DTO in `world_model::WorldModelDto`). A new crate +//! would re-state shape; a facade composes existing surfaces. +//! +//! # The four pieces this facade composes +//! +//! | Piece | Where | Role in scenario | +//! |---|---|---| +//! | `Intervention` (Pearl Rung 3) | `lance-graph-cognitive::world::counterfactual` | The "what if X were x'?" math via `bind/unbind` | +//! | `World { dataset_uri, tick }` | `lance-graph-archetype::world` | Named branch handle wrapping Lance dataset path + version | +//! | `VersionedGraph` (tag, at_version, diff) | `lance-graph::graph::versioned` | The actual storage substrate — ACID branching, time travel, diff | +//! | `WorldModelDto` (gestalt, qualia, ripple state) | `contract::world_model` | The situational snapshot a scenario is reasoning about | +//! +//! The contract crate stays zero-dep; this module declares only the +//! types and trait shape. Concrete implementations that touch +//! `VersionedGraph` live downstream in the planner / cognitive crates. +//! +//! # Reproducibility (Apache-Temporal-extracted method) +//! +//! Apache Temporal is the wrong tool for simulation (it's for workflow +//! replay-on-error). One useful idea ports: **deterministic replay** via +//! captured RNG seed at fork point. Every `ScenarioBranch` carries +//! `fork_seed`; replaying the branch with the same seed is guaranteed +//! to yield the same simulation trajectory. +//! +//! # Forecasting (Chronos-extracted method) +//! +//! Chronos itself (time-series-as-tokens via patch quantization) is too +//! primitive for our substrate. The portable idea: **chain palette +//! compose-table lookups** to forecast the next archetype. We already +//! have palette + ComposeTable in `bgz17`; `forecast_palette(branch, depth)` +//! exposes that as the in-cache forecaster (~2ns/step). +//! +//! # Read also +//! +//! - `.claude/knowledge/user-agent-topic-ripple-model.md` — the four-pole +//! (user, agent, topic, angle) framing. `ScenarioBranch` is a spine +//! fork in the ripple field; `diff_branches` IS the gestalt comparison +//! (overlap + mismatch + unresolved tension). +//! - `.claude/knowledge/vsa-switchboard-architecture.md` — why scenario +//! identity belongs in role-bind (catalogue) not column (content). +//! - `.claude/agents/scenario-world.md` — the specialist agent card with +//! the full decision tree and rejected alternatives. + +// ═══════════════════════════════════════════════════════════════════════════ +// SCENARIO BRANCH — named handle for a divergent future +// ═══════════════════════════════════════════════════════════════════════════ + +/// A named, first-class handle for one divergent future over a parent +/// world state. +/// +/// Composes: +/// - A name (human-readable, opaque to the substrate). +/// - A parent identifier (Lance version + tag) that locates the fork point. +/// - An optional archetype prior bundled into the trajectory. +/// - A captured RNG seed for deterministic replay. +/// - A list of interventions (Pearl Rung 3) applied in order. +/// - A default inference mode (typically `CounterfactualSynthesis`). +/// +/// The branch itself does not own the storage. It is a descriptor that +/// downstream code (in `lance-graph-cognitive` or `lance-graph-planner`) +/// uses to drive `VersionedGraph::tag_version` + new dataset path, +/// `multi_intervene`, and forward simulation. +#[derive(Clone, Debug, PartialEq)] +pub struct ScenarioBranch { + /// Human-readable scenario name. e.g. `"recession_2027"`, + /// `"customer_what_if_churn"`. + pub name: String, + + /// Lance dataset version at the fork point. The branch's writes + /// land in a separate dataset path; reads of the parent state + /// resolve via `VersionedGraph::at_version(forked_from)`. + pub forked_from: u64, + + /// Tag name on the parent dataset that pins the fork point. + /// Created via `VersionedGraph::tag_version(name, forked_from)` + /// at fork creation. Survives independently of this struct. + pub parent_tag: String, + + /// Wall-clock timestamp (ms since epoch) at fork creation. + /// Independent of the parent's logical version — useful for + /// human telemetry and audit. + pub forked_at: u64, + + /// Optional archetype prior. Indexes into the existing palette + /// codebook (256 archetypes) or the archetype role-key catalogue + /// (12 archetype families × 12 voice channels = 144 identities). + /// When set, the archetype identity fingerprint bundles into every + /// trajectory in this branch, biasing forward inference. + pub archetype_prior: Option, + + /// Deterministic-replay seed captured at fork point. + /// Ported from Apache Temporal's deterministic-replay semantics: + /// re-running `simulate_forward` with this seed (and the same + /// intervention list) yields the identical trajectory. + pub fork_seed: u64, + + /// Default NARS inference type for forward simulation in this + /// branch. Typically `CounterfactualSynthesis` (the 7th NARS + /// inference type, slot `[9996..10000)` in role_keys). + pub inference_mode: u8, + + /// Pearl Rung 3 interventions applied to the parent state to + /// define this branch's "what if?" hypothesis. Order matters + /// (later interventions operate on already-modified state per + /// `cognitive::world::counterfactual::multi_intervene`). + /// Stored as opaque u64 IDs that resolve to full + /// `Intervention { target, original, counterfactual }` triples + /// in the cognitive crate's intervention registry. + pub interventions: Vec, +} + +impl ScenarioBranch { + /// Construct a new branch descriptor at the given parent version. + /// Does not touch storage — caller is responsible for invoking + /// `VersionedGraph::tag_version` and creating the branch dataset + /// path. + pub fn new( + name: impl Into, + forked_from: u64, + parent_tag: impl Into, + fork_seed: u64, + ) -> Self { + Self { + name: name.into(), + forked_from, + parent_tag: parent_tag.into(), + forked_at: 0, + archetype_prior: None, + fork_seed, + inference_mode: 6, // CounterfactualSynthesis = 6 in NarsInference + interventions: Vec::new(), + } + } + + /// Attach an archetype prior. The archetype's identity fingerprint + /// will bundle into every trajectory in this branch. + pub fn with_archetype(mut self, archetype_index: u8) -> Self { + self.archetype_prior = Some(archetype_index); + self + } + + /// Set the NARS inference type for this branch. Defaults to + /// `CounterfactualSynthesis`. Set to `Deduction` (0) for + /// "extrapolate forward under current beliefs without + /// counterfactual override." + pub fn with_inference_mode(mut self, nars_inference: u8) -> Self { + self.inference_mode = nars_inference; + self + } + + /// Append an intervention ID (resolving to a full + /// `Intervention { target, original, counterfactual }` in the + /// cognitive crate's registry). + pub fn with_intervention(mut self, intervention_id: u64) -> Self { + self.interventions.push(intervention_id); + self + } + + /// Stamp the wall-clock fork timestamp. + pub fn with_timestamp(mut self, ms_since_epoch: u64) -> Self { + self.forked_at = ms_since_epoch; + self + } + + /// Whether this branch carries an archetype prior. + pub fn has_prior(&self) -> bool { + self.archetype_prior.is_some() + } + + /// Whether any interventions are applied. + pub fn has_interventions(&self) -> bool { + !self.interventions.is_empty() + } +} + +// ═══════════════════════════════════════════════════════════════════════════ +// SCENARIO DIFF — the gestalt of two scenarios +// ═══════════════════════════════════════════════════════════════════════════ + +/// Comparison of two `ScenarioBranch`es at three resolutions. +/// +/// Per `.claude/knowledge/user-agent-topic-ripple-model.md`, a good +/// shared gestalt stores both overlap and conflict. This struct +/// captures that across the three layers a scenario lives in: +/// +/// 1. **Graph layer** (`graph_diff_summary`): which entities/edges +/// differ — composes `VersionedGraph::diff` between the two +/// branches' Lance datasets. +/// 2. **Fingerprint layer** (`fingerprint_divergence`): bit-level +/// divergence on the world fingerprint via `worlds_differ` from +/// `cognitive::world::counterfactual`. +/// 3. **Gestalt layer** (`world_model_dissonance`): differences in +/// the `WorldModelDto` snapshots taken at end of simulation — +/// the situational/relational diff (qualia, gestalt state, +/// proprioception axes). +#[derive(Clone, Debug, PartialEq)] +pub struct ScenarioDiff { + /// Branch A name. + pub a_name: String, + /// Branch B name. + pub b_name: String, + + /// Number of entities that exist in B but not A (graph-level). + /// Resolved via `VersionedGraph::diff(a.forked_from, b.forked_from)` + /// then walking forward through each branch's writes. + pub new_entities_in_b: u32, + /// Number of entities that exist in A but not B. + pub new_entities_in_a: u32, + /// Number of entities present in both whose seal bytes differ. + pub modified_entities: u32, + + /// Bit-level fingerprint divergence in `[0.0, 1.0]`. Comes from + /// `cognitive::world::counterfactual::worlds_differ`. + pub fingerprint_divergence: f32, + + /// Aggregate dissonance across `WorldModelDto.field_state.dissonance` + /// at end-of-simulation for both branches. Higher = more + /// gestalt-level disagreement. + pub world_model_dissonance: f32, +} + +impl ScenarioDiff { + /// Whether the branches are essentially convergent — diff signal + /// below the named threshold across all three resolutions. + pub fn is_convergent(&self, threshold: f32) -> bool { + self.fingerprint_divergence < threshold + && self.world_model_dissonance < threshold + && self.new_entities_in_a == 0 + && self.new_entities_in_b == 0 + && self.modified_entities == 0 + } + + /// Net new entities count (sum of both directions). + pub fn total_new_entities(&self) -> u32 { + self.new_entities_in_a + self.new_entities_in_b + } +} + +// ═══════════════════════════════════════════════════════════════════════════ +// SCENARIO WORLD TRAIT — the surface implementations expose +// ═══════════════════════════════════════════════════════════════════════════ + +/// The minimum surface a scenario implementation must expose. +/// +/// Concrete impls live downstream: +/// - `lance-graph-cognitive::world::ScenarioWorldImpl` wires +/// intervention math to `VersionedGraph` and `WorldModelDto`. +/// - SMB-side or other domain-specific impls may layer additional +/// semantics (per-tenant scoping, archetype overlays, etc.). +/// +/// Errors are deliberately stringly-typed in the trait to keep the +/// contract zero-dep; concrete error enums live in implementations. +pub trait ScenarioWorld { + /// Create a new branch from the current parent state. + /// Implementations call `VersionedGraph::tag_version` to pin the + /// fork point, then create a new dataset path for divergent writes. + fn fork( + &self, + name: &str, + parent_version: u64, + archetype_prior: Option, + ) -> Result; + + /// Run N steps of forward simulation in the branch. The engine + /// dispatches via the branch's `inference_mode` (typically + /// `CounterfactualSynthesis`), consulting the archetype prior + /// (if any) and applying any pending interventions. + fn simulate_forward(&self, branch: &ScenarioBranch, steps: u32) + -> Result; + + /// Compose-chain palette forecast (Chronos-extracted method). + /// Returns the palette index sequence the branch is expected to + /// traverse over `depth` steps. O(depth) table lookups, no + /// neural network. + fn forecast_palette(&self, branch: &ScenarioBranch, depth: u32) -> Vec; + + /// Compare two branches at all three resolutions + /// (graph / fingerprint / world-model gestalt). + fn diff_branches(&self, a: &ScenarioBranch, b: &ScenarioBranch) + -> Result; + + /// Replay a branch from its fork point with the captured seed. + /// Apache-Temporal-extracted determinism: same seed + same + /// intervention list = same trajectory, byte-for-byte. + fn replay(&self, branch: &ScenarioBranch) -> Result; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// TESTS +// ═══════════════════════════════════════════════════════════════════════════ + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn branch_construction_carries_metadata() { + let b = ScenarioBranch::new("recession_2027", 42, "epoch_2026_q2", 0xDEAD_BEEF); + assert_eq!(b.name, "recession_2027"); + assert_eq!(b.forked_from, 42); + assert_eq!(b.parent_tag, "epoch_2026_q2"); + assert_eq!(b.fork_seed, 0xDEAD_BEEF); + assert_eq!(b.inference_mode, 6); // CounterfactualSynthesis + assert!(!b.has_prior()); + assert!(!b.has_interventions()); + } + + #[test] + fn builder_methods_compose() { + let b = ScenarioBranch::new("growth_2027", 42, "epoch", 1) + .with_archetype(7) + .with_intervention(100) + .with_intervention(101) + .with_timestamp(1_700_000_000_000); + assert_eq!(b.archetype_prior, Some(7)); + assert_eq!(b.interventions, vec![100, 101]); + assert_eq!(b.forked_at, 1_700_000_000_000); + assert!(b.has_prior()); + assert!(b.has_interventions()); + } + + #[test] + fn diff_convergence_threshold() { + let d = ScenarioDiff { + a_name: "a".into(), + b_name: "b".into(), + new_entities_in_a: 0, + new_entities_in_b: 0, + modified_entities: 0, + fingerprint_divergence: 0.05, + world_model_dissonance: 0.03, + }; + assert!(d.is_convergent(0.1)); + assert!(!d.is_convergent(0.02)); + } + + #[test] + fn diff_total_new_entities() { + let d = ScenarioDiff { + a_name: "a".into(), + b_name: "b".into(), + new_entities_in_a: 5, + new_entities_in_b: 7, + modified_entities: 3, + fingerprint_divergence: 0.0, + world_model_dissonance: 0.0, + }; + assert_eq!(d.total_new_entities(), 12); + } + + #[test] + fn diff_with_modified_entities_is_not_convergent() { + let d = ScenarioDiff { + a_name: "a".into(), + b_name: "b".into(), + new_entities_in_a: 0, + new_entities_in_b: 0, + modified_entities: 3, + fingerprint_divergence: 0.001, + world_model_dissonance: 0.001, + }; + assert!(!d.is_convergent(0.5)); + } + + #[test] + fn default_inference_is_counterfactual_synthesis() { + let b = ScenarioBranch::new("x", 0, "tag", 0); + // 6 = NarsInference::CounterfactualSynthesis ordinal + assert_eq!(b.inference_mode, 6); + } + + #[test] + fn override_inference_mode() { + let b = ScenarioBranch::new("x", 0, "tag", 0) + .with_inference_mode(0); // Deduction + assert_eq!(b.inference_mode, 0); + } +} diff --git a/docs/ScenarioWorldCounterfactual.md b/docs/ScenarioWorldCounterfactual.md new file mode 100644 index 00000000..464c9dcf --- /dev/null +++ b/docs/ScenarioWorldCounterfactual.md @@ -0,0 +1,299 @@ +# Scenario / World / Counterfactual — Cross-cutting Design + +**Status**: shipped (this PR adds the `ScenarioBranch` facade + wires +`archetype::World::fork` / `at_tick`). + +**Audience**: anyone touching scenario branching, counterfactual +reasoning, time travel, or the Foundry-parity LF-70/71/72 chunks. + +**Owner**: `scenario-world` agent (`.claude/agents/scenario-world.md`). + +--- + +## TL;DR + +Three operations are conflated in casual discussion but are +architecturally distinct: + +| Operation | Question | Surface | Cost | +|---|---|---|---| +| **Time travel** | "Read state as it was at tick T" | `VersionedGraph::at_version(v)` / `archetype::World::at_tick(t)` | One Lance checkout | +| **Branching** | "Run divergent future under hypothesis H, name it `recession_2027`" | `contract::scenario::ScenarioBranch` + `ScenarioWorld` trait | New dataset path + tag + simulation | +| **Intervention** | "What if X were x'?" over a single fingerprint | `cognitive::world::counterfactual::intervene` | Pure bind/unbind math, no storage | + +The `ScenarioBranch` facade composes the four pieces that already +existed: + +1. **Pearl Rung 3 intervention math** in + `lance-graph-cognitive::world::counterfactual` (5 tests passing). +2. **Lance dataset versioning + diff** in + `lance-graph::graph::versioned::VersionedGraph` (`at_version`, + `tag_version`, `diff`). +3. **Archetype meta-state branching** in + `lance-graph-archetype::world::World` (URI-encoded `fork(branch)`, + `at_tick(t)` rewind). +4. **Situational gestalt DTO** in + `lance-graph-contract::world_model::WorldModelDto` (gestalt state, + qualia, proprioception). + +The facade is `ScenarioBranch` + `ScenarioDiff` + `ScenarioWorld` +trait in `lance-graph-contract::scenario`. Total addition to the +substrate: ~300 LOC of contract types + ~50 LOC wiring two stub +methods. + +--- + +## Why the column-widening alternative was rejected + +LF-71 v1 proposed adding a `scenario_id` column to every BindSpace +SoA + SPO row. This was rejected on five grounds: + +1. **Hot-path tax**. Widens every SIMD sweep over `FingerprintColumns`, + `QualiaColumn`, `MetaColumn`, `EdgeColumn` by 8 bytes × N rows. + At 1M rows that's 8 MB of column read on every dispatch. + +2. **Substrate duplication**. Lance datasets already provide + ACID-versioned, time-travel-readable snapshots. Re-implementing + branching at row layer is a parallel state machine with the same + semantics. + +3. **`I-VSA-IDENTITIES` violation**. Per CLAUDE.md iron rule, scenario + identity is *meta about which content version*, not content itself. + It belongs on the addressing/version layer, not as a column. + +4. **Archetype unification break**. Archetypes, personas, + thinking-styles unify by being **role catalogues with disjoint slice + allocations** in the bundle, not new SoA columns. A `SCENARIO_KEY` + role-bind in the existing fingerprint slice headroom would carry + scenario identity for free. + +5. **Ripple-model inconsistency**. Per + `.claude/knowledge/user-agent-topic-ripple-model.md`, scenarios are + spine forks in the ripple field. The fork point is metadata; the + field state at the fork point is the substrate. Adding a column + collapses fork metadata into substrate. + +--- + +## Why a separate scenarios crate was rejected + +The four pieces a scenario needs already exist in distinct crates with +clean dependencies. A new crate would re-state shape; a facade composes +existing surfaces. + +- Cross-consumer types (SMB session, future Foundry-parity work) need + zero-dep access — that means the contract crate. +- Concrete `ScenarioWorld` impls live downstream where they can reach + `VersionedGraph` (lance-graph) and `multi_intervene` + (lance-graph-cognitive). + +This is the same split pattern as `OrchestrationBridge` (trait in +contract, impls in callcenter / planner / cypher). + +--- + +## The three resolutions of `ScenarioDiff` + +A scalar divergence collapses gestalt structure. Per the ripple-model +doc, a good shared gestalt stores both overlap and conflict. So +`ScenarioDiff` carries three resolutions: + +```rust +pub struct ScenarioDiff { + // Names + pub a_name: String, + pub b_name: String, + + // Graph layer — what changed structurally? + // Composes VersionedGraph::diff + pub new_entities_in_a: u32, + pub new_entities_in_b: u32, + pub modified_entities: u32, + + // Fingerprint layer — what changed at bit level? + // Composes worlds_differ + pub fingerprint_divergence: f32, + + // Gestalt layer — what changed relationally? + // Aggregates WorldModelDto.field_state.dissonance + pub world_model_dissonance: f32, +} +``` + +Maps onto the ripple-model framings: +- **Graph diff** ↔ spine-trajectory comparison (causal arc accumulation). +- **Fingerprint diff** ↔ ripple-field divergence (interference at bit + resolution). +- **Gestalt diff** ↔ shared-gestalt overlap/mismatch (the user/agent/ + topic/angle four-pole interaction). + +--- + +## Decision summary table + +| Decision | Chosen | Rejected | Reason | +|---|---|---|---| +| Where to encode scenario identity | URI suffix on `archetype::World` + role-bind in trajectory | Column on BindSpace SoA | Hot-path tax + substrate duplication | +| Where to declare facade types | `lance-graph-contract::scenario` (zero-dep) | New `lance-graph-scenario` crate | Composition over re-statement | +| Storage handle ownership | Downstream resolver | Direct `lance::Dataset` in archetype | Keep archetype lance-free | +| Default inference mode | `CounterfactualSynthesis` (NARS=6) | `Deduction` (NARS=0) | Match user intent on a "what if?" call | +| Determinism strategy | Captured `fork_seed: u64` (Apache-Temporal-extracted) | Implicit non-determinism | Counterfactual research requires reproducibility | +| Forecasting primitive | Palette compose-chain (Chronos-extracted) | Embed Chronos | Our palette + ComposeTable already richer | +| Diff resolution | Three layers (graph, fingerprint, gestalt) | Single divergence scalar | Gestalt is multi-resolution by construction | + +--- + +## Tools we considered and what we extracted + +### NARS — already wired + +`NarsInference::CounterfactualSynthesis` is the 7th inference type with +its own role-key slot at `[9996..10000)`. It was previously unused. +`ScenarioBranch` defaults `inference_mode: 6` so forward simulation +under a branch's hypothesis routes through it. + +### Archetype — scenario priors for free + +12 archetype families × 12 voice channels = 144 identity fingerprints, +each at a disjoint slice in the 16K VSA space. A `ScenarioBranch` with +`archetype_prior: Some(7)` bundles archetype 7's identity into every +trajectory, biasing forward inference toward that archetype's typical +patterns. No code in the substrate — pure role-bind composition. + +### ONNX — branch-local model dispatch + +ONNX models (Jina v5, ModernBERT, etc.) already run on the workspace. +`ScenarioWorld::simulate_forward(branch, steps, model)` passes an +optional `ModelBinding` (already in `contract::ontology`) so each +forward step can consult an external model with branch-local inputs. +This is what makes "given recession archetype, what does the +customer-churn model predict?" actually work. + +### Chronos — extracted method only + +Chronos itself (time-series-as-tokens via patch quantization) is too +primitive for our substrate. The portable idea: **chain palette +compose-table lookups** to forecast next archetype. We already have +palette + ComposeTable in `bgz17`. `ScenarioWorld::forecast_palette` +exposes this as the in-cache forecaster (~2ns/step). + +### Apache Temporal — extracted method only + +Apache Temporal is the wrong tool for simulation (it's for workflow +replay-on-error, not counterfactual forecasting). One useful idea +ports: **deterministic replay** via captured RNG seed at fork point. +Every `ScenarioBranch` carries `fork_seed: u64`. `ScenarioWorld::replay` +re-runs with the same seed → identical trajectory. + +--- + +## LF-80/81 reframed: marketplace = portable auditable counterfactuals + +Earlier framing called LF-80 (`OntologyBundle`) and LF-81 (cross-tenant +install) "speculative". With `ScenarioBranch` shipped, that framing +inverts. The Foundry "marketplace" stage isn't package distribution — +it's **portable, signed, auditable scenario packs** that regulated +industries (finance, insurance, supply chain, regulatory compliance) +pay hard for. + +### The product the substrate now affords + +A `ScenarioBundle` is a sealed unit containing: + +| Component | Source | Role in bundle | +|---|---|---| +| `Ontology` | `contract::ontology` | The schema universe the scenario operates over | +| `ScenarioBranch` (one or many) | `contract::scenario` | The hypothesis: fork point + interventions + archetype prior + seed | +| `ModelBinding` set | `contract::ontology::ModelBinding` | The ONNX models invoked per forward step | +| `AuditEntry` chain | `contract::property::AuditEntry` | The provenance record — every intervention, every replay, every diff, signed | +| Evidence stream URLs | `spider-rs` ingest specs | External web data sources grounding forward steps | +| `LineageHandle` set | `contract::property::LineageHandle` | Where each piece of evidence came from + when | + +This composes existing types only; no new substrate. The bundle is the +deliverable; the audit trail is the moat. + +### Why this matters for enterprise + +Regulated industries don't pay for forecasts — they pay for **defensible +forecasts**. A risk officer at a bank presenting "we modeled the +recession-2028 scenario" to regulators must answer: + +- What hypothesis did you make? → `ScenarioBranch.interventions` +- Under what archetype assumptions? → `ScenarioBranch.archetype_prior` +- What evidence grounded the forecast? → `LineageHandle` per evidence + point + `spider-rs` source URLs in the bundle +- What models did you run? → `ModelBinding` set with version pins +- Can you reproduce it? → `ScenarioBranch.fork_seed` + + `ScenarioWorld::replay` +- What changed since the original run? → `ScenarioWorld::diff_branches` + comparing replay against current state + +Every one of these is a substrate primitive that already exists. + +### spider-rs as the evidence ingest tier + +`spider-rs` is a Rust web crawler/scraper. Its role in a counterfactual +bundle: provide reproducible, time-stamped evidence streams that +forward simulation can consult. Cleanly fits as a connector under the +external unified data-layer DTO (LF-10..14 tier) — it's just another +source impl alongside PostgreSQL, MongoDB, MS Graph, etc. + +The integration shape: +1. spider-rs scrapes an evidence URL set per scenario (news, market + data, regulatory filings, supply-chain telemetry). +2. Each scrape → `LineageHandle` capturing source + timestamp + content + hash. +3. Forward simulation consults the evidence via `ModelBinding` (e.g., + sentiment ONNX over scraped news) per step. +4. The full evidence corpus + lineage ships in the `ScenarioBundle` + for replay. + +### Cross-tenant install (LF-81) becomes audit-portable scenarios + +LF-81's "cross-tenant install" reframes as: "Bank A's compliance team +exports a `recession_2028` ScenarioBundle. Bank B's risk team imports +it, runs `replay()` to verify reproducibility, then runs +`simulate_forward()` against their own `Ontology` to get bank-B-specific +projections." + +The cross-tenant operation isn't sharing a package — it's sharing a +**verified hypothesis with full provenance** that Bank B can reproduce +exactly before adapting. + +### Recommendation update + +| Item | Original verdict | New verdict | Reason | +|---|---|---|---| +| LF-80 OntologyBundle | "Speculative" | **High leverage** | Reframes as ScenarioBundle with audit trail — enterprise sells itself | +| LF-81 cross-tenant install | "Speculative" | **High leverage** | Reframes as portable verified hypothesis exchange | +| spider-rs integration | "Connector tier" | **Promote to LF-15 or earlier** | Evidence ingest is the missing piece for groundable forward simulation | + +The shipping order changes: build LF-50/52 (`ModelRegistry`, +`LlmProvider`) → LF-15 spider-rs connector → LF-80 +(`ScenarioBundle` = `Ontology` + `ScenarioBranch` set + bindings + +audit chain) → LF-81 (cross-tenant verify + replay). + +LF-80/81 become the **anchor product**, not a tail item. Everything +else in the Foundry checklist serves the bundle. + +--- + +## Cross-references + +- `crates/lance-graph-contract/src/scenario.rs` — the facade. +- `crates/lance-graph-cognitive/src/world/counterfactual.rs` — Pearl + Rung 3 intervention math. +- `crates/lance-graph/src/graph/versioned.rs` — Lance versioning + + diff. +- `crates/lance-graph-archetype/src/world.rs` — `World::fork` / + `at_tick`. +- `crates/lance-graph-contract/src/world_model.rs` — `WorldModelDto`, + `FieldState`, `GestaltState`. +- `.claude/agents/scenario-world.md` — specialist agent card with the + full decision tree and rejected alternatives. +- `.claude/knowledge/user-agent-topic-ripple-model.md` — theoretical + framing. +- `.claude/knowledge/vsa-switchboard-architecture.md` — why scenario + identity belongs in role-bind, not column. +- ADR-0001 §61-72 — dataset branching design. +- ADR-0001 §95 — tick semantics.