feat(adr-0035): turn-loop auto-invocation — surfacing only

Wires SafetyCheck and EthicsCheck into ChatRuntime at end-of-turn on
both the main articulation path and _stub_response.  Verdicts attach
to ChatResponse.safety_verdict / .ethics_verdict and TurnEvent.
Observational at v1: no refusal, no re-articulation, no behavioral
change.  Refusal policy is the next ADR with real verdict data in hand.

Runtime-checkable predicates today:
  - preserve_versor_closure         (via _FieldStateWithVersor adapter)
  - no_identity_override            (manifold hash before vs after; equal by construction)
  - no_silent_correction            (runtime._last_refusal_was_typed bookkeeping)
  - acknowledge_uncertainty         (IdentityScore.alignment + hedge detection)
  - disclose_limitations            (walk_surface == _UNKNOWN_DOMAIN_SURFACE)

Predicates with no runtime evidence (no_manipulation, no_fabricated_source,
defer_high_stakes_to_human_review, respect_user_autonomy, no_hot_path_repair)
honestly report runtime_checkable=False per the ADR-0032/0034 discipline.
They become checkable as classifiers and pipelines land — surface contract
doesn't change.

Test coverage: 14 new tests; combined pack-layer surface suite (loaders +
checks + turn-loop) now 122 green.  CLI suites unaffected: smoke 67,
cognition 121, teaching 17, runtime 19.  Cognition eval baseline preserved.
This commit is contained in:
Shay 2026-05-17 20:57:33 -07:00
parent db5bc028f9
commit 514ace0cbf
4 changed files with 444 additions and 3 deletions

View file

