feat(s52): MAF-free nested payload builder (verdict_to_dict oracle) + file notifier
This commit is contained in:
parent
d5b583349a
commit
f206dfbf65
2 changed files with 66 additions and 3 deletions
|
|
@ -15,8 +15,10 @@ Registered in ``tests/test_okf.py``'s ``_MAF_FREE_MODULES`` (direct-import guard
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
import sys
|
import sys
|
||||||
from typing import TYPE_CHECKING, Protocol, TextIO, runtime_checkable
|
from pathlib import Path
|
||||||
|
from typing import TYPE_CHECKING, Any, Protocol, TextIO, runtime_checkable
|
||||||
|
|
||||||
if TYPE_CHECKING: # verdicts imports agent_framework — keep it out of the runtime import graph
|
if TYPE_CHECKING: # verdicts imports agent_framework — keep it out of the runtime import graph
|
||||||
from portfolio_optimiser.verdicts import Verdict
|
from portfolio_optimiser.verdicts import Verdict
|
||||||
|
|
@ -46,3 +48,35 @@ class ConsoleNotifier:
|
||||||
|
|
||||||
def __call__(self, verdict: Verdict) -> None:
|
def __call__(self, verdict: Verdict) -> None:
|
||||||
print(f"[notify] verdict {verdict.id} decision={verdict.decision}", file=self._stream)
|
print(f"[notify] verdict {verdict.id} decision={verdict.decision}", file=self._stream)
|
||||||
|
|
||||||
|
|
||||||
|
def _verdict_payload(verdict: Verdict) -> dict[str, Any]:
|
||||||
|
"""NESTED, byte-deterministic, duck-typed payload — reproduces ``verdicts.verdict_to_dict``'s
|
||||||
|
exact shape (verdicts.py:124-139) WITHOUT importing it (a runtime import would pull
|
||||||
|
``agent_framework``). Pinned against drift by the ``== verdict_to_dict(v)`` oracle test."""
|
||||||
|
f = verdict.proposal_features
|
||||||
|
return {
|
||||||
|
"id": verdict.id,
|
||||||
|
"decision": verdict.decision,
|
||||||
|
"rationale": verdict.rationale,
|
||||||
|
"proposal_features": {
|
||||||
|
"affected_codes": sorted(f.affected_codes),
|
||||||
|
"measure_type": f.measure_type,
|
||||||
|
"claimed_saving_nok": f.claimed_saving_nok,
|
||||||
|
"description": f.description,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FileNotifier:
|
||||||
|
"""Appends one JSONL line per verdict (byte-deterministic: ``sort_keys`` + LF, no wall-clock —
|
||||||
|
the ``outbox._dump`` idiom). Parent directories are created as needed."""
|
||||||
|
|
||||||
|
def __init__(self, path: str) -> None:
|
||||||
|
self._path = Path(path)
|
||||||
|
|
||||||
|
def __call__(self, verdict: Verdict) -> None:
|
||||||
|
self._path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
line = json.dumps(_verdict_payload(verdict), sort_keys=True) + "\n"
|
||||||
|
with self._path.open("a", encoding="utf-8") as handle:
|
||||||
|
handle.write(line)
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,10 @@ import MAF-tainted modules freely (``verdicts``) — only ``notify.py`` itself m
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import io
|
import io
|
||||||
|
import json
|
||||||
|
|
||||||
from portfolio_optimiser.notify import ConsoleNotifier, Notifier
|
from portfolio_optimiser.notify import ConsoleNotifier, FileNotifier, Notifier, _verdict_payload
|
||||||
from portfolio_optimiser.verdicts import ProposalFeatures, Verdict, capture_verdict
|
from portfolio_optimiser.verdicts import ProposalFeatures, Verdict, capture_verdict, verdict_to_dict
|
||||||
|
|
||||||
|
|
||||||
def _verdict() -> Verdict:
|
def _verdict() -> Verdict:
|
||||||
|
|
@ -40,3 +41,31 @@ def test_console_emits_id_and_decision() -> None:
|
||||||
out = stream.getvalue()
|
out = stream.getvalue()
|
||||||
assert verdict.id in out
|
assert verdict.id in out
|
||||||
assert verdict.decision in out
|
assert verdict.decision in out
|
||||||
|
|
||||||
|
|
||||||
|
def test_payload_matches_verdict_to_dict_oracle() -> None:
|
||||||
|
"""DRIFT-ORACLE: the duck-typed ``_verdict_payload`` reproduces ``verdict_to_dict``'s exact
|
||||||
|
NESTED shape (verdicts.py:124-139) without importing it at runtime — the test imports both
|
||||||
|
(tests may pull MAF freely) and pins them equal, so a ``Verdict`` shape change goes RED here
|
||||||
|
instead of silently diverging the notified payload."""
|
||||||
|
verdict = _verdict()
|
||||||
|
assert _verdict_payload(verdict) == verdict_to_dict(verdict)
|
||||||
|
|
||||||
|
|
||||||
|
def test_file_notifier_byte_deterministic(tmp_path) -> None:
|
||||||
|
"""FileNotifier appends one JSONL line per verdict — two writes of the same verdict are
|
||||||
|
byte-identical, LF-terminated, nested per the oracle shape, and carry no wall-clock key."""
|
||||||
|
path = tmp_path / "sub" / "notify.jsonl"
|
||||||
|
verdict = _verdict()
|
||||||
|
notifier = FileNotifier(str(path))
|
||||||
|
notifier(verdict)
|
||||||
|
first = path.read_bytes()
|
||||||
|
notifier(verdict)
|
||||||
|
second = path.read_bytes()
|
||||||
|
assert second == first * 2 # identical appended line bytes
|
||||||
|
line = first.decode("utf-8")
|
||||||
|
assert line.endswith("\n")
|
||||||
|
payload = json.loads(line)
|
||||||
|
assert payload["id"] == verdict.id
|
||||||
|
assert payload["proposal_features"]["affected_codes"] == ["03.1", "05.2"]
|
||||||
|
assert not any(k in payload for k in ("date", "timestamp", "created", "ts"))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue