All checks were successful
CD Pipeline / tests (push) Successful in 1m8s
Code Review / ai-code-review (push) Successful in 11s
Type Sync Check / check-type-sync (push) Successful in 26s
CD Pipeline / build-and-deploy (push) Successful in 4m9s
CD Pipeline / post-deploy-checks (push) Successful in 1m33s
608 lines
20 KiB
Python
608 lines
20 KiB
Python
"""
|
||
Governance API Models — /governance 頁面 Pydantic Schemas
|
||
=========================================================
|
||
PR 1 後端 3 endpoint 的 request/response schema.
|
||
|
||
Endpoints:
|
||
GET /api/v1/ai/governance/events — ai_governance_events 查詢
|
||
GET /api/v1/ai/governance/queue — governance_remediation_dispatch 隊列(Track D 依賴表)
|
||
GET /api/v1/ai/governance/summary — 30d SLO 違反時序統計
|
||
|
||
設計原則:
|
||
- Pydantic V2,對齊 models/ 目錄
|
||
- Severity 映射邏輯集中於此,Router / Service 直接用
|
||
- 禁止硬編碼 IP 或內網位址
|
||
|
||
2026-05-02 ogt + Claude Sonnet 4.6 Asia/Taipei
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime
|
||
from typing import Literal
|
||
|
||
from pydantic import BaseModel, Field
|
||
|
||
# =============================================================================
|
||
# Severity 映射
|
||
# =============================================================================
|
||
|
||
# critical: slo_violation / conservative_mode / governance_slo_data_gap
|
||
# warning: trust_drift / kb_stale / knowledge_degradation / execution_blast_radius
|
||
# info: 其他(含 replay_degraded / self_demotion / llm_hallucination 等)
|
||
|
||
_CRITICAL_TYPES: frozenset[str] = frozenset({
|
||
"slo_violation",
|
||
"conservative_mode",
|
||
"governance_slo_data_gap",
|
||
})
|
||
|
||
_WARNING_TYPES: frozenset[str] = frozenset({
|
||
"trust_drift",
|
||
"kb_stale",
|
||
"knowledge_degradation",
|
||
"execution_blast_radius",
|
||
})
|
||
|
||
|
||
def map_severity(event_type: str) -> Literal["critical", "warning", "info"]:
|
||
"""將 event_type 映射為 severity 等級."""
|
||
if event_type in _CRITICAL_TYPES:
|
||
return "critical"
|
||
if event_type in _WARNING_TYPES:
|
||
return "warning"
|
||
return "info"
|
||
|
||
|
||
# =============================================================================
|
||
# Endpoint 1: events
|
||
# =============================================================================
|
||
|
||
class GovernanceEvent(BaseModel):
|
||
id: str
|
||
event_type: str
|
||
severity: Literal["critical", "warning", "info"]
|
||
triggered_at: datetime
|
||
resolved: bool
|
||
resolved_at: datetime | None = None
|
||
impact: str = Field(description="≤80 字摘要,從 details 抽取")
|
||
details: dict
|
||
remediation: str | None = None
|
||
dispatch_ids: list[str] = Field(default_factory=list)
|
||
|
||
|
||
class GovernanceEventsResponse(BaseModel):
|
||
items: list[GovernanceEvent]
|
||
total: int
|
||
page: int
|
||
size: int
|
||
|
||
|
||
# =============================================================================
|
||
# Endpoint 2: queue
|
||
# =============================================================================
|
||
|
||
class DispatchItem(BaseModel):
|
||
id: str
|
||
governance_event_id: str
|
||
event_type: str
|
||
dispatch_status: str
|
||
executor_type: str | None = None
|
||
proposed_action: str = Field(description="≤120 字動作摘要")
|
||
playbook_id: str | None = None
|
||
playbook_trust: float | None = Field(default=None, ge=0.0, le=1.0)
|
||
created_at: datetime
|
||
dispatched_at: datetime | None = None
|
||
started_at: datetime | None = None
|
||
completed_at: datetime | None = None
|
||
operator_note: str | None = None
|
||
decision_path: str | None = None
|
||
workflow_stage: str | None = None
|
||
workflow_steps: list[str] = Field(default_factory=list)
|
||
next_action: str | None = None
|
||
lead_agent: str | None = None
|
||
support_agents: list[str] = Field(default_factory=list)
|
||
human_owner: str | None = None
|
||
kb_draft_entry_id: str | None = None
|
||
worker_status: str | None = None
|
||
dry_run_plan_fingerprint: str | None = None
|
||
archived_count: int | None = None
|
||
stale_ratio_snapshot: dict | None = None
|
||
|
||
|
||
class GovernanceQueueResponse(BaseModel):
|
||
items: list[DispatchItem]
|
||
total: int
|
||
page: int
|
||
size: int
|
||
table_pending: bool = Field(
|
||
default=False,
|
||
description="dispatch 表尚未建立時為 True",
|
||
)
|
||
|
||
|
||
# =============================================================================
|
||
# Endpoint 2B: KM review draft dedupe
|
||
# =============================================================================
|
||
|
||
class KnowledgeReviewDraftDedupeGroup(BaseModel):
|
||
governance_event_id: str
|
||
canonical_entry_id: str
|
||
canonical_title: str
|
||
canonical_updated_at: datetime | None = None
|
||
preferred_source: Literal["dispatch_context", "latest_review_draft"]
|
||
duplicate_entry_ids: list[str] = Field(default_factory=list)
|
||
duplicate_count: int
|
||
total_entries: int
|
||
suggested_action: str
|
||
owner_action: str
|
||
writes_on_read: bool = False
|
||
can_archive_without_owner_approval: bool = False
|
||
archive_history: list[DispatchItem] = Field(default_factory=list)
|
||
|
||
|
||
class KnowledgeReviewDraftDedupeResponse(BaseModel):
|
||
schema_version: str = "km_review_draft_dedupe_v1"
|
||
total_review_drafts: int
|
||
event_group_total: int
|
||
duplicate_draft_total: int
|
||
groups: list[KnowledgeReviewDraftDedupeGroup]
|
||
generated_at: datetime
|
||
|
||
|
||
class KnowledgeReviewDraftArchiveRequest(BaseModel):
|
||
canonical_entry_id: str = Field(min_length=1, max_length=120)
|
||
duplicate_entry_ids: list[str] = Field(min_length=1, max_length=100)
|
||
owner: str = Field(default="operator_console", min_length=1, max_length=100)
|
||
owner_approved: bool = False
|
||
dry_run: bool = False
|
||
dry_run_plan_fingerprint: str | None = Field(
|
||
default=None,
|
||
max_length=80,
|
||
description="Dry-run response fingerprint that must be echoed before a write.",
|
||
)
|
||
|
||
|
||
class KnowledgeReviewDraftStaleRatioSnapshot(BaseModel):
|
||
stale_count: int
|
||
total_count: int
|
||
stale_ratio: float
|
||
threshold: float
|
||
stale_days: int
|
||
|
||
|
||
class KnowledgeReviewDraftArchiveResponse(BaseModel):
|
||
schema_version: str = "km_review_draft_archive_v1"
|
||
governance_event_id: str
|
||
canonical_entry_id: str
|
||
requested_duplicate_entry_ids: list[str]
|
||
archived_entry_ids: list[str] = Field(default_factory=list)
|
||
skipped_entry_ids: list[str] = Field(default_factory=list)
|
||
would_archive_entry_ids: list[str] = Field(default_factory=list)
|
||
status: Literal["dry_run", "archived", "noop_already_archived"]
|
||
owner: str
|
||
owner_approved: bool
|
||
dry_run: bool
|
||
writes_km: bool
|
||
writes_governance_audit: bool
|
||
audit_dispatch_id: str | None = None
|
||
stale_ratio_snapshot: KnowledgeReviewDraftStaleRatioSnapshot | None = None
|
||
stale_ratio_recheck_status: Literal[
|
||
"dry_run",
|
||
"completed",
|
||
"already_active",
|
||
"not_requested",
|
||
] = "not_requested"
|
||
stale_ratio_recheck_dispatch_id: str | None = None
|
||
dry_run_plan_fingerprint: str | None = None
|
||
next_action: str = "stale_ratio_recheck"
|
||
generated_at: datetime
|
||
|
||
|
||
# =============================================================================
|
||
# Endpoint 2C: KM stale candidates
|
||
# =============================================================================
|
||
|
||
class KnowledgeStaleCandidate(BaseModel):
|
||
entry_id: str
|
||
project_id: str
|
||
title: str
|
||
entry_type: str
|
||
category: str | None = None
|
||
status: str
|
||
source: str | None = None
|
||
updated_at: datetime | None = None
|
||
stale_days: int
|
||
view_count: int
|
||
priority_score: int
|
||
priority_tier: Literal["P0", "P1", "P2"]
|
||
recommended_action: Literal[
|
||
"refresh_with_evidence",
|
||
"owner_review",
|
||
"archive_or_supersede",
|
||
]
|
||
reasons: list[str] = Field(default_factory=list)
|
||
correlation_sources: list[str] = Field(default_factory=list)
|
||
related_incident_id: str | None = None
|
||
related_playbook_id: str | None = None
|
||
related_approval_id: str | None = None
|
||
tags: list[str] = Field(default_factory=list)
|
||
owner_review_dispatch_id: str | None = None
|
||
owner_review_status: str | None = None
|
||
owner_review_stage: str | None = None
|
||
owner_review_next_action: str | None = None
|
||
|
||
|
||
class KnowledgeStaleCandidatesResponse(BaseModel):
|
||
schema_version: str = "km_stale_candidates_v1"
|
||
project_id: str
|
||
total_stale: int
|
||
returned: int
|
||
threshold_days: int
|
||
writes_on_read: bool = False
|
||
manual_review_required: bool = True
|
||
items: list[KnowledgeStaleCandidate]
|
||
generated_at: datetime
|
||
|
||
|
||
class KnowledgeStaleOwnerReviewRequest(BaseModel):
|
||
owner: str = Field(default="operator_console", min_length=1, max_length=100)
|
||
owner_note: str | None = Field(default=None, max_length=240)
|
||
dry_run: bool = False
|
||
|
||
|
||
class KnowledgeStaleOwnerReviewResponse(BaseModel):
|
||
schema_version: str = "km_stale_owner_review_v1"
|
||
entry_id: str
|
||
project_id: str
|
||
status: Literal["dry_run", "queued", "already_queued"]
|
||
governance_event_id: str | None = None
|
||
dispatch_id: str | None = None
|
||
workflow_stage: str
|
||
recommended_action: Literal[
|
||
"refresh_with_evidence",
|
||
"owner_review",
|
||
"archive_or_supersede",
|
||
]
|
||
owner: str
|
||
owner_note: str | None = None
|
||
writes_km: bool = False
|
||
writes_governance_audit: bool
|
||
next_action: str = "owner_review_stale_km_candidate"
|
||
generated_at: datetime
|
||
|
||
|
||
class KnowledgeStaleOwnerReviewBatchQueueRequest(BaseModel):
|
||
project_id: str = Field(default="awoooi", min_length=1, max_length=64)
|
||
priority_tiers: list[Literal["P0", "P1", "P2"]] = Field(
|
||
default_factory=lambda: ["P0", "P1"],
|
||
min_length=1,
|
||
max_length=3,
|
||
)
|
||
limit: int = Field(default=10, ge=1, le=50)
|
||
owner: str = Field(default="operator_console", min_length=1, max_length=100)
|
||
owner_note: str | None = Field(default=None, max_length=240)
|
||
dry_run: bool = False
|
||
dry_run_plan_fingerprint: str | None = Field(
|
||
default=None,
|
||
max_length=80,
|
||
description="Dry-run response fingerprint that must be echoed before queueing a batch.",
|
||
)
|
||
|
||
|
||
class KnowledgeStaleOwnerReviewBatchItem(BaseModel):
|
||
entry_id: str
|
||
title: str
|
||
priority_tier: Literal["P0", "P1", "P2"]
|
||
recommended_action: Literal[
|
||
"refresh_with_evidence",
|
||
"owner_review",
|
||
"archive_or_supersede",
|
||
]
|
||
status: Literal["would_queue", "queued", "already_queued", "skipped"]
|
||
reason: str | None = None
|
||
governance_event_id: str | None = None
|
||
dispatch_id: str | None = None
|
||
workflow_stage: str
|
||
|
||
|
||
class KnowledgeStaleOwnerReviewBatchQueueResponse(BaseModel):
|
||
schema_version: str = "km_stale_owner_review_batch_v1"
|
||
project_id: str
|
||
status: Literal["dry_run", "queued", "noop_already_queued"]
|
||
owner: str
|
||
owner_note: str | None = None
|
||
dry_run: bool
|
||
priority_tiers: list[str]
|
||
requested_limit: int
|
||
candidate_count: int
|
||
queued_count: int
|
||
already_queued_count: int
|
||
skipped_count: int
|
||
batch_governance_event_id: str | None = None
|
||
batch_dispatch_id: str | None = None
|
||
workflow_stage: str
|
||
writes_km: bool = False
|
||
writes_governance_audit: bool
|
||
stale_ratio_snapshot: KnowledgeReviewDraftStaleRatioSnapshot | None = None
|
||
dry_run_plan_fingerprint: str | None = None
|
||
items: list[KnowledgeStaleOwnerReviewBatchItem] = Field(default_factory=list)
|
||
next_action: str = "owner_review_stale_km_batch"
|
||
generated_at: datetime
|
||
|
||
|
||
class KnowledgeStaleOwnerReviewInboxItem(BaseModel):
|
||
dispatch_id: str
|
||
governance_event_id: str
|
||
entry_id: str
|
||
project_id: str
|
||
title: str
|
||
dispatch_status: str
|
||
workflow_stage: str
|
||
next_action: str | None = None
|
||
owner: str | None = None
|
||
owner_note: str | None = None
|
||
batch_governance_event_id: str | None = None
|
||
batch_dispatch_id: str | None = None
|
||
priority_tier: Literal["P0", "P1", "P2"]
|
||
priority_score: int
|
||
recommended_action: Literal[
|
||
"refresh_with_evidence",
|
||
"owner_review",
|
||
"archive_or_supersede",
|
||
]
|
||
stale_days: int
|
||
view_count: int
|
||
correlation_sources: list[str] = Field(default_factory=list)
|
||
reasons: list[str] = Field(default_factory=list)
|
||
related_incident_id: str | None = None
|
||
related_playbook_id: str | None = None
|
||
related_approval_id: str | None = None
|
||
dry_run_plan_fingerprint: str | None = None
|
||
queued_at: datetime | None = None
|
||
started_at: datetime | None = None
|
||
completed_at: datetime | None = None
|
||
|
||
|
||
class KnowledgeStaleOwnerReviewInboxResponse(BaseModel):
|
||
schema_version: str = "km_stale_owner_review_inbox_v1"
|
||
project_id: str
|
||
dispatch_status: str
|
||
total: int
|
||
returned: int
|
||
writes_on_read: bool = False
|
||
manual_review_required: bool = True
|
||
items: list[KnowledgeStaleOwnerReviewInboxItem] = Field(default_factory=list)
|
||
generated_at: datetime
|
||
|
||
|
||
class KnowledgeStaleOwnerReviewBurnDownItem(BaseModel):
|
||
completion_dispatch_id: str
|
||
governance_event_id: str
|
||
source_dispatch_id: str | None = None
|
||
recheck_dispatch_id: str | None = None
|
||
entry_id: str | None = None
|
||
project_id: str
|
||
dispatch_status: str
|
||
workflow_stage: str
|
||
review_outcome: Literal[
|
||
"refresh_with_evidence",
|
||
"archive",
|
||
"supersede",
|
||
] | None = None
|
||
owner: str | None = None
|
||
completed_at: datetime | None = None
|
||
stale_ratio_snapshot: KnowledgeReviewDraftStaleRatioSnapshot | None = None
|
||
stale_count_delta: int | None = None
|
||
stale_ratio_delta: float | None = None
|
||
above_threshold: bool | None = None
|
||
|
||
|
||
class KnowledgeStaleOwnerReviewBurnDownResponse(BaseModel):
|
||
schema_version: str = "km_stale_owner_review_burndown_v1"
|
||
project_id: str
|
||
burn_down_status: Literal["above_threshold", "at_or_below_threshold", "no_data"]
|
||
current_snapshot: KnowledgeReviewDraftStaleRatioSnapshot | None = None
|
||
entries_to_threshold: int
|
||
pending_owner_reviews: int
|
||
completed_owner_reviews: int
|
||
completion_audit_total: int
|
||
stale_ratio_recheck_total: int
|
||
latest_stale_count_delta: int | None = None
|
||
latest_stale_ratio_delta: float | None = None
|
||
writes_on_read: bool = False
|
||
manual_review_required: bool = True
|
||
returned: int
|
||
items: list[KnowledgeStaleOwnerReviewBurnDownItem] = Field(default_factory=list)
|
||
generated_at: datetime
|
||
|
||
|
||
class KnowledgeStaleOwnerReviewCompletionQueueItem(BaseModel):
|
||
dispatch_id: str
|
||
governance_event_id: str
|
||
entry_id: str
|
||
project_id: str
|
||
title: str
|
||
dispatch_status: str
|
||
workflow_stage: str
|
||
readiness: Literal["ready", "blocked", "completed", "failed"]
|
||
recommended_completion_outcome: Literal[
|
||
"refresh_with_evidence",
|
||
"archive",
|
||
"supersede",
|
||
]
|
||
next_action: str
|
||
blockers: list[str] = Field(default_factory=list)
|
||
required_owner_fields: list[str] = Field(default_factory=list)
|
||
can_preview: bool
|
||
can_confirm_after_preview: bool
|
||
writes_km_on_confirm: bool
|
||
owner: str | None = None
|
||
owner_note: str | None = None
|
||
batch_governance_event_id: str | None = None
|
||
batch_dispatch_id: str | None = None
|
||
priority_tier: Literal["P0", "P1", "P2"]
|
||
priority_score: int
|
||
recommended_action: Literal[
|
||
"refresh_with_evidence",
|
||
"owner_review",
|
||
"archive_or_supersede",
|
||
]
|
||
stale_days: int
|
||
view_count: int
|
||
correlation_sources: list[str] = Field(default_factory=list)
|
||
reasons: list[str] = Field(default_factory=list)
|
||
related_incident_id: str | None = None
|
||
related_playbook_id: str | None = None
|
||
related_approval_id: str | None = None
|
||
dry_run_plan_fingerprint: str | None = None
|
||
queued_at: datetime | None = None
|
||
started_at: datetime | None = None
|
||
completed_at: datetime | None = None
|
||
|
||
|
||
class KnowledgeStaleOwnerReviewCompletionQueueResponse(BaseModel):
|
||
schema_version: str = "km_stale_owner_review_completion_queue_v1"
|
||
project_id: str
|
||
status_bucket: Literal["all", "ready", "blocked", "completed", "failed", "pending"]
|
||
priority_tiers: list[str] = Field(default_factory=list)
|
||
recommended_completion_outcome: Literal[
|
||
"all",
|
||
"refresh_with_evidence",
|
||
"archive",
|
||
"supersede",
|
||
] = "all"
|
||
batch_governance_event_id: str | None = None
|
||
can_preview: bool | None = None
|
||
total: int
|
||
returned: int
|
||
pending_count: int
|
||
ready_count: int
|
||
blocked_count: int
|
||
completed_count: int
|
||
failed_count: int
|
||
writes_on_read: bool = False
|
||
manual_review_required: bool = True
|
||
batch_writes_allowed: bool = False
|
||
items: list[KnowledgeStaleOwnerReviewCompletionQueueItem] = Field(default_factory=list)
|
||
generated_at: datetime
|
||
|
||
|
||
class KnowledgeStaleOwnerReviewCompletionBatchPreviewRequest(BaseModel):
|
||
project_id: str = Field(default="awoooi", min_length=1, max_length=64)
|
||
status_bucket: Literal["all", "ready", "blocked", "completed", "failed", "pending"] = "ready"
|
||
priority_tiers: list[Literal["P0", "P1", "P2"]] = Field(
|
||
default_factory=lambda: ["P0", "P1", "P2"],
|
||
min_length=1,
|
||
max_length=3,
|
||
)
|
||
recommended_completion_outcome: Literal[
|
||
"all",
|
||
"refresh_with_evidence",
|
||
"archive",
|
||
"supersede",
|
||
] = "all"
|
||
batch_governance_event_id: str | None = Field(default=None, max_length=120)
|
||
limit: int = Field(default=10, ge=1, le=30)
|
||
owner: str = Field(default="operator_console", min_length=1, max_length=100)
|
||
owner_note: str | None = Field(default=None, max_length=240)
|
||
|
||
|
||
class KnowledgeStaleOwnerReviewCompletionBatchPreviewResponse(BaseModel):
|
||
schema_version: str = "km_stale_owner_review_completion_batch_preview_v1"
|
||
project_id: str
|
||
status: Literal["dry_run"] = "dry_run"
|
||
owner: str
|
||
owner_note: str | None = None
|
||
status_bucket: Literal["all", "ready", "blocked", "completed", "failed", "pending"]
|
||
priority_tiers: list[str]
|
||
recommended_completion_outcome: Literal[
|
||
"all",
|
||
"refresh_with_evidence",
|
||
"archive",
|
||
"supersede",
|
||
]
|
||
batch_governance_event_id: str | None = None
|
||
requested_limit: int
|
||
candidate_count: int
|
||
previewable_count: int
|
||
blocked_count: int
|
||
completed_count: int
|
||
failed_count: int
|
||
writes_km: bool = False
|
||
writes_governance_audit: bool = False
|
||
batch_writes_allowed: bool = False
|
||
manual_review_required: bool = True
|
||
dry_run_plan_fingerprint: str
|
||
next_action: str = "preview_each_ready_item_then_confirm_single_item"
|
||
items: list[KnowledgeStaleOwnerReviewCompletionQueueItem] = Field(default_factory=list)
|
||
generated_at: datetime
|
||
|
||
|
||
class KnowledgeStaleOwnerReviewCompleteRequest(BaseModel):
|
||
dispatch_id: str | None = Field(
|
||
default=None,
|
||
max_length=120,
|
||
description="Owner-review dispatch id. Optional when the backend can resolve the active item by entry id.",
|
||
)
|
||
owner: str = Field(default="operator_console", min_length=1, max_length=100)
|
||
owner_approved: bool = False
|
||
dry_run: bool = False
|
||
review_outcome: Literal[
|
||
"refresh_with_evidence",
|
||
"archive",
|
||
"supersede",
|
||
]
|
||
owner_note: str | None = Field(default=None, max_length=500)
|
||
updated_title: str | None = Field(default=None, min_length=1, max_length=255)
|
||
updated_content: str | None = Field(default=None, min_length=1)
|
||
superseded_by_entry_id: str | None = Field(default=None, max_length=120)
|
||
dry_run_plan_fingerprint: str | None = Field(
|
||
default=None,
|
||
max_length=80,
|
||
description="Dry-run response fingerprint that must be echoed before a write.",
|
||
)
|
||
|
||
|
||
class KnowledgeStaleOwnerReviewCompleteResponse(BaseModel):
|
||
schema_version: str = "km_stale_owner_review_complete_v1"
|
||
entry_id: str
|
||
project_id: str
|
||
status: Literal["dry_run", "completed", "already_completed"]
|
||
review_outcome: Literal[
|
||
"refresh_with_evidence",
|
||
"archive",
|
||
"supersede",
|
||
]
|
||
governance_event_id: str
|
||
dispatch_id: str
|
||
audit_dispatch_id: str | None = None
|
||
stale_ratio_recheck_dispatch_id: str | None = None
|
||
workflow_stage: str
|
||
owner: str
|
||
owner_approved: bool
|
||
dry_run: bool
|
||
writes_km: bool
|
||
writes_governance_audit: bool
|
||
stale_ratio_snapshot: KnowledgeReviewDraftStaleRatioSnapshot | None = None
|
||
dry_run_plan_fingerprint: str | None = None
|
||
next_action: str = "stale_ratio_recheck"
|
||
generated_at: datetime
|
||
|
||
|
||
# =============================================================================
|
||
# Endpoint 3: summary
|
||
# =============================================================================
|
||
|
||
class DailyCount(BaseModel):
|
||
date: str = Field(description="YYYY-MM-DD")
|
||
total: int
|
||
by_type: dict[str, int] = Field(description="{event_type: count}")
|
||
|
||
|
||
class GovernanceSummaryResponse(BaseModel):
|
||
compliance_rate: float = Field(description="0.0-1.0,1 - unresolved/total")
|
||
total_events: int
|
||
unresolved_count: int
|
||
daily_counts: list[DailyCount]
|