@ -1,6 +1,8 @@
from __future__ import annotations
from dataclasses import dataclass, replace
import hashlib
import json
import re
from collections.abc import Sequence
from typing import List
@ -18,14 +20,14 @@ from core.physics.identity import (
IdentityScore,
TurnEvent,
)
from packs.ethics.check import EthicsCheck
from packs.ethics.check import EthicsCheck, EthicsContext
from packs.ethics.loader import (
DEFAULT_ETHICS_PACK as _DEFAULT_ETHICS_PACK,
EthicsPackError,
load_ethics_pack,
)
from packs.identity.loader import load_identity_manifold
from packs.safety.check import SafetyCheck
from packs.safety.check import SafetyCheck, SafetyContext
from packs.safety.loader import load_safety_pack
from field.state import FieldState
from generate.articulation import ArticulationPlan, realize
@ -128,6 +130,80 @@ class _StubBindingFrame:
cycle_index: int
@dataclass(frozen=True, slots=True)
class _FieldStateWithVersor:
"""Adapter exposing ``versor_condition`` for SafetyContext.
``FieldState`` itself does not carry a precomputed
``versor_condition`` attribute; it is computed on demand from
``versor_condition(state.F)``. The SafetyCheck predicate for
``preserve_versor_closure`` reads ``ctx.field_state.versor_condition``
via ``getattr``. This adapter exposes the precomputed value so the
predicate is runtime-checkable each turn.
"""
versor_condition: float
def _hash_identity_manifold(manifold) -> str:
"""Deterministic SHA-256 of the load-bearing identity-manifold fields.
ADR-0035 feeds the ``no_identity_override`` predicate in
:class:`SafetyCheck`. The runtime never mutates ``identity_manifold``
after composition, so before- and after-turn hashes are equal by
construction; an unequal hash would indicate the predicate's exact
failure mode.
"""
payload = {
"value_axes": [
{
"axis_id": axis.axis_id,
"name": axis.name,
"direction": list(axis.direction),
"weight": axis.weight,
}
for axis in manifold.value_axes
],
"boundary_ids": sorted(manifold.boundary_ids),
"alignment_threshold": manifold.alignment_threshold,
}
blob = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
return hashlib.sha256(blob).hexdigest()
def _surface_contains_hedge(surface: str, manifold) -> bool:
"""Detect whether the realized surface emitted a hedge phrase.
Compares case-insensitively against the manifold's preferred hedge
phrases (ADR-0028). False when surface is empty. Coarse but
deterministic: the predicate downstream is observational, so
occasional false negatives are surfaced as
``acknowledge_uncertainty`` violations in audit and corrected by
refining hedge detection, not by silently passing.
"""
if not surface:
return False
prefs = getattr(manifold, "surface_preferences", None)
if prefs is None:
return False
candidates: list[str] = []
for field_name in (
"preferred_hedge_strong",
"preferred_hedge_soft",
"preferred_qualifier",
):
value = getattr(prefs, field_name, "")
if value:
candidates.append(value)
for _, hedge in getattr(prefs, "axis_hedges", ()) or ():
for sub in ("strong", "soft", "qualifier"):
value = getattr(hedge, sub, "")
if value:
candidates.append(value)
surface_fold = surface.casefold()
return any(c.casefold() in surface_fold for c in candidates if c)
def _make_trajectory_from_result(result, turn: int):
from core.physics.reasoning import TrajectoryOperator
@ -167,6 +243,10 @@ class ChatResponse:
# admissibility was checked this turn" (cold start, refusal, stub).
admissibility_trace: tuple = ()
region_was_unconstrained: bool = True
# ADR-0035 — verdicts surfaced from SafetyCheck and EthicsCheck.
# ``None`` only on stub/refusal paths that bypass the turn loop.
safety_verdict: object = None
ethics_verdict: object = None
class ChatRuntime:
@ -274,8 +354,17 @@ class ChatRuntime:
# future ADR.
self.safety_check = SafetyCheck()
# ADR-0034 — structural ethics surface, sibling to SafetyCheck.
# Observational at v1; no auto-invocation in the turn loop.
self.ethics_check = EthicsCheck()
# ADR-0035 — auto-invoke both checks at end-of-turn. The
# manifold is constructed once and never mutated, so the
# pre-turn hash is a stable property of this runtime instance.
# ``last_refusal_was_typed`` defaults True (no untyped refusals
# observed); turn-loop bookkeeping flips this on typed-refusal
# paths so the predicate has live evidence.
self._identity_manifold_hash: str = _hash_identity_manifold(
self.identity_manifold,
)
self._last_refusal_was_typed: bool = True
self.turn_log: List[TurnEvent] = []
self._correction_pass = CorrectionPass()
self._last_valence: float = 0.0
@ -385,6 +474,29 @@ class ChatRuntime:
output_language=self.config.output_language,
frame_id="unknown_domain",
)
# ADR-0035 — stub responses are exactly the ungrounded path that
# triggers ``disclose_limitations``. Surfacing verdicts here
# keeps the audit contract uniform: every ChatResponse carries
# a SafetyVerdict and EthicsVerdict.
safety_ctx = SafetyContext(
field_state=_FieldStateWithVersor(
versor_condition=float(versor_condition(field_state.F)),
),
last_refusal_was_typed=self._last_refusal_was_typed,
identity_manifold_hash_before=self._identity_manifold_hash,
identity_manifold_hash_after=_hash_identity_manifold(self.identity_manifold),
)
safety_verdict = self.safety_check.check(safety_ctx, self.safety_pack)
ethics_ctx = EthicsContext(
alignment_score=0.0,
hedge_threshold_soft=float(
self.identity_manifold.surface_preferences.hedge_threshold_soft
),
hedge_emitted=False,
grounded_in_evidence=False,
disclosure_emitted=True,
)
ethics_verdict = self.ethics_check.check(ethics_ctx, self.ethics_pack)
return ChatResponse(
surface=_UNKNOWN_DOMAIN_SURFACE,
proposition=prop,
@ -401,6 +513,8 @@ class ChatRuntime:
identity_score=None,
character_profile=self.character_profile,
flagged=False,
safety_verdict=safety_verdict,
ethics_verdict=ethics_verdict,
)
def chat(self, text: str, max_tokens: int | None = None) -> ChatResponse:
@ -531,6 +645,31 @@ class ChatRuntime:
)
walk_surface = sentence_plan.surface
vault_hits = int(result.vault_hits)
# ADR-0035 — auto-invoke safety + ethics surfaces. Observational
# at v1; verdicts are attached to TurnEvent and ChatResponse for
# audit but do not gate behavior. Refusal/re-articulation
# wiring is a future ADR.
is_grounded = walk_surface != _UNKNOWN_DOMAIN_SURFACE
hedge_emitted = _surface_contains_hedge(walk_surface, self.identity_manifold)
safety_ctx = SafetyContext(
field_state=_FieldStateWithVersor(
versor_condition=float(versor_condition(result.final_state.F)),
),
last_refusal_was_typed=self._last_refusal_was_typed,
identity_manifold_hash_before=self._identity_manifold_hash,
identity_manifold_hash_after=_hash_identity_manifold(self.identity_manifold),
)
safety_verdict = self.safety_check.check(safety_ctx, self.safety_pack)
ethics_ctx = EthicsContext(
alignment_score=float(getattr(identity_score, "alignment", 0.0)),
hedge_threshold_soft=float(
self.identity_manifold.surface_preferences.hedge_threshold_soft
),
hedge_emitted=hedge_emitted,
grounded_in_evidence=is_grounded,
disclosure_emitted=not is_grounded,
)
ethics_verdict = self.ethics_check.check(ethics_ctx, self.ethics_pack)
turn_event = TurnEvent(
turn=self._context.turn - 1,
input_tokens=tuple(filtered),
@ -544,6 +683,8 @@ class ChatRuntime:
versor_condition=versor_condition(result.final_state.F),
flagged=flagged,
elaboration=sentence_plan.elaboration,
safety_verdict=safety_verdict,
ethics_verdict=ethics_verdict,
)
self.turn_log.append(turn_event)
return ChatResponse(
@ -564,6 +705,8 @@ class ChatRuntime:
flagged=flagged,
admissibility_trace=result.admissibility_trace,
region_was_unconstrained=result.region_was_unconstrained,
safety_verdict=safety_verdict,
ethics_verdict=ethics_verdict,
)
def _unknown_domain_response(self, field_state: FieldState, filtered: list[str]) -> ChatResponse:

