Files
awoooi/apps/api/src/models/governance.py
Your Name 4cfc6a4c79
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
feat(governance): preview stale km completion batches
2026-05-24 23:15:03 +08:00

608 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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.01 - unresolved/total")
total_events: int
unresolved_count: int
daily_counts: list[DailyCount]