feat(trust): ADR-088 Trust Score 持久化 — L4 自動放行核心
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>
This commit is contained in:
OG T
2026-04-17 16:14:33 +08:00
parent 148d59a0e4
commit 9d6aa7ea45
7 changed files with 353 additions and 1 deletions

View File

@@ -32,8 +32,9 @@ import structlog
from src.models.approval import ApprovalRequest
from src.models.incident import IncidentStatus
from src.repositories.interfaces import ILearningRepository
from src.repositories.interfaces import ILearningRepository, ITrustRepository
from src.repositories.learning_repository import get_learning_repository
from src.repositories.trust_repository import get_trust_repository
from src.services.trust_engine import get_trust_manager
logger = structlog.get_logger(__name__)
@@ -156,9 +157,11 @@ class LearningService:
def __init__(
self,
repository: ILearningRepository | None = None,
trust_repository: ITrustRepository | None = None,
):
self._trust_manager = get_trust_manager()
self._repository = repository or get_learning_repository()
self._trust_repo = trust_repository or get_trust_repository()
async def process_execution_result(
self,
@@ -200,6 +203,9 @@ class LearningService:
)
feedback_type = FeedbackType.EXECUTION_FAILURE
# ADR-088: 持久化信任分數到 PostgreSQL (Pod 重啟後不歸零)
await self._persist_trust(action_pattern)
# 取得更新後的信任分數
trust_record = self._trust_manager.get_trust_record(action_pattern)
trust_after = trust_record.score if trust_record else 0
@@ -325,6 +331,9 @@ class LearningService:
)
playbook_updated = await self._demote_playbook(feedback.incident_id)
# ADR-088: 持久化信任分數到 PostgreSQL (Pod 重啟後不歸零)
await self._persist_trust(action_pattern)
trust_record = self._trust_manager.get_trust_record(action_pattern)
trust_after = trust_record.score if trust_record else 0
@@ -955,6 +964,36 @@ class LearningService:
"learning_status": learning_status,
}
async def _persist_trust(self, action_pattern: str) -> None:
"""
将内存中的 TrustRecord 持久化到 PostgreSQL。
ADR-088: 每次 approve/reject 後同步寫入 DB
確保 Pod 重啟後信任分數不歸零。
2026-04-17 ogt + Claude Sonnet 4.6(亞太): Phase 4 信任持久化
"""
record = self._trust_manager.get_trust_record(action_pattern)
if not record:
return
try:
await self._trust_repo.upsert(
action_pattern=action_pattern,
score=record.score,
total_approvals=record.total_approvals,
total_rejections=record.total_rejections,
last_approval_by=record.last_approval_by,
last_approval_at=record.last_approval_at,
last_rejection_by=record.last_rejection_by,
last_rejection_at=record.last_rejection_at,
)
except Exception as e:
logger.warning(
"trust_persist_failed",
action_pattern=action_pattern,
error=str(e),
)
def _get_action_tier(self, action: str) -> int:
"""取得動作的 Tier"""
tier_actions = {

View File

@@ -362,6 +362,46 @@ class TrustScoreManager:
record.score = 0
logger.warning("[TrustEngine] All trust scores reset!")
def bulk_load(self, records: list[dict]) -> int:
"""
從 DB 批量載入 trust records 到記憶體(啟動 warm-up 用)。
ADR-088: Pod 重啟後從 PostgreSQL 恢復信任分數,
確保 AI 不會因重啟而失憶歸零。
Args:
records: list[dict] — 每筆含 action_pattern, score,
total_approvals, total_rejections,
last_approval_by, last_approval_at,
last_rejection_by, last_rejection_at
Returns:
int: 載入筆數
2026-04-17 ogt + Claude Sonnet 4.6(亞太): ADR-088
"""
loaded = 0
for r in records:
pattern = r.get("action_pattern")
if not pattern:
continue
record = TrustRecord(
action_pattern=pattern,
score=r.get("score", 0),
total_approvals=r.get("total_approvals", 0),
total_rejections=r.get("total_rejections", 0),
last_approval_by=r.get("last_approval_by"),
last_approval_at=r.get("last_approval_at"),
last_rejection_by=r.get("last_rejection_by"),
last_rejection_at=r.get("last_rejection_at"),
)
self._records[pattern] = record
loaded += 1
if loaded:
logger.info(f"[TrustEngine] Warm-up: loaded {loaded} trust records from DB")
return loaded
# ==================== Pattern Matching Utilities ====================