feat(adr-0037,adr-0038): per-predicate ethics refusal + hedge injection
Two sibling escalation tiers above the audit-only ethics baseline,
both opt-in per commitment via the ethics pack JSON.
ADR-0037 — refusal_commitments
- EthicsPack.refusal_commitments (frozenset[str]; subset of
commitment_ids; validated at load time, unknown id rejected)
- Generic refusal prefix: "I cannot proceed — boundary violated: "
- Source-tagged refusal ids: "safety:<id>" / "ethics:<id>"
- build_refusal_surface now takes (safety_verdict, ethics_verdict,
ethics_pack); ADR-0036 single-arg call remains valid back-compat
- Default pack ships refusal_commitments: [] — audit-only floor
preserved
- Re-ratified default pack (mastery sha changes with schema field)
ADR-0038 — hedge_commitments
- EthicsPack.hedge_commitments (sibling field; same validator)
- Mutually exclusive with refusal_commitments at load time
- Runtime prepends manifold's preferred_hedge_soft (fallback
preferred_hedge_strong) when an opted-in commitment fires
runtime-checkable
- Refusal supersedes hedge globally; stub path skips hedge (already
a disclosure surface); main path only
- Idempotent on prefix (case-insensitive) — defends against
ADR-0028 assembler hedges
- Does NOT flip _last_refusal_was_typed — hedge is not refusal
Surface contract:
- ChatResponse.walk_surface + articulation_surface preserved unchanged
on both refusal and hedge paths (same audit discipline as ADR-0036)
- Only user-facing ChatResponse.surface (and TurnEvent.surface on
main path) is mutated
Files:
- packs/ethics/loader.py — refusal_commitments + hedge_commitments
fields; _validate_opt_in_subset; mutual-exclusion check
- packs/ethics/default_general_ethics_v1.json — both opt-in lists
empty; re-ratified
- chat/refusal.py — generic prefix, source-tagged ids,
violated_runtime_checkable_ethics, should_inject_hedge,
build_hedge_prefix, inject_hedge
- chat/runtime.py — passes ethics_verdict + ethics_pack to refusal
builder; hedge injection branch after refusal check
- tests/test_ethics_refusal_opt_in.py (new) — 16 tests
- tests/test_hedge_injection.py (new) — 22 tests
- docs/decisions/ADR-0037-per-predicate-ethics-refusal.md (new)
- docs/decisions/ADR-0038-hedge-injection.md (new)
Verification:
- Combined pack-layer suite: 154 green (was 116 after ADR-0036)
- CLI suites unchanged: smoke 67, runtime 19, cognition 121
- core eval cognition: intent 100%, versor_closure 100% (baseline)