diff --git a/docs/workbench/b4-leeway-producer-scope-2026-06-13.md b/docs/workbench/b4-leeway-producer-scope-2026-06-13.md new file mode 100644 index 00000000..f713da92 --- /dev/null +++ b/docs/workbench/b4-leeway-producer-scope-2026-06-13.md @@ -0,0 +1,151 @@ +# B4 leeway producer — engine-side scope (2026-06-13) + +Status: **scoping** (Shay reviews; not yet built). Predecessor: +`b4-leeway-feasibility-gate.md` (the B4a nullable `LeewayEvidence` read model +that renders honest absence today). This brief answers the gate's open question: +*what engine-owned producer populates `LeewayEvidence`, and where.* + +## The finding (verified against source) + +The leeway decision **already exists** in the serving path — it is computed and +then **discarded**, never threaded to the turn result. That, plus the +import-firewall (the workbench may not import `core.reliability_gate` / +`generate.derivation`), is the entire reason B4 is blocked. + +Concretely: + +- **The decision is made at `chat/runtime.py::_surface_estimate`** (≈ line 1057). + At that point the runtime holds: + - `accrual.license` — a real `core.reliability_gate.LicenseDecision` + (`class_name, action, checker, measured, required, ratio, licensed`), + produced by `generate/determine/estimation_license.py::serve_license(predicate)` + (`license_for(tally, Action.SERVE, Ceilings.default())`, or `None` when the + converse-class is absent from the ratified ledger). + - `policy` — a `core.response_governance.ReachPolicy` + (`level, admissible_states, rationale, license_ratio`) from + `govern_response(...)`. + - the disclosed surface (`shape_surface` adds the `[approximate]` prefix). +- **It is discarded.** The only governance residue on the response is + `reach_level=policy.level.value` (a string). `accrual.license` (the class, θ, + ratio) and the disclosure semantics never leave the function. +- **`CognitiveTurnResult` carries no governance/leeway/license field** (grep: + empty). So nothing downstream — including the workbench — can see it. +- **`workbench/api.py::_run_chat_turn` never sets `leeway_evidence`** (grep: + empty) → the workbench `ChatTurnResult.leeway_evidence` defaults to `None` → + the B4a UI shows "No leeway evidence recorded." + +So this is genuinely **engine work**: the producer must live where the +`LicenseDecision` is made (the serving path), attach a plain record to the +result, and let the workbench map it. The workbench cannot reach across the +firewall to compute it. + +## The data is already a near-perfect match + +`LicenseDecision` + `ReachPolicy` map almost one-to-one onto the B4a +`workbench/schemas.py::LeewayEvidence` tuple +(`class_name, license, theta, claim_disclosure, source_digest, calibration_evidence_ref`): + +| `LeewayEvidence` field | Engine source | +|---|---| +| `class_name` | `LicenseDecision.class_name` (the `converse_class_name(predicate)`); `"none"` when no estimate was attempted | +| `license` | `SERVE` if a licensed `Action.SERVE`; `PROPOSE` if a licensed `Action.PROPOSE`; `"blocked"` if a decision exists but `licensed == False`; `"unknown"` if no decision (no ratified tally) | +| `theta` | `LicenseDecision.required` (the θ ceiling — `0.99` for SERVE) | +| `claim_disclosure` | `"approximate"` when `ReachLevel.APPROXIMATE`; `"none"` when `STRICT` (no latitude granted); `"proposal_only"` in PROPOSE contexts; `"verified"` **reserved** (see open Q3) | +| `source_digest` | sha256 of the ratified `ClassTally` bytes the decision read — provenance of the calibration evidence | +| `calibration_evidence_ref` | `class_name`, resolvable to the workbench Calibration subject (`/calibration?inspect=`) | + +## Proposed seam + +1. **Engine (the producer).** Add an engine-owned `@dataclass(frozen=True)` + `LeewayRecord` to `core/cognition/result.py` (NOT the workbench schema — the + engine must not import workbench). Populate it at the `govern_response` seam + in `chat/runtime.py`. It carries the six fields above, derived from the + `LicenseDecision` + `ReachPolicy` the runtime already computes. +2. **Result.** Thread `leeway: LeewayRecord | None` onto `CognitiveTurnResult` + (additive, default `None`), the same way `versor_condition` / `trace_hash` + already ride the result. +3. **Workbench (thin mapping).** `workbench/api.py::_run_chat_turn` maps + `result.leeway` → `workbench/schemas.py::LeewayEvidence` + (`leeway_evidence=_leeway_from_result(result)`). This is a pure projection of + a plain dataclass off the result — **no `reliability_gate` import**, so the + firewall holds. The journal already persists `leeway_evidence`; the B4a UI + already renders it. **No workbench schema or UI change is required** — only + the mapping is wired. + +## Two honest layers + +The "engine earns the right to guess" path (`APPROXIMATE`) is **off by default** +and only fires on a converse-guess estimate whose predicate-class holds a +ratified `SERVE` license. So most served turns are `STRICT`. The producer should +be honest at both levels: + +- **Layer 1 — governance-level (every governed turn, STRICT today).** Emit a + truthful "no latitude granted" record: `level=strict`, `license` ∈ + {`blocked`, `unknown`}, `claim_disclosure=none`, `theta` = the SERVE ceiling + it would have had to clear. This **immediately unblocks** the B4 UI from "No + leeway evidence recorded" to "STRICT governance — no latitude; fully-grounded + commits only," which is itself the impressive discipline (the engine refuses + to widen). Fully additive; byte-identical serving. +- **Layer 2 — earned-leeway (the real B4 story).** When `_surface_estimate` + widens to `APPROXIMATE` because `serve_license(predicate)` returned a licensed + `SERVE`, emit the full record: real `class_name`, `license=SERVE`, + `theta=0.99`, `claim_disclosure=approximate`, the ratio. This is where a + reviewer sees *which class earned latitude, at which θ, with the `[approximate]` + disclosure* — the B4 intent verbatim. + +## Non-negotiable constraints (CLAUDE.md) + +- **Observational, not authorizing.** The producer only *reports* the decision + the runtime already made. It must never call `license_for` itself, mutate + `Ceilings`, or alter the served surface. Ceilings stay human-set. +- **`wrong == 0` untouched.** `STRICT` stays the load-bearing default and + byte-identical; the record is evidence-only. A licensed `APPROXIMATE` estimate + is already a *disclosed* `[approximate]` surface (so a wrong estimate is a + disclosed wrong, not a silent one) — the producer adds no new commit path. +- **Firewall.** Engine emits a plain dataclass; the workbench maps it. The + workbench gains no `reliability_gate` / `generate.derivation` import. +- **Determinism.** `LicenseDecision` is already pure/deterministic; the + `source_digest` must hash the ledger bytes, not a timestamp. + +## Minimal first PR + +Layer 1 only — smallest safe slice that clears the gate: + +1. `LeewayRecord` dataclass on `core/cognition/result.py`; `leeway` field on + `CognitiveTurnResult` (default `None`). +2. Populate it for **every governed turn** at the `chat/runtime.py` seam + (STRICT-honest; the `_surface_estimate` widening path fills the full record). +3. `workbench/api.py` mapping `result.leeway` → `LeewayEvidence`. +4. Tests (non-vacuous): a STRICT turn yields `license=blocked/unknown`, + `claim_disclosure=none`, surface unchanged; a constructed `APPROXIMATE` turn + (licensed-class fixture) yields `license=SERVE`, `claim_disclosure=approximate`; + the record never alters the served surface; `source_digest` reproducible. + +Layer 2's earned path is exercised by the same seam; the only added cost is a +fixture with a ratified SERVE-licensed converse-class to drive `APPROXIMATE`. + +## Open questions for Shay + +1. **First-PR scope:** Layer 1 only (unblocks the UI, fully safe, no behavior + change), or Layer 1 + the Layer 2 earned-path test (needs a SERVE-licensed + fixture)? Recommendation: **ship Layer 1, then Layer 2 as a follow-up** so the + UI unblocks on a zero-behavior-change PR. +2. **Record home:** `LeewayRecord` on `CognitiveTurnResult` (clean for the + workbench seam) vs. a turn-event/telemetry channel? Recommendation: on the + result, symmetric with `versor_condition` / `pipeline_record`. +3. **`claim_disclosure` for STRICT-grounded commits:** `"none"` (no latitude was + needed) vs. `"verified"`. Recommendation: **`"none"`** — `VERIFIED` is a + RESERVED `EpistemicState` (canonical-comparison pass not built), so claiming + `"verified"` leeway would over-state. `level=strict` already carries the + "fully grounded" story. +4. **`calibration_evidence_ref` format:** raw `class_name`, or a resolvable + subject URL (`/calibration?inspect=`)? Recommendation: store the + `class_name`; let the UI build the link (keeps the engine UI-agnostic). + +## Why this finally moves the blocker + +B4a deliberately shipped a *nullable* read model and gated the rest precisely +because the producer is engine-side and the decision wasn't on the result. This +brief shows the decision already exists at a single, well-understood seam, maps +cleanly onto the existing schema, and unblocks with an additive, byte-identical +Layer-1 PR. No new schema, no new UI, no firewall breach.