core/tests/test_correction_telemetry.py
Shay 29449f3775 feat(adr-0059): correction-pass telemetry emission — backward perturbation auditable
`ChatRuntime.correct()` propagates a backward perturbation through the
session graph (per session/correction.py): each past turn whose output
versor has non-trivial CGA-alignment with the correction versor is
blended toward it (decayed by graph distance).  The forward regen turn
that followed already emitted a TurnEvent — but the backward
perturbation itself was invisible to the telemetry sink.

ADR-0059 closes that gap with a discriminated event line.

- chat/telemetry.py — adds `serialize_correction_event` +
  `format_correction_event_jsonl` emitting one JSONL line discriminated
  by `"type": "correction"`.  Payload: target_turn, records_count,
  turns_skipped, turn_idxs_affected, max_delta_norm, mean_delta_norm,
  SHA-256 correction_versor_digest, pack ids.  No raw versor coordinates.
- chat/runtime.py — `_emit_correction_event` (mirrors
  `_emit_turn_event`); called from `correct()` after the graph state
  is updated but before the forward regen turn.  No-op without sink.
- tests/test_correction_telemetry.py — 7 tests pin: no-op without
  sink, emission with sink, payload shape (required keys + types +
  ranges), SHA-256 digest shape, trust boundary (no versor
  coordinates leaked), determinism (byte-identical lines across
  runs), correction event and turn event coexist in the sink.

Trust boundary (per CLAUDE.md):
  - Metadata-only: only L2 deltas + SHA-256 digest.
  - No implicit wall-clock.
  - Deterministic: same CorrectionResult → byte-identical line.
  - Sink contract unchanged: `emit(line: str)`.
  - `versor_condition < 1e-6` invariant: untouched (telemetry-only).

Verification: smoke 67 / runtime 19 / correction telemetry 7 — green.
2026-05-18 13:47:48 -07:00

174 lines
5.7 KiB
Python

"""ADR-0059 — correction-pass telemetry emission.
The backward perturbation triggered by ``ChatRuntime.correct()`` must
be visible to audit consumers. Today the forward regen turn that
follows ``correct()`` emits a turn event, but the correction delta
itself — which past turns moved, how far, and toward what — was
invisible to the telemetry sink.
ADR-0059 closes that gap with a dedicated correction-event JSONL line
discriminated by ``"type": "correction"``. Same sink contract; new
event type alongside ``TurnEvent`` lines.
These tests pin:
- No emission when no sink is attached.
- Emission when a sink is attached AND ``correct()`` triggered at
least one record.
- Payload shape: type discriminator, target_turn, records_count,
turns_skipped, turn_idxs_affected, max_delta_norm,
mean_delta_norm, correction_versor_digest (SHA-256), pack ids.
- Trust boundary: no versor coordinates in the line, only L2 deltas
and a deterministic versor digest.
- Determinism: same input → byte-identical line.
"""
from __future__ import annotations
import json
from chat.runtime import ChatRuntime
from chat.telemetry import JsonlBufferSink
def _seed_two_turns(rt: ChatRuntime) -> None:
"""Populate the session graph with enough turns for backward walk."""
rt.chat("Why does light exist?")
rt.chat("Why does thought exist?")
def test_no_emission_without_sink() -> None:
rt = ChatRuntime()
_seed_two_turns(rt)
response = rt.correct("light reveals truth")
assert response.surface != "" # regen still runs
def test_emission_when_sink_attached() -> None:
rt = ChatRuntime()
sink = JsonlBufferSink()
rt.attach_telemetry_sink(sink)
_seed_two_turns(rt)
n_before = len(sink.lines)
rt.correct("light reveals truth")
n_after = len(sink.lines)
assert n_after > n_before
correction_lines = [
json.loads(line) for line in sink.lines
if json.loads(line).get("type") == "correction"
]
assert len(correction_lines) == 1
def test_correction_payload_shape() -> None:
rt = ChatRuntime()
sink = JsonlBufferSink()
rt.attach_telemetry_sink(sink)
_seed_two_turns(rt)
rt.correct("light reveals truth")
correction_lines = [
json.loads(line) for line in sink.lines
if json.loads(line).get("type") == "correction"
]
assert len(correction_lines) == 1
payload = correction_lines[0]
required_keys = {
"type",
"target_turn",
"identity_pack_id",
"safety_pack_id",
"ethics_pack_id",
"records_count",
"turns_skipped",
"turn_idxs_affected",
"max_delta_norm",
"mean_delta_norm",
"correction_versor_digest",
}
assert required_keys.issubset(payload.keys())
assert payload["type"] == "correction"
assert payload["target_turn"] == -1
assert isinstance(payload["records_count"], int)
assert isinstance(payload["turn_idxs_affected"], list)
assert all(isinstance(t, int) for t in payload["turn_idxs_affected"])
assert isinstance(payload["max_delta_norm"], (int, float))
assert payload["max_delta_norm"] >= 0.0
assert isinstance(payload["mean_delta_norm"], (int, float))
assert payload["mean_delta_norm"] >= 0.0
def test_correction_versor_digest_is_sha256_prefix_or_empty() -> None:
rt = ChatRuntime()
sink = JsonlBufferSink()
rt.attach_telemetry_sink(sink)
_seed_two_turns(rt)
rt.correct("light reveals truth")
correction_lines = [
json.loads(line) for line in sink.lines
if json.loads(line).get("type") == "correction"
]
digest = correction_lines[0]["correction_versor_digest"]
assert isinstance(digest, str)
# SHA-256 hex digest = 64 chars; emit empty only if versor absent.
assert digest == "" or (len(digest) == 64 and all(c in "0123456789abcdef" for c in digest))
def test_no_versor_coordinates_in_payload() -> None:
"""Trust boundary — only L2 deltas + SHA-256 digest, never coords."""
rt = ChatRuntime()
sink = JsonlBufferSink()
rt.attach_telemetry_sink(sink)
_seed_two_turns(rt)
rt.correct("light reveals truth")
payload = next(
json.loads(line) for line in sink.lines
if json.loads(line).get("type") == "correction"
)
forbidden_keys = {"old_versor", "new_versor", "correction_versor", "versor", "records"}
leaked = forbidden_keys & set(payload.keys())
assert not leaked, (
f"correction event leaked raw versor field(s) {leaked!r}: {payload}"
)
def test_correction_event_is_deterministic_across_runs() -> None:
"""Two runtimes seeded identically and corrected identically must
emit byte-identical correction-event lines."""
def _run() -> str:
rt = ChatRuntime()
sink = JsonlBufferSink()
rt.attach_telemetry_sink(sink)
_seed_two_turns(rt)
rt.correct("light reveals truth")
return next(
line for line in sink.lines
if json.loads(line).get("type") == "correction"
)
a = _run()
b = _run()
assert a == b
def test_turn_event_and_correction_event_coexist() -> None:
"""``correct()`` emits a correction event AND the regen turn emits
its own turn event. Both types appear in the sink."""
rt = ChatRuntime()
sink = JsonlBufferSink()
rt.attach_telemetry_sink(sink)
_seed_two_turns(rt)
n_before = len(sink.lines)
rt.correct("light reveals truth")
new_lines = [json.loads(line) for line in sink.lines[n_before:]]
types_seen = {payload.get("type", "turn") for payload in new_lines}
# Correction event discriminated; turn event has no "type" key by today's
# serializer, so it presents as the default "turn".
assert "correction" in types_seen