feat(governance): trace km stale ratio rechecks
All checks were successful
Code Review / ai-code-review (push) Successful in 10s
Type Sync Check / check-type-sync (push) Successful in 26s
CD Pipeline / tests (push) Successful in 3m34s
CD Pipeline / build-and-deploy (push) Successful in 4m2s
CD Pipeline / post-deploy-checks (push) Successful in 1m48s
All checks were successful
Code Review / ai-code-review (push) Successful in 10s
Type Sync Check / check-type-sync (push) Successful in 26s
CD Pipeline / tests (push) Successful in 3m34s
CD Pipeline / build-and-deploy (push) Successful in 4m2s
CD Pipeline / post-deploy-checks (push) Successful in 1m48s
This commit is contained in:
@@ -154,6 +154,14 @@ class KnowledgeReviewDraftArchiveRequest(BaseModel):
|
||||
dry_run: bool = False
|
||||
|
||||
|
||||
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
|
||||
@@ -169,6 +177,14 @@ class KnowledgeReviewDraftArchiveResponse(BaseModel):
|
||||
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
|
||||
next_action: str = "stale_ratio_recheck"
|
||||
generated_at: datetime
|
||||
|
||||
|
||||
@@ -13,10 +13,12 @@ Owner-approved operations for Hermes KM healthcheck review drafts.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Any, Literal
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.db.base import get_db_context
|
||||
from src.db.models import (
|
||||
@@ -29,8 +31,10 @@ from src.models.governance import (
|
||||
KnowledgeReviewDraftArchiveRequest,
|
||||
KnowledgeReviewDraftArchiveResponse,
|
||||
KnowledgeReviewDraftDedupeGroup,
|
||||
KnowledgeReviewDraftStaleRatioSnapshot,
|
||||
)
|
||||
from src.models.knowledge import EntryStatus, EntryType
|
||||
from src.services.governance_agent import KM_STALE_DAYS, KM_STALE_RATIO
|
||||
from src.services.governance_query_service import query_km_review_draft_dedupe
|
||||
from src.utils.timezone import now_taipei
|
||||
|
||||
@@ -105,9 +109,11 @@ async def archive_km_review_draft_duplicates(
|
||||
would_archive_entry_ids=duplicate_ids,
|
||||
writes_km=False,
|
||||
writes_governance_audit=False,
|
||||
stale_ratio_snapshot=await _load_current_km_stale_ratio_snapshot(),
|
||||
stale_ratio_recheck_status="dry_run",
|
||||
)
|
||||
|
||||
archived_ids, audit_dispatch_id = await _archive_duplicates_and_write_audit(
|
||||
archive_result = await _archive_duplicates_and_write_audit(
|
||||
governance_event_id=governance_event_id,
|
||||
request=request,
|
||||
duplicate_ids=duplicate_ids,
|
||||
@@ -118,10 +124,13 @@ async def archive_km_review_draft_duplicates(
|
||||
request=request,
|
||||
duplicate_ids=duplicate_ids,
|
||||
status="archived",
|
||||
archived_entry_ids=archived_ids,
|
||||
writes_km=bool(archived_ids),
|
||||
archived_entry_ids=archive_result["archived_ids"],
|
||||
writes_km=bool(archive_result["archived_ids"]),
|
||||
writes_governance_audit=True,
|
||||
audit_dispatch_id=audit_dispatch_id,
|
||||
audit_dispatch_id=archive_result["audit_dispatch_id"],
|
||||
stale_ratio_snapshot=archive_result["stale_ratio_snapshot"],
|
||||
stale_ratio_recheck_status=archive_result["recheck_status"],
|
||||
stale_ratio_recheck_dispatch_id=archive_result["recheck_dispatch_id"],
|
||||
)
|
||||
|
||||
|
||||
@@ -190,7 +199,7 @@ async def _archive_duplicates_and_write_audit(
|
||||
governance_event_id: str,
|
||||
request: KnowledgeReviewDraftArchiveRequest,
|
||||
duplicate_ids: list[str],
|
||||
) -> tuple[list[str], str]:
|
||||
) -> dict[str, Any]:
|
||||
"""Soft-archive duplicate rows and append a terminal audit dispatch."""
|
||||
now = now_taipei()
|
||||
async with get_db_context() as db:
|
||||
@@ -232,6 +241,16 @@ async def _archive_duplicates_and_write_audit(
|
||||
record.updated_at = now
|
||||
archived_ids.append(entry_id)
|
||||
|
||||
await db.flush()
|
||||
stale_ratio_snapshot = await _compute_km_stale_ratio_snapshot(db)
|
||||
recheck_dispatch_id, recheck_status = await _ensure_stale_ratio_recheck_dispatch(
|
||||
db,
|
||||
governance_event_id=governance_event_id,
|
||||
request=request,
|
||||
archived_ids=archived_ids,
|
||||
stale_ratio_snapshot=stale_ratio_snapshot,
|
||||
)
|
||||
|
||||
audit = GovernanceRemediationDispatch(
|
||||
id=generate_uuid(),
|
||||
governance_event_id=governance_event_id,
|
||||
@@ -241,6 +260,9 @@ async def _archive_duplicates_and_write_audit(
|
||||
governance_event_id=governance_event_id,
|
||||
request=request,
|
||||
archived_ids=archived_ids,
|
||||
stale_ratio_snapshot=stale_ratio_snapshot,
|
||||
recheck_dispatch_id=recheck_dispatch_id,
|
||||
recheck_status=recheck_status,
|
||||
),
|
||||
executor_type="hermes_km_review_dedupe_owner_archive",
|
||||
attempt_count=0,
|
||||
@@ -259,8 +281,16 @@ async def _archive_duplicates_and_write_audit(
|
||||
canonical_entry_id=request.canonical_entry_id,
|
||||
duplicate_count=len(archived_ids),
|
||||
audit_dispatch_id=audit.id,
|
||||
recheck_dispatch_id=recheck_dispatch_id,
|
||||
recheck_status=recheck_status,
|
||||
)
|
||||
return archived_ids, str(audit.id)
|
||||
return {
|
||||
"archived_ids": archived_ids,
|
||||
"audit_dispatch_id": str(audit.id),
|
||||
"stale_ratio_snapshot": stale_ratio_snapshot,
|
||||
"recheck_dispatch_id": recheck_dispatch_id,
|
||||
"recheck_status": recheck_status,
|
||||
}
|
||||
|
||||
|
||||
def _is_archive_candidate(
|
||||
@@ -303,6 +333,9 @@ def _build_archive_audit_context(
|
||||
governance_event_id: str,
|
||||
request: KnowledgeReviewDraftArchiveRequest,
|
||||
archived_ids: list[str],
|
||||
stale_ratio_snapshot: KnowledgeReviewDraftStaleRatioSnapshot | None = None,
|
||||
recheck_dispatch_id: str | None = None,
|
||||
recheck_status: str = "not_requested",
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"schema_version": "km_review_draft_archive_audit_v1",
|
||||
@@ -327,6 +360,14 @@ def _build_archive_audit_context(
|
||||
"canonical_entry_id": request.canonical_entry_id,
|
||||
"archived_entry_ids": archived_ids,
|
||||
"archived_count": len(archived_ids),
|
||||
"stale_ratio_snapshot": (
|
||||
stale_ratio_snapshot.model_dump() if stale_ratio_snapshot else None
|
||||
),
|
||||
"stale_ratio_recheck": {
|
||||
"status": recheck_status,
|
||||
"dispatch_id": recheck_dispatch_id,
|
||||
"executor_type": "hermes_km_stale_ratio_recheck",
|
||||
},
|
||||
"dry_run": request.dry_run,
|
||||
"owner_approved": request.owner_approved,
|
||||
}
|
||||
@@ -344,6 +385,14 @@ def _build_archive_response(
|
||||
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,
|
||||
) -> KnowledgeReviewDraftArchiveResponse:
|
||||
return KnowledgeReviewDraftArchiveResponse(
|
||||
governance_event_id=governance_event_id,
|
||||
@@ -359,9 +408,160 @@ def _build_archive_response(
|
||||
writes_km=writes_km,
|
||||
writes_governance_audit=writes_governance_audit,
|
||||
audit_dispatch_id=audit_dispatch_id,
|
||||
stale_ratio_snapshot=stale_ratio_snapshot,
|
||||
stale_ratio_recheck_status=stale_ratio_recheck_status,
|
||||
stale_ratio_recheck_dispatch_id=stale_ratio_recheck_dispatch_id,
|
||||
generated_at=now_taipei(),
|
||||
)
|
||||
|
||||
|
||||
def _enum_value(value: Any) -> str:
|
||||
return str(value.value if hasattr(value, "value") else value)
|
||||
|
||||
|
||||
async def _load_current_km_stale_ratio_snapshot() -> KnowledgeReviewDraftStaleRatioSnapshot:
|
||||
async with get_db_context() as db:
|
||||
return await _compute_km_stale_ratio_snapshot(db)
|
||||
|
||||
|
||||
async def _compute_km_stale_ratio_snapshot(
|
||||
db: AsyncSession,
|
||||
) -> KnowledgeReviewDraftStaleRatioSnapshot:
|
||||
"""Use the same KM stale definition as GovernanceAgent.check_knowledge_degradation."""
|
||||
stale_cutoff = now_taipei() - timedelta(days=KM_STALE_DAYS)
|
||||
total_result = await db.execute(
|
||||
select(func.count()).select_from(KnowledgeEntryRecord).where(
|
||||
KnowledgeEntryRecord.status != EntryStatus.ARCHIVED,
|
||||
)
|
||||
)
|
||||
total = int(total_result.scalar() or 0)
|
||||
|
||||
stale_result = await db.execute(
|
||||
select(func.count()).select_from(KnowledgeEntryRecord).where(
|
||||
KnowledgeEntryRecord.status != EntryStatus.ARCHIVED,
|
||||
KnowledgeEntryRecord.updated_at < stale_cutoff,
|
||||
)
|
||||
)
|
||||
stale = int(stale_result.scalar() or 0)
|
||||
ratio = round(stale / total, 3) if total > 0 else 0.0
|
||||
return KnowledgeReviewDraftStaleRatioSnapshot(
|
||||
stale_count=stale,
|
||||
total_count=total,
|
||||
stale_ratio=ratio,
|
||||
threshold=KM_STALE_RATIO,
|
||||
stale_days=KM_STALE_DAYS,
|
||||
)
|
||||
|
||||
|
||||
async def _ensure_stale_ratio_recheck_dispatch(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
governance_event_id: str,
|
||||
request: KnowledgeReviewDraftArchiveRequest,
|
||||
archived_ids: list[str],
|
||||
stale_ratio_snapshot: KnowledgeReviewDraftStaleRatioSnapshot,
|
||||
) -> tuple[str | None, Literal["completed", "already_active"]]:
|
||||
"""Record the post-archive recheck unless another active dispatch owns the event."""
|
||||
active_result = await db.execute(
|
||||
select(GovernanceRemediationDispatch)
|
||||
.where(
|
||||
GovernanceRemediationDispatch.governance_event_id == governance_event_id,
|
||||
GovernanceRemediationDispatch.dispatch_status.in_([
|
||||
"pending",
|
||||
"dispatched",
|
||||
"executing",
|
||||
]),
|
||||
)
|
||||
.order_by(GovernanceRemediationDispatch.dispatched_at.desc())
|
||||
.limit(1)
|
||||
)
|
||||
active = active_result.scalar_one_or_none()
|
||||
if active is not None:
|
||||
return str(active.id), "already_active"
|
||||
|
||||
recheck = GovernanceRemediationDispatch(
|
||||
id=generate_uuid(),
|
||||
governance_event_id=governance_event_id,
|
||||
event_type="knowledge_degradation",
|
||||
dispatch_status="succeeded",
|
||||
decision_context=_build_stale_ratio_recheck_context(
|
||||
governance_event_id=governance_event_id,
|
||||
request=request,
|
||||
archived_ids=archived_ids,
|
||||
stale_ratio_snapshot=stale_ratio_snapshot,
|
||||
),
|
||||
executor_type="hermes_km_stale_ratio_recheck",
|
||||
attempt_count=0,
|
||||
max_attempts=1,
|
||||
dispatched_at=taipei_now(),
|
||||
started_at=taipei_now(),
|
||||
completed_at=taipei_now(),
|
||||
created_by=request.owner[:100],
|
||||
)
|
||||
db.add(recheck)
|
||||
await db.flush()
|
||||
return str(recheck.id), "completed"
|
||||
|
||||
|
||||
def _build_stale_ratio_recheck_context(
|
||||
*,
|
||||
governance_event_id: str,
|
||||
request: KnowledgeReviewDraftArchiveRequest,
|
||||
archived_ids: list[str],
|
||||
stale_ratio_snapshot: KnowledgeReviewDraftStaleRatioSnapshot,
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"version": "v1",
|
||||
"trigger_source": "km_review_dedupe_archive",
|
||||
"triggered_metric": "knowledge_degradation",
|
||||
"metric_value": stale_ratio_snapshot.stale_ratio,
|
||||
"threshold": stale_ratio_snapshot.threshold,
|
||||
"suggested_action": "run_stale_ratio_recheck",
|
||||
"next_action": "run_stale_ratio_recheck",
|
||||
"decision_path": "owner_approved_recheck_after_archive",
|
||||
"ownership": {
|
||||
"lead_agent": "Hermes",
|
||||
"support_agents": [
|
||||
"OpenClaw:提供知識劣化風險脈絡,不直接批量改寫 KM。",
|
||||
"ElephantAlpha:read-only 稽核 stale ratio recheck 結果。",
|
||||
],
|
||||
"human_owner": "KM owner / SRE owner",
|
||||
},
|
||||
"workflow": {
|
||||
"work_item_id": f"governance:knowledge_degradation:{governance_event_id}:stale_ratio_recheck",
|
||||
"work_kind": "km_stale_ratio_recheck",
|
||||
"current_stage": "stale_ratio_recheck",
|
||||
"steps": [
|
||||
"detected",
|
||||
"draft_km_updates",
|
||||
"waiting_owner_review",
|
||||
"owner_approved_duplicate_archive",
|
||||
"stale_ratio_recheck",
|
||||
"km_governance_close_or_continue",
|
||||
],
|
||||
"stage_by_dispatch_status": {
|
||||
"pending": "stale_ratio_recheck",
|
||||
"dispatched": "stale_ratio_recheck",
|
||||
"executing": "stale_ratio_recheck",
|
||||
"succeeded": "km_governance_rechecked",
|
||||
"failed": "needs_manual_km_triage",
|
||||
"skipped": "needs_manual_km_triage",
|
||||
"cancelled": "cancelled",
|
||||
},
|
||||
"next_action": "run_stale_ratio_recheck",
|
||||
"writes_km_without_approval": False,
|
||||
"writes_km": False,
|
||||
"source_archive_action": "review_canonical_and_archive_duplicate_drafts",
|
||||
"canonical_entry_id": request.canonical_entry_id,
|
||||
"archived_entry_ids": archived_ids,
|
||||
"stale_ratio_snapshot": stale_ratio_snapshot.model_dump(),
|
||||
},
|
||||
"worker_result": {
|
||||
"status": "stale_ratio_rechecked",
|
||||
"canonical_entry_id": request.canonical_entry_id,
|
||||
"archived_count": len(archived_ids),
|
||||
"stale_ratio": stale_ratio_snapshot.stale_ratio,
|
||||
"threshold": stale_ratio_snapshot.threshold,
|
||||
"above_threshold": stale_ratio_snapshot.stale_ratio > stale_ratio_snapshot.threshold,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -33,10 +33,12 @@ from src.models.governance import (
|
||||
KnowledgeReviewDraftArchiveResponse,
|
||||
KnowledgeReviewDraftDedupeGroup,
|
||||
KnowledgeReviewDraftDedupeResponse,
|
||||
KnowledgeReviewDraftStaleRatioSnapshot,
|
||||
map_severity,
|
||||
)
|
||||
from src.services.governance_km_review_service import (
|
||||
KmReviewDraftArchiveError,
|
||||
_build_stale_ratio_recheck_context,
|
||||
_validate_archive_request_against_plan,
|
||||
)
|
||||
from src.services.governance_query_service import (
|
||||
@@ -510,6 +512,15 @@ class TestKmReviewDraftDedupe:
|
||||
writes_km=True,
|
||||
writes_governance_audit=True,
|
||||
audit_dispatch_id="dispatch-audit-001",
|
||||
stale_ratio_snapshot=KnowledgeReviewDraftStaleRatioSnapshot(
|
||||
stale_count=120,
|
||||
total_count=200,
|
||||
stale_ratio=0.6,
|
||||
threshold=0.2,
|
||||
stale_days=7,
|
||||
),
|
||||
stale_ratio_recheck_status="completed",
|
||||
stale_ratio_recheck_dispatch_id="dispatch-recheck-001",
|
||||
generated_at=NOW,
|
||||
)
|
||||
captured: dict = {}
|
||||
@@ -541,6 +552,9 @@ class TestKmReviewDraftDedupe:
|
||||
assert data["writes_km"] is True
|
||||
assert data["writes_governance_audit"] is True
|
||||
assert data["audit_dispatch_id"] == "dispatch-audit-001"
|
||||
assert data["stale_ratio_recheck_status"] == "completed"
|
||||
assert data["stale_ratio_recheck_dispatch_id"] == "dispatch-recheck-001"
|
||||
assert data["stale_ratio_snapshot"]["stale_ratio"] == pytest.approx(0.6)
|
||||
|
||||
def test_archive_endpoint_maps_validation_error_to_http(self, client):
|
||||
async def mock_archive(**kwargs):
|
||||
@@ -588,6 +602,38 @@ class TestKmReviewDraftDedupe:
|
||||
assert exc.value.status_code == 409
|
||||
assert "latest dedupe plan" in exc.value.detail
|
||||
|
||||
def test_stale_ratio_recheck_context_is_operator_visible(self):
|
||||
snapshot = KnowledgeReviewDraftStaleRatioSnapshot(
|
||||
stale_count=120,
|
||||
total_count=200,
|
||||
stale_ratio=0.6,
|
||||
threshold=0.2,
|
||||
stale_days=7,
|
||||
)
|
||||
request = KnowledgeReviewDraftArchiveRequest(
|
||||
canonical_entry_id="km-canonical",
|
||||
duplicate_entry_ids=["km-dup-1", "km-dup-2"],
|
||||
owner="operator_console",
|
||||
owner_approved=True,
|
||||
)
|
||||
|
||||
ctx = _build_stale_ratio_recheck_context(
|
||||
governance_event_id="event-001",
|
||||
request=request,
|
||||
archived_ids=["km-dup-1", "km-dup-2"],
|
||||
stale_ratio_snapshot=snapshot,
|
||||
)
|
||||
|
||||
assert ctx["next_action"] == "run_stale_ratio_recheck"
|
||||
assert ctx["decision_path"] == "owner_approved_recheck_after_archive"
|
||||
assert ctx["workflow"]["work_kind"] == "km_stale_ratio_recheck"
|
||||
assert ctx["workflow"]["current_stage"] == "stale_ratio_recheck"
|
||||
assert ctx["workflow"]["stage_by_dispatch_status"]["pending"] == "stale_ratio_recheck"
|
||||
assert ctx["workflow"]["writes_km"] is False
|
||||
assert ctx["workflow"]["stale_ratio_snapshot"]["stale_ratio"] == pytest.approx(0.6)
|
||||
assert ctx["worker_result"]["status"] == "stale_ratio_rechecked"
|
||||
assert ctx["worker_result"]["above_threshold"] is True
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 4. summary endpoint compliance_rate
|
||||
|
||||
@@ -1925,11 +1925,20 @@
|
||||
"failed": "Archive action failed; refresh and verify the latest dedupe plan.",
|
||||
"requiresOwner": "Owner review required; backend rechecks the latest plan.",
|
||||
"result": "Archived {archived}; audit dispatch: {audit}",
|
||||
"recheck": "Stale-ratio recheck: {status}; dispatch: {dispatch}",
|
||||
"snapshot": "Current stale {stale} / total {total}; ratio {ratio}; threshold {threshold}",
|
||||
"statuses": {
|
||||
"dry_run": "Dry run complete",
|
||||
"archived": "Archived",
|
||||
"noop_already_archived": "Already archived",
|
||||
"unknown": "Status pending"
|
||||
},
|
||||
"recheckStatuses": {
|
||||
"dry_run": "Dry run only",
|
||||
"completed": "Completed",
|
||||
"already_active": "Already active",
|
||||
"not_requested": "Not requested",
|
||||
"unknown": "Status pending"
|
||||
}
|
||||
},
|
||||
"statuses": {
|
||||
@@ -1950,6 +1959,10 @@
|
||||
"waiting_owner_review": "Waiting owner review",
|
||||
"km_writeback_after_approval": "KM writeback after approval",
|
||||
"stale_ratio_recheck": "Stale-ratio recheck",
|
||||
"owner_approved_duplicate_archive": "Owner approved duplicate archive",
|
||||
"km_duplicate_archive_after_owner_approval": "Duplicate archive after owner review",
|
||||
"km_governance_rechecked": "KM governance rechecked",
|
||||
"km_governance_close_or_continue": "Close or continue governance",
|
||||
"needs_manual_km_triage": "Manual KM triage needed",
|
||||
"cancelled": "Cancelled",
|
||||
"queued_for_review": "Queued for governance review",
|
||||
|
||||
@@ -1926,11 +1926,20 @@
|
||||
"failed": "封存動作失敗;請重新整理後確認最新 dedupe plan。",
|
||||
"requiresOwner": "需要 owner 審核;後端會重新比對最新 plan。",
|
||||
"result": "已封存 {archived} 份;稽核 dispatch:{audit}",
|
||||
"recheck": "Stale ratio 回測:{status};dispatch:{dispatch}",
|
||||
"snapshot": "目前 stale {stale} / total {total};ratio {ratio};門檻 {threshold}",
|
||||
"statuses": {
|
||||
"dry_run": "乾跑完成",
|
||||
"archived": "封存完成",
|
||||
"noop_already_archived": "已封存,無需重複處理",
|
||||
"unknown": "狀態待確認"
|
||||
},
|
||||
"recheckStatuses": {
|
||||
"dry_run": "乾跑未排程",
|
||||
"completed": "已完成回測",
|
||||
"already_active": "已有活躍回測",
|
||||
"not_requested": "尚未建立",
|
||||
"unknown": "狀態待確認"
|
||||
}
|
||||
},
|
||||
"statuses": {
|
||||
@@ -1951,6 +1960,10 @@
|
||||
"waiting_owner_review": "等待 owner 審核",
|
||||
"km_writeback_after_approval": "審核後寫回 KM",
|
||||
"stale_ratio_recheck": "回測 stale ratio",
|
||||
"owner_approved_duplicate_archive": "Owner 已批准封存重複草稿",
|
||||
"km_duplicate_archive_after_owner_approval": "Owner 審核後封存重複草稿",
|
||||
"km_governance_rechecked": "KM 治理已回測",
|
||||
"km_governance_close_or_continue": "關閉或繼續治理",
|
||||
"needs_manual_km_triage": "需要人工整理 KM",
|
||||
"cancelled": "已取消",
|
||||
"queued_for_review": "等待治理審核",
|
||||
|
||||
@@ -290,6 +290,19 @@ type KnowledgeReviewDraftArchiveResponse = {
|
||||
writes_km: boolean;
|
||||
writes_governance_audit: boolean;
|
||||
audit_dispatch_id?: string | null;
|
||||
stale_ratio_snapshot?: {
|
||||
stale_count: number;
|
||||
total_count: number;
|
||||
stale_ratio: number;
|
||||
threshold: number;
|
||||
stale_days: number;
|
||||
} | null;
|
||||
stale_ratio_recheck_status:
|
||||
| "dry_run"
|
||||
| "completed"
|
||||
| "already_active"
|
||||
| "not_requested";
|
||||
stale_ratio_recheck_dispatch_id?: string | null;
|
||||
next_action: string;
|
||||
generated_at?: string | null;
|
||||
};
|
||||
@@ -666,6 +679,10 @@ function governanceKmStageKey(stage?: string | null) {
|
||||
stage === "waiting_owner_review" ||
|
||||
stage === "km_writeback_after_approval" ||
|
||||
stage === "stale_ratio_recheck" ||
|
||||
stage === "owner_approved_duplicate_archive" ||
|
||||
stage === "km_duplicate_archive_after_owner_approval" ||
|
||||
stage === "km_governance_rechecked" ||
|
||||
stage === "km_governance_close_or_continue" ||
|
||||
stage === "needs_manual_km_triage" ||
|
||||
stage === "cancelled" ||
|
||||
stage === "queued_for_review" ||
|
||||
@@ -771,6 +788,18 @@ function groupArchiveStatusKey(status?: string | null) {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function staleRatioRecheckStatusKey(status?: string | null) {
|
||||
if (
|
||||
status === "dry_run" ||
|
||||
status === "completed" ||
|
||||
status === "already_active" ||
|
||||
status === "not_requested"
|
||||
) {
|
||||
return status;
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function buildWorkItems(
|
||||
telemetry: Telemetry,
|
||||
t: ReturnType<typeof useTranslations>
|
||||
@@ -1717,6 +1746,26 @@ function KnowledgeGovernancePanel({
|
||||
audit: archiveAction.result.audit_dispatch_id ?? "--",
|
||||
})}
|
||||
</p>
|
||||
<p className="mt-1 text-[#5f5b52]">
|
||||
{t("archiveActions.recheck", {
|
||||
status: t(
|
||||
`archiveActions.recheckStatuses.${staleRatioRecheckStatusKey(
|
||||
archiveAction.result.stale_ratio_recheck_status
|
||||
)}` as never
|
||||
),
|
||||
dispatch: archiveAction.result.stale_ratio_recheck_dispatch_id ?? "--",
|
||||
})}
|
||||
</p>
|
||||
{archiveAction.result.stale_ratio_snapshot ? (
|
||||
<p className="mt-1 text-[#5f5b52]">
|
||||
{t("archiveActions.snapshot", {
|
||||
stale: archiveAction.result.stale_ratio_snapshot.stale_count,
|
||||
total: archiveAction.result.stale_ratio_snapshot.total_count,
|
||||
ratio: archiveAction.result.stale_ratio_snapshot.stale_ratio,
|
||||
threshold: archiveAction.result.stale_ratio_snapshot.threshold,
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user