View file

@ -275,3 +275,8 @@ class TurnEvent:
versor_condition: float
flagged: bool
elaboration: Optional[str] = None
# ADR-0035 — verdicts from SafetyCheck and EthicsCheck at end-of-turn.
# Observational at v1: surfaced for audit; no behavioral effect.
# Typed as ``object`` to avoid coupling identity.py to packs.*.
safety_verdict: object = None
ethics_verdict: object = None

View file

@ -0,0 +1,129 @@
# ADR-0035: Turn-Loop Verdict Surfacing for SafetyCheck and EthicsCheck
**Status:** Accepted (2026-05-17)
**Author:** Joshua Shay + planner pass
**Companion docs:** [`ADR-0032-safety-check-surface.md`](ADR-0032-safety-check-surface.md), [`ADR-0034-ethics-check-surface.md`](ADR-0034-ethics-check-surface.md)
## Context
ADR-0032 and ADR-0034 landed the structural surfaces — `SafetyCheck` and `EthicsCheck` — as observational, registry-of-predicates classes parallel in shape to `IdentityCheck`. Both ADRs deliberately left **auto-invocation in the turn loop** as a follow-up.
The follow-up is overdue. An observation surface that no caller invokes is a label without a verdict. `ChatRuntime` constructs both checks at startup and exposes them on `self.safety_check` / `self.ethics_check`, but no path in the turn loop calls `.check(...)`. The pack-layer audit is uniformly silent regardless of what the turn actually did.
Two scope options were on the table:
1. **Surfacing only.** Auto-invoke both checks at end-of-turn, populate contexts from whatever evidence the runtime has, attach verdicts to `ChatResponse` and `TurnEvent` for audit. No behavioral change.
2. **Surfacing + refusal.** Same as above plus typed refusal on runtime-checkable violations.
The conservative choice — option 1 — was selected. Reasoning, from the implementation discussion:
> Most context fields are `None` by default and the runtime today only has evidence for ~3 of the ~10 fields across both surfaces (versor_condition, identity_manifold hashes, alignment_score, plus the unknown-domain signal). With sparse evidence, most violations are *unobservable* at v1 — wiring refusal now would refuse on a tiny fraction of theoretical violations while letting the rest slip silently through. The right sequence is to land the invocation point, observe what evidence actually surfaces across real turns, *then* decide refusal policy with that data in hand.
CLAUDE.md's "small, load-bearing PRs with clear evidence" doctrine maps to this.
## Decision
`ChatRuntime` invokes both checks at the end of every chat turn — both the main articulation path and the `_stub_response` path. Verdicts attach to `ChatResponse` and `TurnEvent` as new optional fields. No behavioral effect; no refusal; no re-articulation; no retry.
### Invocation sites
| Path | When invoked | What populates the context |
|---|---|---|
| Main articulation path | Just before constructing `TurnEvent` | `result.final_state`, `identity_score`, `walk_surface` |
| `_stub_response` | Just before constructing the `ChatResponse` | `field_state`, fixed ungrounded signal |
Stub paths trigger exactly when grounding is insufficient — i.e., when the `disclose_limitations` commitment is most active. Surfacing verdicts there preserves the contract "every `ChatResponse` carries a `SafetyVerdict` and `EthicsVerdict`" and gives `disclose_limitations` a stable runtime-checkable signal.
### Evidence the turn loop populates today
**SafetyContext (runtime-checkable in production):**
| Field | Source | Predicate it unlocks |
|---|---|---|
| `field_state.versor_condition` | `versor_condition(final_state.F)` via an `_FieldStateWithVersor` adapter | `preserve_versor_closure` |
| `last_refusal_was_typed` | `runtime._last_refusal_was_typed` (default `True`; reserved for future typed-refusal bookkeeping) | `no_silent_correction` |
| `identity_manifold_hash_before` | `runtime._identity_manifold_hash` — captured once at startup; manifold is never mutated | `no_identity_override` |
| `identity_manifold_hash_after` | recomputed at end-of-turn — equal by construction | `no_identity_override` |
**EthicsContext (runtime-checkable in production):**
| Field | Source | Predicate it unlocks |
|---|---|---|
| `alignment_score` | `IdentityScore.alignment` (zero on stub path) | `acknowledge_uncertainty` |
| `hedge_threshold_soft` | `identity_manifold.surface_preferences.hedge_threshold_soft` | `acknowledge_uncertainty` |
| `hedge_emitted` | `_surface_contains_hedge(walk_surface, identity_manifold)` — substring check against the manifold's hedge phrases (strong/soft/qualifier + per-axis variants) | `acknowledge_uncertainty` |
| `grounded_in_evidence` | `walk_surface != _UNKNOWN_DOMAIN_SURFACE` | `disclose_limitations` |
| `disclosure_emitted` | `walk_surface == _UNKNOWN_DOMAIN_SURFACE` (the inverse) | `disclose_limitations` |
**Not yet populated (predicates default to `runtime_checkable=False, upheld=True`):**
- `cited_source_shas` / `allowed_source_shas` — citation pipeline isn't wired into chat turns.
- `high_stakes_topic` / `recommended_human_review` — no high-stakes classifier.
- `prescribed_single_answer` / `presented_options_count` — no prescriptiveness classifier.
These fields surface in verdicts as `runtime_checkable=False` per the ADR-0032/0034 honest-reporting discipline. As classifiers land (future ADRs), the corresponding predicates become checkable without changing the surface contract.
### Hash-of-manifold helper
`_hash_identity_manifold(manifold)` produces a deterministic SHA-256 of the load-bearing manifold fields:
```python
payload = {
"value_axes": [{"axis_id", "name", "direction", "weight"} for axis in manifold.value_axes],
"boundary_ids": sorted(manifold.boundary_ids),
"alignment_threshold": manifold.alignment_threshold,
}
sha256(canonical_json(payload))
```
Captured once at `ChatRuntime.__init__` and recomputed each turn. Because the runtime never mutates the manifold post-construction, before/after hashes are equal by construction — and that *is* the correct semantics for `no_identity_override`. An unequal hash would indicate the specific failure mode the predicate exists to surface.
### Surface contract changes
New optional fields, defaulting to `None`:
- `ChatResponse.safety_verdict: object``SafetyVerdict` on every non-error path; `None` only if a future path constructs `ChatResponse` directly without invoking the surface.
- `ChatResponse.ethics_verdict: object``EthicsVerdict` symmetric.
- `TurnEvent.safety_verdict: object` — present on every main-path turn; `None` on stub paths (which bypass `turn_log` by existing design).
- `TurnEvent.ethics_verdict: object` — symmetric.
The fields are typed as `object` to avoid forcing `core/physics/identity.py` and `chat/runtime.py` to import the pack layer at module-resolution time — the underlying types are `packs.safety.check.SafetyVerdict` and `packs.ethics.check.EthicsVerdict`. Callers downcast at the use site.
### What this ADR explicitly does NOT do
- **No refusal.** A runtime-checkable violation produces an audit verdict; the response is unchanged.
- **No re-articulation.** No retry path, no hedge injection on `acknowledge_uncertainty` violation, no escalation on `disclose_limitations` violation.
- **No logging integration.** Verdicts are attached to the data structures; emitting them to logs / telemetry is a downstream decision.
- **No CLI surface.** No `core chat --show-verdicts` flag yet.
- **No GenerationResult coupling.** Verdicts live on the chat-turn-shaped objects (`ChatResponse`, `TurnEvent`), not on `GenerationResult` — they belong to the turn, not the generation pass.
- **No cross-surface verdict bundle.** Three separate verdicts (identity / safety / ethics) per turn. A unified type is convenience sugar deferred to a future ADR.
## Consequences
### Positive
- **Audit no longer silent.** Every chat turn now produces three orthogonal verdicts (identity score + safety verdict + ethics verdict). An operator inspecting `runtime.turn_log[-1]` sees what each surface concluded.
- **Honest evidence-availability gradient.** Predicates report `runtime_checkable=False` where the runtime has no evidence, `runtime_checkable=True` where it does. Future ADRs that wire in more evidence (high-stakes classifier, citation pipeline, prescriptiveness detector) increase the checkable count without changing the surface contract.
- **Surface contract uniform across stub and main paths.** `disclose_limitations` is the first commitment where stub paths matter — and they correctly fire as `upheld=True` because stubs emit the unknown-domain marker.
- **Forward-compatible with refusal.** When the refusal-wiring ADR lands, the call site already exists; only the policy hook changes.
- **Cheap.** Auto-invocation is two predicate-registry passes per turn. No measurable cost on the smoke/cognition/runtime suites.
### Negative / risks
- **`_FieldStateWithVersor` adapter is a small piece of glue.** `FieldState` does not expose `versor_condition` as an attribute, so the SafetyCheck predicate cannot read it via `getattr` directly. The adapter is a frozen dataclass with one field. Mitigated: the adapter is one local class; if `FieldState` later carries the value natively, the adapter becomes a trivial removal.
- **`hedge_emitted` is substring-based.** A more rigorous detector (token-aware, hedge-phrase registry separate from the manifold) would catch edge cases. Acceptable at v1 because false negatives surface as `acknowledge_uncertainty` violations in audit — exactly where audit is supposed to direct attention — rather than passing silently.
- **Stub path does not append to `turn_log`.** Pre-existing behavior. The verdict is on the `ChatResponse` but the `TurnEvent` does not exist for stub turns. Documented as a known limit; a future ADR could append `TurnEvent` records for stub paths too if audit completeness becomes a priority.
## Verification
- `tests/test_turn_loop_verdicts.py` — 14 tests covering: verdicts attached to `ChatResponse` / `TurnEvent`; runtime-checkability of `preserve_versor_closure`, `no_identity_override`, `no_silent_correction`, `acknowledge_uncertainty`, `disclose_limitations`; `no_manipulation` honestly remaining non-runtime-checkable; hash determinism + before/after equality; hedge-detection happy and edge cases.
- Combined pack-layer test surface (loaders + checks + turn-loop) is **122 tests, all green**.
- CLI suites unaffected: smoke 67, cognition 121, teaching 17, runtime 19.
## Open questions deferred to a future ADR
1. **Refusal / re-articulation policy.** What does the runtime do with a violation? Refuse? Hedge? Log only? Per-pack policy? Per-predicate policy? Now that real verdict data flows, this can be decided empirically.
2. **TurnEvent for stub paths.** Should stub turns append a `TurnEvent` so audit completeness covers the entire turn stream?
3. **Verdict telemetry / logging.** A structured log sink that consumes verdicts is the next operational concern.
4. **CLI audit flag.** `core chat --show-verdicts` would print per-turn verdict summaries.
5. **Unified verdict bundle type.** A `TurnVerdicts` record grouping identity + safety + ethics for callers that want a single object.

