core/core/cognition/leeway.py
Shay 471131cabf feat(b4): engine-side leeway producer — populate LeewayEvidence (Wave M B4)
Clears the long-standing B4 block: the leeway decision was already made on the
serving path (chat/runtime.py::_surface_estimate has a real LicenseDecision +
the ReachPolicy) and then DISCARDED — never threaded to the result, and the
workbench can't import reliability_gate. This wires it through.

Producer (observational, never authorizing):
  - core/cognition/leeway.py: LeewayRecord + build_leeway_record(reach_level,
    license_decision) — duck-typed on the decision, zero new cross-package
    coupling. Maps to: no decision -> "unknown" (STRICT, no latitude); denied
    -> "blocked" (gate consulted, said no); licensed SERVE/PROPOSE widening ->
    the real class / theta / "[approximate]" disclosure. "verified" is never
    emitted (RESERVED state). source_digest content-addresses the decision
    (deterministic, no wall-clock).
  - core/cognition/result.py: additive `leeway: LeewayRecord | None = None` on
    CognitiveTurnResult.
  - core/cognition/pipeline.py: build it at result construction from the data
    the runtime ALREADY exposes (response.reach_level +
    runtime.last_turn_accrual().license). chat/runtime.py is UNTOUCHED.
  - workbench/api.py: _leeway_evidence_from_result maps result.leeway ->
    LeewayEvidence (pure projection; no reliability_gate import — firewall
    intact). The journal already persists it; the B4a UI (Replay / Proposals /
    RightInspector) already renders it — no frontend or schema change needed.

Safety (this touches the serving path, so proven, not asserted):
  - trace_hash is a NAMED-field hash (core/cognition/trace.py); `leeway` is not
    in it -> byte-identical serving. All provenance/trace tests pass.
  - response_governance STRICT stays byte-identical (375 governance/serving/
    provenance tests green, incl. the live-wiring + estimation-lane + ADR-0206
    seam tests).
  - core eval cognition: 13 cases, 100% intent / groundedness / versor closure.
  - replay determinism holds (leeway is in CRITICAL_FIELDS; deterministic).

Tests: engine (build_leeway_record: strict/blocked/SERVE/PROPOSE, no "verified",
deterministic digest) + workbench mapping (field-for-field, honest absence,
invalid-enum clamp) + integration (a real turn now carries an honest leeway
record, not the null "No evidence recorded"). 151 workbench/leeway Python + 68
frontend (leeway/replay/proposals/schemaDrift) green; schema-snapshot unchanged.

Docs: gate cleared (b4-leeway-feasibility-gate.md), residue ledger flipped to
implemented, scope brief is b4-leeway-producer-scope-2026-06-13.md.
2026-06-13 20:22:39 -07:00

107 lines
4.2 KiB
Python

"""Per-turn leeway record — the engine-side B4 producer.
When a turn is governed (ADR-0206 ``response_governance``), the reliability gate
either grants latitude to a class (a licensed ``Action.SERVE`` converse-guess,
surfaced as a DISCLOSED ``[approximate]`` estimate) or withholds it (the STRICT
default). That decision is computed on the serving path and was previously
discarded. This module turns the decision the runtime ALREADY made into a small
observational record on the turn result — it never calls the gate, never
authorizes anything, and never alters the served surface.
The record mirrors the workbench ``LeewayEvidence`` tuple field-for-field, so the
workbench maps it with a trivial projection and gains no serving-path import.
The ``license_decision`` is read duck-typed (a
``core.reliability_gate.LicenseDecision`` when present) so this stays a pure
cognition-layer leaf with no new cross-package coupling.
"""
from __future__ import annotations
import hashlib
import json
from dataclasses import dataclass
from typing import Any
@dataclass(frozen=True, slots=True)
class LeewayRecord:
"""Observational record of the leeway the gate granted (or withheld)."""
class_name: str
license: str # "SERVE" | "PROPOSE" | "blocked" | "unknown"
theta: float | None
claim_disclosure: str # "approximate" | "verified" | "proposal_only" | "none"
source_digest: str | None
calibration_evidence_ref: str | None
def _decision_digest(license_decision: Any) -> str:
"""Content-address the licensing decision — provenance, deterministic.
Hashes only the decision's deterministic fields (never a timestamp), so a
replayed turn produces the same digest.
"""
payload = {
"class_name": str(getattr(license_decision, "class_name", "")),
"action": str(getattr(getattr(license_decision, "action", None), "name", "")),
"checker": str(getattr(license_decision, "checker", "")),
"measured": float(getattr(license_decision, "measured", 0.0) or 0.0),
"required": float(getattr(license_decision, "required", 0.0) or 0.0),
"licensed": bool(getattr(license_decision, "licensed", False)),
}
canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"))
return "sha256:" + hashlib.sha256(canonical.encode("utf-8")).hexdigest()
def build_leeway_record(
*,
reach_level: str,
license_decision: Any | None,
) -> LeewayRecord:
"""Map the governed turn's reach level + license decision to a record.
- No decision consulted (the STRICT main path): ``license="unknown"``, no
latitude.
- A decision exists but was denied: ``license="blocked"`` — the gate was
consulted and said no.
- A licensed SERVE/PROPOSE that widened: the real class, θ, and the
``[approximate]`` disclosure — the "engine earned the right to guess" case.
``claim_disclosure`` reflects what was actually disclosed this turn
(``approximate`` iff the surface widened); ``"verified"`` is intentionally
never emitted (it is a RESERVED epistemic state — claiming it would
over-state).
"""
disclosure = "approximate" if reach_level == "approximate" else "none"
if license_decision is None:
return LeewayRecord(
class_name="none",
license="unknown",
theta=None,
claim_disclosure=disclosure,
source_digest=None,
calibration_evidence_ref=None,
)
action_name = str(getattr(getattr(license_decision, "action", None), "name", ""))
licensed = bool(getattr(license_decision, "licensed", False))
if licensed and action_name == "SERVE":
license = "SERVE"
elif licensed and action_name == "PROPOSE":
license = "PROPOSE"
else:
license = "blocked"
class_name = str(getattr(license_decision, "class_name", "") or "none")
required = getattr(license_decision, "required", None)
return LeewayRecord(
class_name=class_name,
license=license,
theta=float(required) if required is not None else None,
claim_disclosure=disclosure,
source_digest=_decision_digest(license_decision),
calibration_evidence_ref=class_name if class_name != "none" else None,
)