session-memory Phase 2: hub decision integration (T05)

decisions.py: every final promote/reject becomes a record_decision-shaped
payload (rationale + source key + evidence snapshot). DecisionRecorder degrades
gracefully under a hub outage — pluggable sink with a durable local-queue
fallback and ordered flush/replay (mirrors Phase 1's after-the-fact sync).
Wired into review() via an optional recorder. 6 new tests; suite 70/70 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 00:31:22 +02:00
parent ab22d22bfb
commit 4b7a628b6f
4 changed files with 193 additions and 3 deletions

View File

@@ -22,6 +22,7 @@ from datetime import datetime, timezone
from typing import Callable, Optional
from .catalog import Catalog
from .decisions import DecisionRecorder
from .gating import GateConfig, evaluate
from .schema import Provenance, Resolution, Scope, SolutionPattern
@@ -119,13 +120,16 @@ class ReviewResult:
def review(candidates: list[dict], decide: Decider, catalog: Catalog,
log: ReviewLog, gate: Optional[GateConfig] = None) -> ReviewResult:
log: ReviewLog, gate: Optional[GateConfig] = None,
recorder: Optional[DecisionRecorder] = None) -> ReviewResult:
"""Run each candidate through ``decide``; promote approvals into ``catalog``.
When a ``gate`` (T04 evidence bar) is supplied, the promoted pattern's
``status``/``distribution_ready`` are set from the gate evaluation, so an
approved-but-thin candidate lands as ``provisional`` rather than
distribution-ready.
distribution-ready. When a ``recorder`` (T05) is supplied, each final
promote/reject is logged as an auditable hub decision (queued if the hub is
down).
"""
result = ReviewResult()
for cand in candidates:
@@ -149,4 +153,6 @@ def review(candidates: list[dict], decide: Decider, catalog: Catalog,
else:
raise ValueError(f"unknown review action {action!r}")
log.record(cand, action, rationale)
if recorder is not None:
recorder.record(cand, action, rationale)
return result