core/chat/verdicts.py
Shay f3cc408f82 feat(adr-0039): audit completeness — TurnVerdicts bundle, stub TurnEvent, hedge_injected
Closes three audit gaps left by the ADR-0035→ADR-0038 pack-layer
surface:

1. TurnVerdicts bundle (chat/verdicts.py) — frozen dataclass
   aggregating identity_score + safety_verdict + ethics_verdict +
   refusal_emitted + hedge_injected.  Attached to both
   ChatResponse.verdicts and TurnEvent.verdicts.  Fields typed as
   object for the same module-coupling reason as
   TurnEvent.safety_verdict.

2. Stub-path TurnEvent emission — _stub_response accepts optional
   tokens kwarg and appends a TurnEvent to turn_log when invoked
   from a real turn.  Audit consumers can now iterate turn_log
   end-to-end without missing stub paths.  Defensive call sites
   (correct() fallback) bypass the append by omitting tokens.

3. refusal_emitted / hedge_injected flags — runtime tracks whether
   it actually mutated the surface this turn.  hedge_injected uses
   idempotent-on-prefix semantics (True iff the runtime ADDED a
   hedge, not iff a hedge happens to be present).

Test-pattern note: previous "gate on rt.turn_log to detect main vs
stub" pattern is now broken; updated to gate on walk_surface ==
_UNKNOWN_DOMAIN_SURFACE.  One existing hedge-injection test gate
updated accordingly.

Back-compat: ADR-0035→0038 per-field accessors
(response.safety_verdict, etc.) still work.  New consumers should
read response.verdicts.

Files:
- chat/verdicts.py (new) — TurnVerdicts dataclass
- chat/runtime.py — _stub_response tokens kwarg + stub TurnEvent
  append + hedge_injected tracking + bundle construction
- core/physics/identity.py — TurnEvent.verdicts: object = None
- tests/test_turn_verdicts_bundle.py (new) — 16 tests
- tests/test_hedge_injection.py — gate fix for stub detection
- docs/decisions/ADR-0039-audit-completeness.md (new)

Verification:
- Combined pack-layer suite: 170 green (was 154 after ADR-0038)
- CLI suites unchanged: smoke 67, runtime 19, cognition 121
- core eval cognition: intent 100%, versor_closure 100% (baseline)
2026-05-17 21:32:46 -07:00

49 lines
1.6 KiB
Python

"""ADR-0039 — unified per-turn verdict bundle.
Three orthogonal verdict surfaces fire on every turn:
* ``IdentityScore`` — geometric alignment against the manifold.
* ``SafetyVerdict`` — universal-floor boundary predicates.
* ``EthicsVerdict`` — deployment-commitment predicates.
Plus two remediation flags captured by the runtime:
* ``refusal_emitted`` — typed refusal replaced the surface this turn.
* ``hedge_injected`` — manifold's hedge phrase was prepended.
The bundle is a convenience aggregate. The individual fields remain
on ``ChatResponse`` and ``TurnEvent`` for back-compat with callers
written against ADR-0035 / ADR-0036; the bundle is the new single
accessor for downstream telemetry and audit consumers.
"""
from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class TurnVerdicts:
"""Single-turn verdict bundle.
Fields are typed as ``object`` to avoid coupling this module's
consumers to the concrete packs.* and core.physics.identity
types at import time. Callers that need the concrete shape
downcast at use site — same discipline ``TurnEvent`` uses.
The two remediation flags are derived from the surface that was
actually emitted:
* ``refusal_emitted=True`` iff the response surface is a typed
refusal (ADR-0036 / ADR-0037).
* ``hedge_injected=True`` iff a hedge phrase was prepended this
turn (ADR-0038).
The two are mutually exclusive: refusal supersedes hedge.
"""
identity_score: object
safety_verdict: object
ethics_verdict: object
refusal_emitted: bool
hedge_injected: bool