dcy-034 마지막 조각. sync(슬롯 qty←브로커+warning해제+OQ-A 전이엣지+FR-7 평단seed)/verify(C 재대조·강제해제없음). core-A QC minor 2건 정리 동봉. QC(opus) PASS, 683 passed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VHtM5Ws1HKCKQ4JGi6GRb5
67 lines
2.2 KiB
Python
67 lines
2.2 KiB
Python
"""Application ports (REQ-007). The intent dedup/audit store contract.
|
|
|
|
Kept in the application layer (not domain) so the pure slot domain stays
|
|
unaware of the intent concept. Concrete implementations live in adapters.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Optional, Protocol
|
|
from uuid import UUID
|
|
|
|
|
|
class IntentStore(Protocol):
|
|
"""Durable idempotency + audit store for accepted intents (dcy-002 OQ-1).
|
|
|
|
`try_insert` is the dedup gate: it returns False if `intent_id` already
|
|
exists, which prevents a duplicate intent from re-driving a slot
|
|
transition (NFR-3). `finalize` records the decision after processing.
|
|
"""
|
|
|
|
def try_insert(self, intent_id: str, intent_type: str, payload: dict) -> bool:
|
|
"""Insert a new intent row in 'processing' state.
|
|
|
|
Returns True if newly inserted, False if `intent_id` already exists.
|
|
"""
|
|
...
|
|
|
|
def get(self, intent_id: str) -> Optional["IntentRecord"]:
|
|
"""Return the stored intent record, or None."""
|
|
...
|
|
|
|
def finalize(
|
|
self,
|
|
intent_id: str,
|
|
status: str,
|
|
slot_id: Optional[UUID],
|
|
reason: Optional[str],
|
|
) -> None:
|
|
"""Update an intent row with its final decision (admitted/rejected)."""
|
|
...
|
|
|
|
|
|
class IntentRecord:
|
|
"""Snapshot of an intent_log row (duck-typed; see adapters)."""
|
|
|
|
intent_id: str
|
|
intent_type: str
|
|
status: str
|
|
slot_id: Optional[UUID]
|
|
reason: Optional[str]
|
|
|
|
|
|
class PortfolioHubCostBasisPort(Protocol):
|
|
"""FR-7 (dcy-034 / REQ-105): portfolio-hub WAC (weighted average cost) read port.
|
|
|
|
Wraps `GET /v1/portfolios/{id}/holdings/cost-basis` from portfolio-hub REQ-043.
|
|
When unavailable or when the portfolio has no holding, implementations return None.
|
|
sync() degrades gracefully: None => skip avg_price seed + WARN log.
|
|
"""
|
|
|
|
def get_cost_basis(self, portfolio_id: str) -> Optional[float]:
|
|
"""Return the weighted average cost (WAC) for the portfolio's current holding.
|
|
|
|
Returns None when the portfolio has no holding, the endpoint is unavailable,
|
|
or any error occurs. The caller must handle None as a graceful-degrade signal.
|
|
"""
|
|
...
|