feat(core-A): reconcile-warning durable store + blocking gate (BAT-050, REQ-102/103/104/107/108)
B/C 불일치 시 슬롯을 durable 차단(ReconcileWarningStore)하고 전략 intent 경로에 pause∨reconcile_warned 게이트를 추가한다. 안전망(safety-sell/kill-switch/watchdog) 경로는 SlotService 직접 호출로 게이트를 구조적으로 우회(OQ-B 확정). SlotState enum/전이표/wire 계약 필드는 불변, reconcile_warned 필드만 additive 추가. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VHtM5Ws1HKCKQ4JGi6GRb5
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
-- BAT-050 / REQ-102: reconcile-warning durable store (dcy-034).
|
||||
-- PK: slot_id (TEXT, UUID string). pause_store 동형 설계.
|
||||
-- ON CONFLICT DO UPDATE: 동일 slot_id 재삽입 시 최신 값으로 갱신.
|
||||
CREATE TABLE IF NOT EXISTS slot_reconcile_warning (
|
||||
slot_id TEXT PRIMARY KEY,
|
||||
portfolio_id TEXT NOT NULL,
|
||||
case_code TEXT NOT NULL,
|
||||
actor TEXT NOT NULL DEFAULT 'reconcile',
|
||||
reason TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
@@ -1,6 +1,12 @@
|
||||
"""portfolio-hub read adapters (REQ-027, dcy-005)."""
|
||||
from trex_core.adapters.portfolio.hub_read import PortfolioHubReadAdapter
|
||||
from trex_core.adapters.portfolio.pause_store import PostgreSQLPauseStore
|
||||
from trex_core.adapters.portfolio.reconcile_warning_store import PostgreSQLReconcileWarningStore
|
||||
from trex_core.adapters.portfolio.stub_read import StubPortfolioRead
|
||||
|
||||
__all__ = ["PortfolioHubReadAdapter", "PostgreSQLPauseStore", "StubPortfolioRead"]
|
||||
__all__ = [
|
||||
"PortfolioHubReadAdapter",
|
||||
"PostgreSQLPauseStore",
|
||||
"PostgreSQLReconcileWarningStore",
|
||||
"StubPortfolioRead",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
"""PostgreSQL-backed durable reconcile-warning store (REQ-102, BAT-050).
|
||||
|
||||
pause_store.py와 동형 설계. PK: slot_id. case_code: 'B' | 'C'.
|
||||
Survives daemon restart (dcy-034 NFR-1).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from trex_core.adapters.persistence.postgresql import _require_psycopg
|
||||
|
||||
|
||||
class PostgreSQLReconcileWarningStore:
|
||||
"""`slot_reconcile_warning` table CRUD. slot_id PK is the gate (REQ-102)."""
|
||||
|
||||
def __init__(self, dsn: str, *, psycopg_module: Any = None) -> None:
|
||||
self._psycopg = _require_psycopg(psycopg_module)
|
||||
self._dsn = dsn
|
||||
|
||||
def set_warned(
|
||||
self,
|
||||
slot_id: str,
|
||||
*,
|
||||
portfolio_id: str,
|
||||
case_code: str,
|
||||
actor: str = "reconcile",
|
||||
reason: str = "",
|
||||
) -> None:
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
with self._psycopg.connect(self._dsn, autocommit=False) as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"INSERT INTO slot_reconcile_warning "
|
||||
"(slot_id, portfolio_id, case_code, actor, reason, created_at, updated_at) "
|
||||
"VALUES (%s, %s, %s, %s, %s, %s, %s) "
|
||||
"ON CONFLICT (slot_id) DO UPDATE "
|
||||
"SET portfolio_id = EXCLUDED.portfolio_id, "
|
||||
" case_code = EXCLUDED.case_code, "
|
||||
" actor = EXCLUDED.actor, "
|
||||
" reason = EXCLUDED.reason, "
|
||||
" updated_at = EXCLUDED.updated_at",
|
||||
(slot_id, portfolio_id, case_code, actor, reason, now, now),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
def clear_warned(self, slot_id: str) -> None:
|
||||
with self._psycopg.connect(self._dsn, autocommit=False) as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"DELETE FROM slot_reconcile_warning WHERE slot_id = %s",
|
||||
(slot_id,),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
def is_warned(self, slot_id: str) -> bool:
|
||||
with self._psycopg.connect(self._dsn) as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT 1 FROM slot_reconcile_warning WHERE slot_id = %s LIMIT 1",
|
||||
(slot_id,),
|
||||
)
|
||||
return cur.fetchone() is not None
|
||||
|
||||
def list_warned(self) -> set[str]:
|
||||
with self._psycopg.connect(self._dsn) as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT slot_id FROM slot_reconcile_warning")
|
||||
return {row[0] for row in cur.fetchall()}
|
||||
Reference in New Issue
Block a user