From ec62e9866f63f67e111ffef6caa2c5c930dfc80c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 19:07:14 +0000 Subject: [PATCH] D-TEH-1: move bridge_gate to lance-graph-contract; callcenter drops its thinking-engine dependency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First code wave of thinking-engine-harvest-closure-v1 (W1). The zero-dep cross-tenant authorization contract — CognitiveOpKind, CognitiveAuthResult, CognitiveBridgeError, auth_to_result, the CognitiveBridgeGate trait, PassthroughGate, DenyAllGate — moves from thinking_engine::bridge_gate to lance_graph_contract::bridge_gate as one module with identical shapes and its 9 unit tests. thinking-engine keeps a pub-use shim so every old path resolves, and keeps the engine-side pure_ops_dont_touch_gate test next to the lens modules it calls. lance-graph-callcenter re-imports from the contract at both sites and its pure_ops_emit_zero_audit_events test now exercises the crate's own gate-free helpers (prefetch_from_u8 over every depth, auth_to_result over every verdict, the verdict predicates) with the real sink and counter still asserted at zero; the four thinking-engine lens lookups are gone. The path dependency is removed. Falsifier: with the dependency line removed and nothing else changed, callcenter fails at exactly the six crossing sites (E0433 x6). After the move it builds and passes 156/156. Verified: contract 1303/1303; callcenter 156/156 (named test green); clippy -D warnings clean on both crates; fmt clean; cognitive-shader- driver builds default and with-engine; thinking-engine lib builds and the shim test passes; cargo metadata shows no thinking-engine dependency for callcenter; git diff is empty on dto.rs, engine_bridge.rs, cognitive_shader.rs, mailbox_soa.rs and the driver manifest (the ALU artery is untouched). Not done by stop condition: re-pointing the driver's with-engine edge waits on D-TTV-1 (Queued); the engine hook still lives in thinking-engine. Board: LATEST_STATE inventory delta, PR_ARC entry, STATUS_BOARD D-TEH-1 Shipped, closure plan W1 + D-TEH-1 rows, SUPERSESSION-INDEX regenerated. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PFnYKqw6d7TTiB9cT8eFdK --- .claude/board/LATEST_STATE.md | 8 + .claude/board/PR_ARC_INVENTORY.md | 9 + .claude/board/STATUS_BOARD.md | 2 +- .../thinking-engine-harvest-closure-v1.md | 4 +- crates/lance-graph-callcenter/Cargo.toml | 6 +- .../src/cognitive_bridge_gate.rs | 42 ++- crates/lance-graph-callcenter/src/lib.rs | 3 +- .../lance-graph-contract/src/bridge_gate.rs | 354 ++++++++++++++++++ crates/lance-graph-contract/src/lib.rs | 3 + crates/thinking-engine/src/bridge_gate.rs | 351 +---------------- 10 files changed, 425 insertions(+), 357 deletions(-) create mode 100644 crates/lance-graph-contract/src/bridge_gate.rs diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index 015f9ecf7..10a7ed0b1 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -1,3 +1,11 @@ +## 2026-09-02 — branch (D-TEH-1, first code wave after #1138): `bridge_gate` moved to the contract — CONTRACT INVENTORY DELTA + +- ADDED `lance_graph_contract::bridge_gate` — the cross-tenant authorization injection point, moved from `thinking_engine::bridge_gate` with all seven public items together and identical shapes: `CognitiveOpKind` (4 variants, `repr(u8)`), `CognitiveAuthResult { Allow, Deny, Escalate }` + `is_allowed/is_denied`, `CognitiveBridgeError { Denied, Escalation }`, `auth_to_result`, the `CognitiveBridgeGate: Send + Sync` trait (3 sync methods), `PassthroughGate`, `DenyAllGate`. Zero-dep (std only). Its 9 unit tests moved with it; the engine-side `pure_ops_dont_touch_gate` stays in thinking-engine next to the lens modules it calls. +- `thinking_engine::bridge_gate` is now a `pub use lance_graph_contract::bridge_gate::{…}` re-export (migration-wave compatibility; every old path still resolves). +- MEASURED dependency edge, before → after: `lance-graph-callcenter` → `thinking-engine` was a REQUIRED path dep used at 6 sites (2 production imports of 3 symbols, 4 test-only lens lookups); dropping the line alone failed with 6 × E0433. After the move the dep line is gone, `cargo metadata` lists no thinking-engine dependency for callcenter, and the crate builds + 156 tests pass. The remaining production consumer of `thinking-engine` in the workspace is `cognitive-shader-driver`'s OPTIONAL `with-engine` feature (the ALU's engine hook) — untouched, builds green with and without the feature. +- UNCHANGED (verified by empty diff): `thinking-engine/src/dto.rs`, `cognitive-shader-driver/src/engine_bridge.rs`, `contract/src/cognitive_shader.rs`, `mailbox_soa.rs`, driver `Cargo.toml` — the ALU artery and its DTO bus are byte-identical. +- NOT DONE, by stop condition: re-pointing the driver's `with-engine` edge waits on D-TTV-1 (Queued) — today the engine hook still lives in thinking-engine, so there is nothing to re-point it at. + ## 2026-09-02 — branch (D-POP-1 result): PROBE-POP-READOUT-1 — INVENTORY DELTA - ADDED `crates/deepnsm-v2/examples/pop_readout.rs` — the D-POP-1 probe (no library surface, no new type, no tenant). Consumes `Cam96Space::distance`, `basin_self_code`, `partial_spearman` and `lance_graph_contract::exploration::{FrontierEdge, NarsTruth}`; produces a KILL verdict plus three side-findings (plan §6a). diff --git a/.claude/board/PR_ARC_INVENTORY.md b/.claude/board/PR_ARC_INVENTORY.md index a0eecd9ce..5666faa83 100644 --- a/.claude/board/PR_ARC_INVENTORY.md +++ b/.claude/board/PR_ARC_INVENTORY.md @@ -10,6 +10,15 @@ > census §8.3 trap 10: read the body FIRST, then open for write — never > inline both in one expression. +## 2026-09-02 — lance-graph branch `claude/medcare-rs-continue-6nhbxn` (D-TEH-1 PR, after #1138) — `bridge_gate` → contract; callcenter drops its thinking-engine dependency + +- **Added:** `crates/lance-graph-contract/src/bridge_gate.rs` (seven items + 9 tests, moved not re-derived); `pub mod bridge_gate` in the contract lib; `thinking_engine::bridge_gate` as a re-export shim keeping `pure_ops_dont_touch_gate` engine-side. +- **Changed:** callcenter imports (2 sites) → `lance_graph_contract::bridge_gate`; `pure_ops_emit_zero_audit_events` rewritten over the crate's own gate-free helpers (`prefetch_from_u8` all depths, `auth_to_result` all verdicts, predicates) with the real sink + counter still asserted at zero; callcenter `Cargo.toml` dep line removed; three doc comments. +- **Measured:** before = required path dep, 6 crossing sites, dep-drop alone fails 6 × E0433; after = zero thinking-engine deps in callcenter metadata, contract 1303/1303, callcenter 156/156, driver default + `with-engine` green, thinking-engine lib green + shim test 1/1, clippy `-D warnings` clean on both touched crates, fmt clean. +- **Locked:** the ALU artery untouched (empty diff on `dto.rs`, `engine_bridge.rs`, `cognitive_shader.rs`, `mailbox_soa.rs`, driver manifest). +- **Deferred:** step 6 (re-point `with-engine`) — stop condition: D-TTV-1 has not landed, the engine hook is still in thinking-engine. D-TEH-2..5, D-HOUSE-1, D-ARW not started. +- **Confidence:** High. + ## 2026-09-02 — lance-graph branch `claude/medcare-rs-continue-6nhbxn` (rulings PR, after #1137) — closure-plan rulings 1, 2, 4 recorded; ruling 3 re-stated - **Added:** EPIPHANIES `E-JC-IS-THE-HOME-OF-ALL-CALIBRATED-MATH-1`; closure diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 5e7a17830..8681592bd 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -18,7 +18,7 @@ | D-id | deliverable | status | |---|---|---| | D-TEH-0 | census: live footprint measured (one required consumer via `bridge_gate`; one optional via `with-engine`), 51-file fate table, open-row reconciliation, four-wave closure, idea harvest, four rulings asked | **Shipped (plan-only, this PR)** | -| D-TEH-1 | W1: `bridge_gate` trait → `lance-graph-contract`; callcenter drops the path dep; DTO-ladder home ruling recorded (D-TTV-1); thinking-engine becomes a leaf | Queued — first code wave | +| D-TEH-1 | W1: `bridge_gate` (seven items) → `lance_graph_contract::bridge_gate`; callcenter re-imports and drops the path dep; thinking-engine keeps a re-export shim | **Shipped 2026-09-02** (before: required dep, 6 crossing sites, dep-drop alone fails 6 × E0433; after: zero thinking-engine deps in callcenter metadata, contract 1303/1303, callcenter 156/156, driver default + `with-engine` green, ALU artery files byte-identical). `with-engine` re-point deferred by stop condition: D-TTV-1 not landed | | D-TEH-2 | W2: ghost prior → planner `nars/ghost_prior.rs` over `WisdomMarker`, per-thought, two-sided falsifiers; crate `ghosts.rs` deleted | Queued — consumer D-HOUSE-4 | | D-TEH-3 | W2: calibration MATH → `jc` (ruling 4, 2026-09-02: lift if correct, perfect in jc if not; crate copies die); glue stays in the lab crate; `semantic_chunker` / `spiral_segment` decided by their falsifiers | Queued | | D-TEH-4 | W3: ENTROPY M8 engine collapse with dtype parity suite; 5 cascade shapes + 3 lens modules collapse | Queued | diff --git a/.claude/plans/thinking-engine-harvest-closure-v1.md b/.claude/plans/thinking-engine-harvest-closure-v1.md index df73c80b3..411b99f3d 100644 --- a/.claude/plans/thinking-engine-harvest-closure-v1.md +++ b/.claude/plans/thinking-engine-harvest-closure-v1.md @@ -164,7 +164,7 @@ Each wave lands only with its gate green; no wave ports without a consumer. | wave | content | gate | |---|---|---| | **W0 (this PR)** | census, fate table, row regrades, idea harvest (§4) | boards consistent; SUPERSESSION-INDEX regenerated | -| **W1 — cut the hard dependency** | move `bridge_gate` trait + gates to `lance-graph-contract` (trait-only, zero-dep); callcenter re-imports; decide the DTO-ladder home (D-TTV-1 ruling) so the driver's `with-engine` feature can point at it | `lance-graph-callcenter` builds with no thinking-engine path dep; driver `with-engine` still green; thinking-engine becomes a LEAF | +| **W1 — cut the hard dependency** | move `bridge_gate` trait + gates to `lance-graph-contract` (trait-only, zero-dep); callcenter re-imports; the `with-engine` re-point follows D-TTV-1 (the DTO home itself is ruled — §6.3) | **DONE 2026-09-02 (D-TEH-1)** for the hard edge: `lance-graph-callcenter` builds with no thinking-engine path dep; driver `with-engine` still green; thinking-engine is a LEAF for required edges. The `with-engine` re-point stays open until D-TTV-1 lands the engine hook | | **W2 — harvest gems with consumers** | ghost prior → planner `nars/ghost_prior.rs` (D-TEH-2, consumer D-HOUSE-4); the MATH of the calibration battery → `jc` per ruling 4 (`cronbach` compared then lifted-or-perfected; Spearman, re-encode drift, SiLU-correction statistics likewise); `semantic_chunker` → deepnsm-v2 only if its falsifier passes; `spiral_segment` → codec home via certification battery | each port has a two-sided falsifier + a disable run; each source file deleted in the same PR | | **W3 — M8** | one enum-dispatched engine; the 5 cascade shapes and 3 lens modules collapse; parity suite across u8/BF16/i8/f32 | NOT bit-parity across dtypes — u8 / BF16 / i8 / f32 differ in encoding by design, and `dual_engine.rs` exists to MEASURE that disagreement (Codex on #1137). Gate: per-dtype output tolerances plus dtype-invariant ranking/convergence invariants (top-k order, `converged`, `cycle_count` bounds) on real engine fixtures that instantiate all four engines (the driver's fixtures do not — they round-trip `BusDto` only); the pre-collapse `DualResult` disagreement is the baseline the collapsed engine must not exceed; the `branching` spawn shape kept as a mode, not lost | | **W4 — retire and rename** | delete RESIDUE (1e) + retired persona A2A; rename what is left (GLUE only, all math already in jc) `thinking-lab` (calibration feature, `--manifest-path` CI line); regrade every row in §2; pay `TD-THINKING-ENGINE-EXCLUDED-DEBT-1` | the name `thinking-engine` no longer appears in any `Cargo.toml` dependency; board rows closed or re-owned | @@ -205,7 +205,7 @@ harvest and not an amputation. | D-id | title | scope | status | |---|---|---|---| | D-TEH-0 | census + fate table + open-row reconciliation + idea harvest (this plan) | plan + board rows | Shipped (this PR) | -| D-TEH-1 | W1: `bridge_gate` trait → contract; DTO-ladder home ruling recorded; thinking-engine becomes a leaf | contract + callcenter (+ driver feature pointer) | Queued — first code wave | +| D-TEH-1 | W1: `bridge_gate` (seven items) → `lance_graph_contract::bridge_gate`; callcenter re-imports and drops the path dep; thinking-engine keeps a re-export shim | contract + callcenter | **Shipped 2026-09-02** — edge measured before (required dep, 6 crossing sites, dep-drop fails 6 × E0433) and after (zero thinking-engine deps in callcenter metadata; 1303 + 156 tests, driver default + `with-engine` green). The `with-engine` re-point is NOT part of this wave: D-TTV-1 is Queued and the engine hook still lives in thinking-engine, so there is nothing to re-point it at (stop condition honoured). thinking-engine is now a leaf for every REQUIRED edge; the one remaining edge is the ALU's optional engine hook | | D-TEH-2 | W2: ghost prior harvested as planner `nars/ghost_prior.rs` over `WisdomMarker`, per-thought, with two-sided falsifiers; crate `ghosts.rs` deleted | planner | Queued — consumer = D-HOUSE-4 | | D-TEH-3 | W2: calibration MATH → jc (ruling 4: compare, then lift or perfect in jc; crate copies deleted); `semantic_chunker` / `spiral_segment` decided by their falsifiers | jc / deepnsm-v2 / codec home | Queued | | D-TEH-4 | W3: M8 engine collapse with parity suite; cascade shapes and lens modules collapse | thinking-engine → the one engine | Queued (owns ENTROPY M8) | diff --git a/crates/lance-graph-callcenter/Cargo.toml b/crates/lance-graph-callcenter/Cargo.toml index 44bffc992..ca3403e17 100644 --- a/crates/lance-graph-callcenter/Cargo.toml +++ b/crates/lance-graph-callcenter/Cargo.toml @@ -12,10 +12,8 @@ build = "build.rs" lance-graph-contract = { path = "../lance-graph-contract" } # D-SDR-4b audit sinks — always available for the trait + CompositeSink log = "0.4" -# PR-F1 — CognitiveBridgeGate: UnifiedBridgeGate wraps UnifiedBridge and -# implements the trait from thinking-engine. Direction: callcenter → thinking-engine. -# The reverse dep (thinking-engine → callcenter) is forbidden to avoid circular deps. -thinking-engine = { path = "../thinking-engine", default-features = false } +# PR-F1 — CognitiveBridgeGate: UnifiedBridgeGate implements the gate trait from +# lance-graph-contract::bridge_gate (D-TEH-1 moved it there; no thinking-engine dep). # D-CASCADE-V1-3 — bridge collapse: factories project from the canonical SoA. lance-graph-ontology = { path = "../lance-graph-ontology" } # D-SDR-1 (super-domain-rbac-tenancy-v1 §3.9 + §13.1) — UnifiedBridge composes diff --git a/crates/lance-graph-callcenter/src/cognitive_bridge_gate.rs b/crates/lance-graph-callcenter/src/cognitive_bridge_gate.rs index 628f15577..bff2845a8 100644 --- a/crates/lance-graph-callcenter/src/cognitive_bridge_gate.rs +++ b/crates/lance-graph-callcenter/src/cognitive_bridge_gate.rs @@ -1,7 +1,8 @@ //! `UnifiedBridgeGate` — production `CognitiveBridgeGate` impl. //! //! Wraps a `UnifiedBridge` and implements `CognitiveBridgeGate` from -//! `thinking-engine::bridge_gate`. Cross-tenant ops are authorized through +//! `lance_graph_contract::bridge_gate` (moved there from `thinking-engine` +//! by D-TEH-1; the engine re-exports the same items). Cross-tenant ops are authorized through //! the existing `UnifiedBridge::authorize_read` / `authorize_act` paths, //! which already emit `UnifiedAuditEvent` via the `AuditChain` (D-SDR-5). //! @@ -42,10 +43,12 @@ use lance_graph_contract::property::PrefetchDepth; use lance_graph_ontology::bridge::NamespaceBridge; use crate::unified_bridge::UnifiedBridge; -use thinking_engine::bridge_gate::{CognitiveAuthResult, CognitiveBridgeGate, CognitiveOpKind}; +use lance_graph_contract::bridge_gate::{ + CognitiveAuthResult, CognitiveBridgeGate, CognitiveOpKind, +}; // ═══════════════════════════════════════════════════════════════════════════ -// PrefetchDepth helper — avoids exposing lance-graph-contract in thinking-engine +// PrefetchDepth helper — keeps the gate trait's `depth: u8` free of the PrefetchDepth type // ═══════════════════════════════════════════════════════════════════════════ /// Map the `depth: u8` passed through `CognitiveBridgeGate::authorize_retrieval` @@ -194,7 +197,9 @@ mod tests { use crate::super_domain::SuperDomain as SD; use crate::unified_audit::UnifiedAuditEvent; use crate::unified_bridge::{TenantId, UnifiedBridge}; - use thinking_engine::bridge_gate::{CognitiveAuthResult, CognitiveBridgeGate, CognitiveOpKind}; + use lance_graph_contract::bridge_gate::{ + CognitiveAuthResult, CognitiveBridgeGate, CognitiveOpKind, + }; // ── StubBridge (mirrors unified_bridge tests) ──────────────────────────── @@ -454,9 +459,17 @@ mod tests { assert_eq!(gate.chinese_wall_deny_count(), 1); } - /// Pure cognitive ops (encode, qualia compute, distance lookup) do NOT call - /// the gate. Verified by the absence of any chinese_wall increment and the - /// zero-overhead passthrough in thinking-engine standalone mode. + /// Pure ops do NOT call the gate, so they emit no audit event and no + /// Chinese-wall increment. The pure ops exercised here are this crate's + /// own gate-free helpers (`prefetch_from_u8` over every depth, + /// `auth_to_result` over every verdict, the verdict predicates) — none of + /// them touches `UnifiedBridgeGate`. The engine-side half of the same + /// claim (codebook / distance lookups never reach the gate) lives in + /// `thinking-engine::bridge_gate::tests::pure_ops_dont_touch_gate`, next + /// to the lens modules it calls; D-TEH-1 removed this crate's path + /// dependency on that engine, so its lens functions are not called here. + /// The gate object is constructed so the assertion measures a REAL sink + /// and a REAL counter, not the absence of a gate. #[test] fn pure_ops_emit_zero_audit_events() { let sink = Arc::new(RecordingSink::default()); @@ -470,11 +483,16 @@ mod tests { ); let gate = UnifiedBridgeGate::new(unified); - // Pure codebook lookups — gate never called. - let _ = thinking_engine::jina_lens::jina_lookup(42); - let _ = thinking_engine::jina_lens::jina_distance(0, 1); - let _ = thinking_engine::bge_m3_lens::bge_m3_lookup(100); - let _ = thinking_engine::reranker_lens::reranker_lookup(500); + // Pure helpers — the gate object exists but is never consulted. + use lance_graph_contract::bridge_gate::auth_to_result; + for depth in 0u8..=4 { + let _ = prefetch_from_u8(depth); + } + assert!(auth_to_result(CognitiveAuthResult::Allow).is_ok()); + assert!(auth_to_result(CognitiveAuthResult::Deny).is_err()); + assert!(auth_to_result(CognitiveAuthResult::Escalate).is_err()); + assert!(CognitiveAuthResult::Allow.is_allowed()); + assert!(CognitiveAuthResult::Escalate.is_denied()); assert_eq!(sink.count(), 0, "pure ops must emit zero audit events"); assert_eq!(gate.chinese_wall_deny_count(), 0); diff --git a/crates/lance-graph-callcenter/src/lib.rs b/crates/lance-graph-callcenter/src/lib.rs index 27325bc8c..35e425e95 100644 --- a/crates/lance-graph-callcenter/src/lib.rs +++ b/crates/lance-graph-callcenter/src/lib.rs @@ -213,7 +213,8 @@ pub use savant_reasoners::{ // PR-F1 — UnifiedBridgeGate: production CognitiveBridgeGate impl. // Wraps UnifiedBridge; Chinese-wall check fires before policy evaluation -// on cross-tenant ops (§3.8). No dep on thinking-engine from thinking-engine. +// on cross-tenant ops (§3.8). The gate trait lives in lance-graph-contract +// (D-TEH-1); this crate has no dependency on thinking-engine. pub mod cognitive_bridge_gate; pub use cognitive_bridge_gate::UnifiedBridgeGate; diff --git a/crates/lance-graph-contract/src/bridge_gate.rs b/crates/lance-graph-contract/src/bridge_gate.rs new file mode 100644 index 000000000..48f65d890 --- /dev/null +++ b/crates/lance-graph-contract/src/bridge_gate.rs @@ -0,0 +1,354 @@ +//! `CognitiveBridgeGate` — injection point for cross-tenant authorization +//! in the cognitive pipeline. +//! +//! **Home (D-TEH-1, 2026-09-02):** this module moved here from +//! `thinking-engine::bridge_gate` unchanged in shape — all seven public items +//! (`CognitiveOpKind`, `CognitiveAuthResult`, `CognitiveBridgeError`, +//! `auth_to_result`, `CognitiveBridgeGate`, `PassthroughGate`, `DenyAllGate`) +//! together, per `thinking-engine-harvest-closure-v1` §1c. It is zero-dep +//! (std only), which is why it belongs in the contract: the production impl +//! `UnifiedBridgeGate` in `lance-graph-callcenter` and the standalone +//! engine both consume it, and neither should depend on the other for it. +//! `thinking-engine::bridge_gate` re-exports this module during the +//! migration wave so no import path is left without a home. +//! +//! ## Design rules +//! +//! - **No `lance-graph-callcenter` dependency.** The production impl +//! depends on this crate, never the reverse. +//! - **All methods synchronous.** The cognitive pipeline is not async. +//! - **Zero-cost default.** `PassthroughGate` (the default) unconditionally +//! allows every op with a single branch. No allocations, no locks. +//! +//! ## Gated op categories (per spec §1.2) +//! +//! - **Category A** — Cross-tenant sensor-lens retrieval (jina/bge-m3/reranker). +//! - **Category B** — Persona switch reading shared archetype corpus. +//! - **Category C** — L6 delegation / L8 integration (multi-tenant). +//! +//! Pure ops (encode, qualia compute, l4 learn, spiral geometry, calibration) +//! bypass the gate entirely; the engine-side proof of that +//! (`pure_ops_dont_touch_gate`) stays in `thinking-engine`, next to the pure +//! ops it calls. + +// ═══════════════════════════════════════════════════════════════════════════ +// CognitiveOpKind — taxonomy for Category C ops +// ═══════════════════════════════════════════════════════════════════════════ + +/// Which cognitive operation is requesting authorization (Category C). +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum CognitiveOpKind { + /// L6 fan-out: delegating to multiple lenses scoped to different tenants. + L6Delegation = 1, + /// L8 integration: aggregating cross-tenant evidence. + L8Integration = 2, + /// Direct qualia-vector write crossing a tenant boundary. + QualiaWrite = 3, + /// MetaWord style-bits commit crossing a tenant boundary. + MetaWordCommit = 4, +} + +impl std::fmt::Display for CognitiveOpKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::L6Delegation => write!(f, "L6Delegation"), + Self::L8Integration => write!(f, "L8Integration"), + Self::QualiaWrite => write!(f, "QualiaWrite"), + Self::MetaWordCommit => write!(f, "MetaWordCommit"), + } + } +} + +// ═══════════════════════════════════════════════════════════════════════════ +// CognitiveAuthResult +// ═══════════════════════════════════════════════════════════════════════════ + +/// Authorization decision returned by every `CognitiveBridgeGate` method. +/// +/// - `Allow` — proceed with the cross-tenant op. +/// - `Deny` — abort; caller must surface `CognitiveBridgeError::Denied`. +/// - `Escalate` — a human-approval / MFA step is required before retrying. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CognitiveAuthResult { + Allow, + Deny, + Escalate, +} + +impl CognitiveAuthResult { + /// `true` when the caller may proceed. + #[inline] + pub const fn is_allowed(self) -> bool { + matches!(self, Self::Allow) + } + + /// `true` when the caller must not proceed. + #[inline] + pub const fn is_denied(self) -> bool { + !self.is_allowed() + } +} + +// ═══════════════════════════════════════════════════════════════════════════ +// CognitiveBridgeError — surface to callers +// ═══════════════════════════════════════════════════════════════════════════ + +/// Error returned when a `CognitiveBridgeGate` denies or escalates an op. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CognitiveBridgeError { + /// Gate returned `Deny`. The cross-tenant op was not executed. + Denied, + /// Gate returned `Escalate`. Human approval / MFA is required first. + Escalation, +} + +impl std::fmt::Display for CognitiveBridgeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Denied => write!(f, "cognitive bridge: access denied"), + Self::Escalation => write!(f, "cognitive bridge: escalation required"), + } + } +} + +impl std::error::Error for CognitiveBridgeError {} + +/// Convert a `CognitiveAuthResult` to `Result<(), CognitiveBridgeError>`. +/// +/// Convenience for call sites that want to propagate an error early. +#[inline] +pub fn auth_to_result(result: CognitiveAuthResult) -> Result<(), CognitiveBridgeError> { + match result { + CognitiveAuthResult::Allow => Ok(()), + CognitiveAuthResult::Deny => Err(CognitiveBridgeError::Denied), + CognitiveAuthResult::Escalate => Err(CognitiveBridgeError::Escalation), + } +} + +// ═══════════════════════════════════════════════════════════════════════════ +// CognitiveBridgeGate — the trait +// ═══════════════════════════════════════════════════════════════════════════ + +/// Injection point for cross-tenant authorization in the cognitive pipeline. +/// +/// **Production impl:** `UnifiedBridgeGate` in `lance-graph-callcenter`. +/// **Default impl:** [`PassthroughGate`] — unconditionally allows everything. +/// +/// All methods are synchronous. Implementations must be `Send + Sync` so +/// they can be shared via `Arc` across threads. +pub trait CognitiveBridgeGate: Send + Sync { + /// Category A — cross-tenant sensor-lens retrieval. + /// + /// Called before any ANN / codebook lookup that queries a shared embedding + /// index (jina / bge-m3 / reranker). `entity_type` is a human-readable + /// label ("Document", "Persona", etc.); `depth` maps to `PrefetchDepth` + /// on the callcenter side. + fn authorize_retrieval( + &self, + tenant_id: u32, + entity_type: &str, + depth: u8, + ) -> CognitiveAuthResult; + + /// Category B — persona switch touching a shared archetype corpus. + /// + /// `mode` is the `PersonaMode` ordinal (avoids coupling to the enum): + /// `0 = Work`, `1 = Personal`, `2 = Hybrid`. Called before the mode is + /// committed; returning `Deny` / `Escalate` leaves the persona unchanged. + fn authorize_persona_switch(&self, tenant_id: u32, mode: u8) -> CognitiveAuthResult; + + /// Category C — L6 fan-out or L8 integration across tenant boundaries. + fn authorize_cognitive_op( + &self, + tenant_id: u32, + op_kind: CognitiveOpKind, + ) -> CognitiveAuthResult; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// PassthroughGate — default / standalone impl (NoopGate alias in spec) +// ═══════════════════════════════════════════════════════════════════════════ + +/// Default gate — unconditionally allows every op. Zero overhead. +/// +/// Used when the engine runs standalone without a callcenter. +/// The `Arc` fields in sensors default to +/// `Arc::new(PassthroughGate)`. +pub struct PassthroughGate; + +impl CognitiveBridgeGate for PassthroughGate { + #[inline] + fn authorize_retrieval( + &self, + _tenant_id: u32, + _entity_type: &str, + _depth: u8, + ) -> CognitiveAuthResult { + CognitiveAuthResult::Allow + } + + #[inline] + fn authorize_persona_switch(&self, _tenant_id: u32, _mode: u8) -> CognitiveAuthResult { + CognitiveAuthResult::Allow + } + + #[inline] + fn authorize_cognitive_op( + &self, + _tenant_id: u32, + _op_kind: CognitiveOpKind, + ) -> CognitiveAuthResult { + CognitiveAuthResult::Allow + } +} + +// ═══════════════════════════════════════════════════════════════════════════ +// DenyAllGate — test helper; denies every op +// ═══════════════════════════════════════════════════════════════════════════ + +/// Gate that denies every op. Useful in tests to verify that gated paths +/// are unreachable when the gate is strict. +pub struct DenyAllGate; + +impl CognitiveBridgeGate for DenyAllGate { + #[inline] + fn authorize_retrieval( + &self, + _tenant_id: u32, + _entity_type: &str, + _depth: u8, + ) -> CognitiveAuthResult { + CognitiveAuthResult::Deny + } + + #[inline] + fn authorize_persona_switch(&self, _tenant_id: u32, _mode: u8) -> CognitiveAuthResult { + CognitiveAuthResult::Deny + } + + #[inline] + fn authorize_cognitive_op( + &self, + _tenant_id: u32, + _op_kind: CognitiveOpKind, + ) -> CognitiveAuthResult { + CognitiveAuthResult::Deny + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Arc; + + #[test] + fn passthrough_allows_all() { + let gate = PassthroughGate; + assert_eq!( + gate.authorize_retrieval(1, "Document", 0), + CognitiveAuthResult::Allow + ); + assert_eq!( + gate.authorize_persona_switch(1, 0), + CognitiveAuthResult::Allow + ); + assert_eq!( + gate.authorize_cognitive_op(1, CognitiveOpKind::L6Delegation), + CognitiveAuthResult::Allow + ); + assert_eq!( + gate.authorize_cognitive_op(1, CognitiveOpKind::L8Integration), + CognitiveAuthResult::Allow + ); + assert_eq!( + gate.authorize_cognitive_op(1, CognitiveOpKind::QualiaWrite), + CognitiveAuthResult::Allow + ); + assert_eq!( + gate.authorize_cognitive_op(1, CognitiveOpKind::MetaWordCommit), + CognitiveAuthResult::Allow + ); + } + + #[test] + fn deny_all_denies_all() { + let gate = DenyAllGate; + assert_eq!( + gate.authorize_retrieval(1, "Document", 0), + CognitiveAuthResult::Deny + ); + assert_eq!( + gate.authorize_persona_switch(1, 2), + CognitiveAuthResult::Deny + ); + assert_eq!( + gate.authorize_cognitive_op(1, CognitiveOpKind::L8Integration), + CognitiveAuthResult::Deny + ); + } + + #[test] + fn auth_to_result_allow() { + assert!(auth_to_result(CognitiveAuthResult::Allow).is_ok()); + } + + #[test] + fn auth_to_result_deny() { + assert_eq!( + auth_to_result(CognitiveAuthResult::Deny), + Err(CognitiveBridgeError::Denied) + ); + } + + #[test] + fn auth_to_result_escalate() { + assert_eq!( + auth_to_result(CognitiveAuthResult::Escalate), + Err(CognitiveBridgeError::Escalation) + ); + } + + #[test] + fn gate_as_arc_dyn() { + let gate: Arc = Arc::new(PassthroughGate); + assert!(gate.authorize_retrieval(42, "Persona", 1).is_allowed()); + } + + #[test] + fn cognitive_op_kind_display() { + assert_eq!(CognitiveOpKind::L6Delegation.to_string(), "L6Delegation"); + assert_eq!(CognitiveOpKind::L8Integration.to_string(), "L8Integration"); + } + + #[test] + fn cognitive_auth_result_predicates() { + assert!(CognitiveAuthResult::Allow.is_allowed()); + assert!(!CognitiveAuthResult::Allow.is_denied()); + assert!(CognitiveAuthResult::Deny.is_denied()); + assert!(CognitiveAuthResult::Escalate.is_denied()); + } + + // ── NoopGate integration: thinking-engine works standalone ─────────────── + + /// Simulate the full gated lookup path with PassthroughGate. Confirms that + /// the default gate never blocks the codebook lookup. + #[test] + fn passthrough_gate_noop_integration() { + let gate: Arc = Arc::new(PassthroughGate); + let tenant_id = 7u32; + + // Category A: retrieval + let result = auth_to_result(gate.authorize_retrieval(tenant_id, "Document", 0)); + assert!(result.is_ok(), "PassthroughGate must allow retrieval"); + + // Category B: persona switch + let result = auth_to_result(gate.authorize_persona_switch(tenant_id, 1)); + assert!(result.is_ok(), "PassthroughGate must allow persona switch"); + + // Category C: cognitive op + let result = + auth_to_result(gate.authorize_cognitive_op(tenant_id, CognitiveOpKind::L6Delegation)); + assert!(result.is_ok(), "PassthroughGate must allow L6 delegation"); + } +} diff --git a/crates/lance-graph-contract/src/lib.rs b/crates/lance-graph-contract/src/lib.rs index 2337fb1a4..63c15091d 100644 --- a/crates/lance-graph-contract/src/lib.rs +++ b/crates/lance-graph-contract/src/lib.rs @@ -53,6 +53,9 @@ pub mod attention_facet; pub mod auth; pub mod awareness_facet; pub mod band_reading; +// D-TEH-1: cross-tenant authorization injection point (moved from thinking-engine, +// seven items, zero-dep). Consumed by lance-graph-callcenter's UnifiedBridgeGate. +pub mod bridge_gate; pub mod callcenter; pub mod cam; pub mod canonical_node; diff --git a/crates/thinking-engine/src/bridge_gate.rs b/crates/thinking-engine/src/bridge_gate.rs index 49466fa4c..b9b708a13 100644 --- a/crates/thinking-engine/src/bridge_gate.rs +++ b/crates/thinking-engine/src/bridge_gate.rs @@ -1,346 +1,23 @@ -//! `CognitiveBridgeGate` — injection point for cross-tenant authorization -//! in the cognitive pipeline. +//! `CognitiveBridgeGate` — re-exported from `lance_graph_contract::bridge_gate`. //! -//! ## Design rules +//! D-TEH-1 (`thinking-engine-harvest-closure-v1` W1, 2026-09-02): the +//! zero-dep gate contract now lives in the contract crate, all seven items +//! together, so `lance-graph-callcenter` no longer needs a path dependency on +//! this crate to implement it. This re-export keeps every existing +//! `thinking_engine::bridge_gate::*` path valid during the migration wave; the +//! gate's own unit tests moved with the items. //! -//! - **No `lance-graph-callcenter` dependency.** This trait lives here so -//! `thinking-engine` remains the lower-level crate. The production impl -//! `UnifiedBridgeGate` lives in `lance-graph-callcenter` and depends on -//! this crate (not the reverse). -//! - **All methods synchronous.** The cognitive pipeline is not async. -//! - **Zero-cost default.** `PassthroughGate` (the default) unconditionally -//! allows every op with a single branch. No allocations, no locks. -//! -//! ## Gated op categories (per spec §1.2) -//! -//! - **Category A** — Cross-tenant sensor-lens retrieval (jina/bge-m3/reranker). -//! - **Category B** — Persona switch reading shared archetype corpus. -//! - **Category C** — L6 delegation / L8 integration (multi-tenant). -//! -//! Pure ops (encode, qualia compute, l4 learn, spiral geometry, calibration) -//! bypass the gate entirely. - -// ═══════════════════════════════════════════════════════════════════════════ -// CognitiveOpKind — taxonomy for Category C ops -// ═══════════════════════════════════════════════════════════════════════════ - -/// Which cognitive operation is requesting authorization (Category C). -#[repr(u8)] -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum CognitiveOpKind { - /// L6 fan-out: delegating to multiple lenses scoped to different tenants. - L6Delegation = 1, - /// L8 integration: aggregating cross-tenant evidence. - L8Integration = 2, - /// Direct qualia-vector write crossing a tenant boundary. - QualiaWrite = 3, - /// MetaWord style-bits commit crossing a tenant boundary. - MetaWordCommit = 4, -} - -impl std::fmt::Display for CognitiveOpKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::L6Delegation => write!(f, "L6Delegation"), - Self::L8Integration => write!(f, "L8Integration"), - Self::QualiaWrite => write!(f, "QualiaWrite"), - Self::MetaWordCommit => write!(f, "MetaWordCommit"), - } - } -} - -// ═══════════════════════════════════════════════════════════════════════════ -// CognitiveAuthResult -// ═══════════════════════════════════════════════════════════════════════════ - -/// Authorization decision returned by every `CognitiveBridgeGate` method. -/// -/// - `Allow` — proceed with the cross-tenant op. -/// - `Deny` — abort; caller must surface `CognitiveBridgeError::Denied`. -/// - `Escalate` — a human-approval / MFA step is required before retrying. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum CognitiveAuthResult { - Allow, - Deny, - Escalate, -} - -impl CognitiveAuthResult { - /// `true` when the caller may proceed. - #[inline] - pub const fn is_allowed(self) -> bool { - matches!(self, Self::Allow) - } - - /// `true` when the caller must not proceed. - #[inline] - pub const fn is_denied(self) -> bool { - !self.is_allowed() - } -} - -// ═══════════════════════════════════════════════════════════════════════════ -// CognitiveBridgeError — surface to callers -// ═══════════════════════════════════════════════════════════════════════════ - -/// Error returned when a `CognitiveBridgeGate` denies or escalates an op. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum CognitiveBridgeError { - /// Gate returned `Deny`. The cross-tenant op was not executed. - Denied, - /// Gate returned `Escalate`. Human approval / MFA is required first. - Escalation, -} - -impl std::fmt::Display for CognitiveBridgeError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Denied => write!(f, "cognitive bridge: access denied"), - Self::Escalation => write!(f, "cognitive bridge: escalation required"), - } - } -} - -impl std::error::Error for CognitiveBridgeError {} - -/// Convert a `CognitiveAuthResult` to `Result<(), CognitiveBridgeError>`. -/// -/// Convenience for call sites that want to propagate an error early. -#[inline] -pub fn auth_to_result(result: CognitiveAuthResult) -> Result<(), CognitiveBridgeError> { - match result { - CognitiveAuthResult::Allow => Ok(()), - CognitiveAuthResult::Deny => Err(CognitiveBridgeError::Denied), - CognitiveAuthResult::Escalate => Err(CognitiveBridgeError::Escalation), - } -} - -// ═══════════════════════════════════════════════════════════════════════════ -// CognitiveBridgeGate — the trait -// ═══════════════════════════════════════════════════════════════════════════ - -/// Injection point for cross-tenant authorization in the cognitive pipeline. -/// -/// **Production impl:** `UnifiedBridgeGate` in `lance-graph-callcenter`. -/// **Default impl:** [`PassthroughGate`] — unconditionally allows everything. -/// -/// All methods are synchronous. Implementations must be `Send + Sync` so -/// they can be shared via `Arc` across threads. -pub trait CognitiveBridgeGate: Send + Sync { - /// Category A — cross-tenant sensor-lens retrieval. - /// - /// Called before any ANN / codebook lookup that queries a shared embedding - /// index (jina / bge-m3 / reranker). `entity_type` is a human-readable - /// label ("Document", "Persona", etc.); `depth` maps to `PrefetchDepth` - /// on the callcenter side. - fn authorize_retrieval( - &self, - tenant_id: u32, - entity_type: &str, - depth: u8, - ) -> CognitiveAuthResult; - - /// Category B — persona switch touching a shared archetype corpus. - /// - /// `mode` is the `PersonaMode` ordinal (avoids coupling to the enum): - /// `0 = Work`, `1 = Personal`, `2 = Hybrid`. Called before the mode is - /// committed; returning `Deny` / `Escalate` leaves the persona unchanged. - fn authorize_persona_switch(&self, tenant_id: u32, mode: u8) -> CognitiveAuthResult; - - /// Category C — L6 fan-out or L8 integration across tenant boundaries. - fn authorize_cognitive_op( - &self, - tenant_id: u32, - op_kind: CognitiveOpKind, - ) -> CognitiveAuthResult; -} - -// ═══════════════════════════════════════════════════════════════════════════ -// PassthroughGate — default / standalone impl (NoopGate alias in spec) -// ═══════════════════════════════════════════════════════════════════════════ - -/// Default gate — unconditionally allows every op. Zero overhead. -/// -/// Used when `thinking-engine` runs standalone without a callcenter. -/// The `Arc` fields in sensors default to -/// `Arc::new(PassthroughGate)`. -pub struct PassthroughGate; - -impl CognitiveBridgeGate for PassthroughGate { - #[inline] - fn authorize_retrieval( - &self, - _tenant_id: u32, - _entity_type: &str, - _depth: u8, - ) -> CognitiveAuthResult { - CognitiveAuthResult::Allow - } - - #[inline] - fn authorize_persona_switch(&self, _tenant_id: u32, _mode: u8) -> CognitiveAuthResult { - CognitiveAuthResult::Allow - } - - #[inline] - fn authorize_cognitive_op( - &self, - _tenant_id: u32, - _op_kind: CognitiveOpKind, - ) -> CognitiveAuthResult { - CognitiveAuthResult::Allow - } -} +//! What stays here is the engine-side proof that PURE ops (codebook lookup, +//! distance) never touch the gate — it calls this crate's lens modules, so it +//! belongs next to them, not in the contract. -// ═══════════════════════════════════════════════════════════════════════════ -// DenyAllGate — test helper; denies every op -// ═══════════════════════════════════════════════════════════════════════════ - -/// Gate that denies every op. Useful in tests to verify that gated paths -/// are unreachable when the gate is strict. -pub struct DenyAllGate; - -impl CognitiveBridgeGate for DenyAllGate { - #[inline] - fn authorize_retrieval( - &self, - _tenant_id: u32, - _entity_type: &str, - _depth: u8, - ) -> CognitiveAuthResult { - CognitiveAuthResult::Deny - } - - #[inline] - fn authorize_persona_switch(&self, _tenant_id: u32, _mode: u8) -> CognitiveAuthResult { - CognitiveAuthResult::Deny - } - - #[inline] - fn authorize_cognitive_op( - &self, - _tenant_id: u32, - _op_kind: CognitiveOpKind, - ) -> CognitiveAuthResult { - CognitiveAuthResult::Deny - } -} +pub use lance_graph_contract::bridge_gate::{ + auth_to_result, CognitiveAuthResult, CognitiveBridgeError, CognitiveBridgeGate, + CognitiveOpKind, DenyAllGate, PassthroughGate, +}; #[cfg(test)] mod tests { - use super::*; - use std::sync::Arc; - - #[test] - fn passthrough_allows_all() { - let gate = PassthroughGate; - assert_eq!( - gate.authorize_retrieval(1, "Document", 0), - CognitiveAuthResult::Allow - ); - assert_eq!( - gate.authorize_persona_switch(1, 0), - CognitiveAuthResult::Allow - ); - assert_eq!( - gate.authorize_cognitive_op(1, CognitiveOpKind::L6Delegation), - CognitiveAuthResult::Allow - ); - assert_eq!( - gate.authorize_cognitive_op(1, CognitiveOpKind::L8Integration), - CognitiveAuthResult::Allow - ); - assert_eq!( - gate.authorize_cognitive_op(1, CognitiveOpKind::QualiaWrite), - CognitiveAuthResult::Allow - ); - assert_eq!( - gate.authorize_cognitive_op(1, CognitiveOpKind::MetaWordCommit), - CognitiveAuthResult::Allow - ); - } - - #[test] - fn deny_all_denies_all() { - let gate = DenyAllGate; - assert_eq!( - gate.authorize_retrieval(1, "Document", 0), - CognitiveAuthResult::Deny - ); - assert_eq!( - gate.authorize_persona_switch(1, 2), - CognitiveAuthResult::Deny - ); - assert_eq!( - gate.authorize_cognitive_op(1, CognitiveOpKind::L8Integration), - CognitiveAuthResult::Deny - ); - } - - #[test] - fn auth_to_result_allow() { - assert!(auth_to_result(CognitiveAuthResult::Allow).is_ok()); - } - - #[test] - fn auth_to_result_deny() { - assert_eq!( - auth_to_result(CognitiveAuthResult::Deny), - Err(CognitiveBridgeError::Denied) - ); - } - - #[test] - fn auth_to_result_escalate() { - assert_eq!( - auth_to_result(CognitiveAuthResult::Escalate), - Err(CognitiveBridgeError::Escalation) - ); - } - - #[test] - fn gate_as_arc_dyn() { - let gate: Arc = Arc::new(PassthroughGate); - assert!(gate.authorize_retrieval(42, "Persona", 1).is_allowed()); - } - - #[test] - fn cognitive_op_kind_display() { - assert_eq!(CognitiveOpKind::L6Delegation.to_string(), "L6Delegation"); - assert_eq!(CognitiveOpKind::L8Integration.to_string(), "L8Integration"); - } - - #[test] - fn cognitive_auth_result_predicates() { - assert!(CognitiveAuthResult::Allow.is_allowed()); - assert!(!CognitiveAuthResult::Allow.is_denied()); - assert!(CognitiveAuthResult::Deny.is_denied()); - assert!(CognitiveAuthResult::Escalate.is_denied()); - } - - // ── NoopGate integration: thinking-engine works standalone ─────────────── - - /// Simulate the full gated lookup path with PassthroughGate. Confirms that - /// the default gate never blocks the codebook lookup. - #[test] - fn passthrough_gate_noop_integration() { - let gate: Arc = Arc::new(PassthroughGate); - let tenant_id = 7u32; - - // Category A: retrieval - let result = auth_to_result(gate.authorize_retrieval(tenant_id, "Document", 0)); - assert!(result.is_ok(), "PassthroughGate must allow retrieval"); - - // Category B: persona switch - let result = auth_to_result(gate.authorize_persona_switch(tenant_id, 1)); - assert!(result.is_ok(), "PassthroughGate must allow persona switch"); - - // Category C: cognitive op - let result = - auth_to_result(gate.authorize_cognitive_op(tenant_id, CognitiveOpKind::L6Delegation)); - assert!(result.is_ok(), "PassthroughGate must allow L6 delegation"); - } - /// Pure-op test: confirm that pure ops (encode, distance lookup) are /// NOT routed through the gate. This is a documentation/design test — /// there is no gate call in those paths, so they succeed unconditionally.