All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 10m40s
TrustScoreManager 從記憶體升級為 PostgreSQL 持久化,
Pod 重啟後信任分數不再歸零,AI 能真正累積到 L4 自動放行門檻。
變更:
- migrations/adr088_trust_score_persistence.sql: trust_records 表
- db/models.py: TrustRecordDB ORM model
- repositories/interfaces.py: ITrustRepository Protocol
- repositories/trust_repository.py: PG upsert ON CONFLICT DO UPDATE
- services/trust_engine.py: bulk_load() 啟動 warm-up
- services/learning_service.py: _persist_trust() + 2 call sites
- main.py: 啟動時 load_all() → bulk_load()
流程: 批准 5 次 → score=5 寫入 DB → Pod 重啟 → warm-up 讀回
→ evaluate_adjusted_risk MEDIUM→LOW → 自動執行
2026-04-17 ogt + Claude Sonnet 4.6(亞太): ADR-088
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
146 lines
4.8 KiB
Python
146 lines
4.8 KiB
Python
"""
|
||
Trust Repository - PostgreSQL 持久化層
|
||
======================================
|
||
ADR-088: TrustScoreManager 持久化升級
|
||
|
||
職責: trust_records 表的 upsert / load_all
|
||
設計: 實作 ITrustRepository Protocol
|
||
|
||
Pod 重啟後信任分數不歸零,AI 能真正累積到 L4 自動放行門檻。
|
||
|
||
2026-04-17 ogt + Claude Sonnet 4.6(亞太): Phase 4 信任持久化
|
||
"""
|
||
|
||
from datetime import datetime
|
||
|
||
import structlog
|
||
from sqlalchemy import select
|
||
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||
|
||
from src.db.base import get_db_context
|
||
from src.db.models import TrustRecordDB
|
||
from src.repositories.interfaces import ITrustRepository
|
||
|
||
logger = structlog.get_logger(__name__)
|
||
|
||
|
||
class TrustRepository:
|
||
"""
|
||
Trust Repository 實作(PostgreSQL)
|
||
|
||
使用 PostgreSQL ON CONFLICT DO UPDATE (upsert),
|
||
保證並行安全且無競爭條件。
|
||
"""
|
||
|
||
async def upsert(
|
||
self,
|
||
action_pattern: str,
|
||
score: int,
|
||
total_approvals: int,
|
||
total_rejections: int,
|
||
last_approval_by: str | None = None,
|
||
last_approval_at: datetime | None = None,
|
||
last_rejection_by: str | None = None,
|
||
last_rejection_at: datetime | None = None,
|
||
) -> bool:
|
||
"""
|
||
INSERT or UPDATE trust record。
|
||
|
||
使用 PostgreSQL upsert 保證並行安全。
|
||
action_pattern 是主鍵,衝突時更新除 created_at 外的所有欄位。
|
||
"""
|
||
from src.utils.timezone import now_taipei
|
||
|
||
try:
|
||
async with get_db_context() as db:
|
||
stmt = pg_insert(TrustRecordDB).values(
|
||
action_pattern=action_pattern,
|
||
score=score,
|
||
total_approvals=total_approvals,
|
||
total_rejections=total_rejections,
|
||
last_approval_by=last_approval_by,
|
||
last_approval_at=last_approval_at,
|
||
last_rejection_by=last_rejection_by,
|
||
last_rejection_at=last_rejection_at,
|
||
created_at=now_taipei(),
|
||
updated_at=now_taipei(),
|
||
).on_conflict_do_update(
|
||
index_elements=["action_pattern"],
|
||
set_={
|
||
"score": score,
|
||
"total_approvals": total_approvals,
|
||
"total_rejections": total_rejections,
|
||
"last_approval_by": last_approval_by,
|
||
"last_approval_at": last_approval_at,
|
||
"last_rejection_by": last_rejection_by,
|
||
"last_rejection_at": last_rejection_at,
|
||
"updated_at": now_taipei(),
|
||
},
|
||
)
|
||
await db.execute(stmt)
|
||
|
||
logger.debug(
|
||
"trust_record_upserted",
|
||
action_pattern=action_pattern,
|
||
score=score,
|
||
)
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(
|
||
"trust_record_upsert_failed",
|
||
action_pattern=action_pattern,
|
||
error=str(e),
|
||
)
|
||
return False
|
||
|
||
async def load_all(self) -> list[dict]:
|
||
"""
|
||
載入所有 trust records,供啟動 warm-up。
|
||
|
||
Returns:
|
||
list[dict] — 每筆含 action_pattern, score, total_approvals,
|
||
total_rejections, last_approval_by, last_approval_at,
|
||
last_rejection_by, last_rejection_at
|
||
"""
|
||
try:
|
||
async with get_db_context() as db:
|
||
result = await db.execute(select(TrustRecordDB))
|
||
rows = result.scalars().all()
|
||
|
||
records = [
|
||
{
|
||
"action_pattern": r.action_pattern,
|
||
"score": r.score,
|
||
"total_approvals": r.total_approvals,
|
||
"total_rejections": r.total_rejections,
|
||
"last_approval_by": r.last_approval_by,
|
||
"last_approval_at": r.last_approval_at,
|
||
"last_rejection_by": r.last_rejection_by,
|
||
"last_rejection_at": r.last_rejection_at,
|
||
}
|
||
for r in rows
|
||
]
|
||
|
||
logger.info("trust_records_loaded", count=len(records))
|
||
return records
|
||
|
||
except Exception as e:
|
||
logger.error("trust_records_load_failed", error=str(e))
|
||
return []
|
||
|
||
|
||
# =============================================================================
|
||
# Singleton
|
||
# =============================================================================
|
||
|
||
_repository: TrustRepository | None = None
|
||
|
||
|
||
def get_trust_repository() -> ITrustRepository:
|
||
"""取得 TrustRepository 單例"""
|
||
global _repository
|
||
if _repository is None:
|
||
_repository = TrustRepository()
|
||
return _repository
|