Commit graph

1 commit

Author SHA1 Message Date
Shay
a0372c951f feat(adr-0036): safety-only typed refusal policy
Runtime-checkable SafetyVerdict violations now replace
ChatResponse.surface (and TurnEvent.surface on the main path) with a
deterministic typed refusal string.  Ethics violations remain
audit-only.

Why safety-only: safety is the universal floor (ADR-0029,
never-swappable, fail-closed).  Ethics is swappable per-deployment;
wiring ethics into refusal would let pack-swappers silently change
refusal behavior via JSON edit.  Wrong coupling.

Why typed refusal (not hedge injection / not re-articulation): typed
refusal is deterministic, audit-detectable by prefix, and preserves
replayability.  Hedge injection would blur surface-preferences-driven
hedging vs predicate-driven refusal.  Re-articulation retry yields the
same surface (planner is deterministic; no refusal-bias hint surface
exists).  Deferred to a future ADR.

Refusal contract:
- ChatResponse.surface = typed refusal string
- walk_surface + articulation_surface = unchanged (audit preserved)
- runtime._last_refusal_was_typed = True (next-turn evidence for
  no_silent_correction)
- Only runtime_checkable=True violations refuse
- Stub path symmetric

Files:
- chat/refusal.py (new) — pure refusal builder + audit helpers
- chat/runtime.py — invoke build_refusal_surface after safety_verdict
- tests/test_safety_refusal.py (new) — 20 tests
- docs/decisions/ADR-0036-safety-refusal-policy.md (new)

Verification:
- 20 new tests; combined pack-layer suite 116 green
- CLI suites unchanged: smoke 67, runtime 19, cognition 121
- core eval cognition: intent 100%, versor_closure 100% (baseline)
2026-05-17 21:10:52 -07:00