docs(workbench): scope B4 leeway producer — engine-side seam + honest mapping

The leeway decision already exists at chat/runtime.py::_surface_estimate
(accrual.license is a real LicenseDecision) but is discarded — never threaded to
CognitiveTurnResult, and the workbench can't import reliability_gate. Brief lays
out: the exact producer seam, the LicenseDecision/ReachPolicy -> LeewayEvidence
mapping, two honest layers (STRICT-now / earned-APPROXIMATE), the firewall-safe
workbench mapping, constraints, a minimal additive Layer-1 first PR, and 4 open
questions.
This commit is contained in:
Shay 2026-06-13 20:01:26 -07:00
parent c41389474d
commit 217d4ed618

View file

@ -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=<class>`) |
## 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=<class>`)? 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.