View file

@ -0,0 +1,164 @@
"""ADR-0035 — turn-loop auto-invocation of SafetyCheck and EthicsCheck.
Observational surfacing: every non-stub turn attaches a SafetyVerdict
and EthicsVerdict to both the ChatResponse and the TurnEvent. No
behavioral change. Tests assert presence, shape, and the few
runtime-evidence fields the turn loop actually populates today.
"""
from __future__ import annotations
from chat.runtime import (
ChatRuntime,
_hash_identity_manifold,
_surface_contains_hedge,
)
from core.config import RuntimeConfig
from packs.ethics.check import EthicsVerdict
from packs.safety.check import SafetyVerdict
# ---------- ChatResponse / TurnEvent contract ----------
class TestVerdictsAttachedToResponse:
def test_chat_response_carries_safety_verdict(self) -> None:
rt = ChatRuntime(config=RuntimeConfig())
resp = rt.chat("light is")
assert isinstance(resp.safety_verdict, SafetyVerdict)
assert resp.safety_verdict.pack_id == rt.safety_pack.pack_id
def test_chat_response_carries_ethics_verdict(self) -> None:
rt = ChatRuntime(config=RuntimeConfig())
resp = rt.chat("light is")
assert isinstance(resp.ethics_verdict, EthicsVerdict)
assert resp.ethics_verdict.pack_id == rt.ethics_pack.pack_id
def test_turn_event_carries_both_verdicts_when_appended(self) -> None:
"""When the turn loop appends a TurnEvent (non-stub path), the
event carries both verdicts."""
rt = ChatRuntime(config=RuntimeConfig())
rt.chat("light is")
# Stub paths bypass turn_log by design; only assert when the
# main path was taken.
if rt.turn_log:
event = rt.turn_log[-1]
assert isinstance(event.safety_verdict, SafetyVerdict)
assert isinstance(event.ethics_verdict, EthicsVerdict)
# ---------- runtime evidence the turn loop populates ----------
class TestVersorClosureRuntimeCheckable:
def test_versor_closure_predicate_runtime_checkable_each_turn(self) -> None:
rt = ChatRuntime(config=RuntimeConfig())
resp = rt.chat("light is")
verdict = resp.safety_verdict
result = _find_safety(verdict, "preserve_versor_closure")
assert result.runtime_checkable
assert result.upheld
class TestIdentityOverrideRuntimeCheckable:
def test_identity_unchanged_upheld_each_turn(self) -> None:
rt = ChatRuntime(config=RuntimeConfig())
resp = rt.chat("light is")
result = _find_safety(resp.safety_verdict, "no_identity_override")
assert result.runtime_checkable
assert result.upheld
class TestNoSilentCorrectionRuntimeCheckable:
def test_default_path_typed_refusal_upheld(self) -> None:
rt = ChatRuntime(config=RuntimeConfig())
resp = rt.chat("light is")
result = _find_safety(resp.safety_verdict, "no_silent_correction")
assert result.runtime_checkable
assert result.upheld
# ---------- ethics evidence the turn loop populates ----------
class TestGroundednessSignal:
def test_acknowledge_uncertainty_runtime_checkable(self) -> None:
rt = ChatRuntime(config=RuntimeConfig())
resp = rt.chat("light is")
result = _find_ethics(resp.ethics_verdict, "acknowledge_uncertainty")
assert result.runtime_checkable
def test_disclose_limitations_runtime_checkable(self) -> None:
rt = ChatRuntime(config=RuntimeConfig())
resp = rt.chat("light is")
result = _find_ethics(resp.ethics_verdict, "disclose_limitations")
assert result.runtime_checkable
# ---------- helpers exercised independently ----------
class TestHashIdentityManifold:
def test_hash_is_deterministic(self) -> None:
rt = ChatRuntime(config=RuntimeConfig())
h1 = _hash_identity_manifold(rt.identity_manifold)
h2 = _hash_identity_manifold(rt.identity_manifold)
assert h1 == h2
assert len(h1) == 64
def test_pre_turn_hash_matches_post_turn_hash(self) -> None:
rt = ChatRuntime(config=RuntimeConfig())
before = _hash_identity_manifold(rt.identity_manifold)
rt.chat("light is")
after = _hash_identity_manifold(rt.identity_manifold)
assert before == after
class TestSurfaceContainsHedge:
def test_no_hedge_returns_false(self) -> None:
rt = ChatRuntime(config=RuntimeConfig())
assert not _surface_contains_hedge(
"the cat sat on the mat", rt.identity_manifold,
)
def test_empty_surface_returns_false(self) -> None:
rt = ChatRuntime(config=RuntimeConfig())
assert not _surface_contains_hedge("", rt.identity_manifold)
def test_known_hedge_phrase_detected(self) -> None:
rt = ChatRuntime(config=RuntimeConfig())
prefs = rt.identity_manifold.surface_preferences
hedge_phrase = prefs.preferred_hedge_soft or prefs.preferred_hedge_strong
if not hedge_phrase:
return # no hedge phrases configured in this pack
text = f"the answer is {hedge_phrase} that it works"
assert _surface_contains_hedge(text, rt.identity_manifold)
# ---------- verdict aggregation (no_manipulation always non-checkable) ----------
class TestNoManipulationStructural:
def test_no_manipulation_runtime_checkable_false_each_turn(self) -> None:
rt = ChatRuntime(config=RuntimeConfig())
resp = rt.chat("light is")
result = _find_ethics(resp.ethics_verdict, "no_manipulation")
assert not result.runtime_checkable
assert result.upheld
# ---------- helpers ----------
def _find_safety(verdict: SafetyVerdict, boundary_id: str):
for r in verdict.results:
if r.boundary_id == boundary_id:
return r
raise AssertionError(f"{boundary_id!r} not in safety verdict")
def _find_ethics(verdict: EthicsVerdict, commitment_id: str):
for r in verdict.results:
if r.commitment_id == commitment_id:
return r
raise AssertionError(f"{commitment_id!r} not in ethics verdict")