diff --git a/apps/api/src/models/governance.py b/apps/api/src/models/governance.py index 1370fcbd..cad90ca2 100644 --- a/apps/api/src/models/governance.py +++ b/apps/api/src/models/governance.py @@ -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 diff --git a/apps/api/src/services/governance_km_review_service.py b/apps/api/src/services/governance_km_review_service.py index 3fd4be67..132bc292 100644 --- a/apps/api/src/services/governance_km_review_service.py +++ b/apps/api/src/services/governance_km_review_service.py @@ -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, + }, + } diff --git a/apps/api/tests/test_ai_governance_endpoints.py b/apps/api/tests/test_ai_governance_endpoints.py index 1a75c0ab..a993bfb3 100644 --- a/apps/api/tests/test_ai_governance_endpoints.py +++ b/apps/api/tests/test_ai_governance_endpoints.py @@ -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 diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 75239cbd..e58a320a 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -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", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 5757a483..59dda199 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -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": "等待治理審核", diff --git a/apps/web/src/app/[locale]/awooop/work-items/page.tsx b/apps/web/src/app/[locale]/awooop/work-items/page.tsx index 08d282f3..7c2763cf 100644 --- a/apps/web/src/app/[locale]/awooop/work-items/page.tsx +++ b/apps/web/src/app/[locale]/awooop/work-items/page.tsx @@ -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 @@ -1717,6 +1746,26 @@ function KnowledgeGovernancePanel({ audit: archiveAction.result.audit_dispatch_id ?? "--", })}

+

+ {t("archiveActions.recheck", { + status: t( + `archiveActions.recheckStatuses.${staleRatioRecheckStatusKey( + archiveAction.result.stale_ratio_recheck_status + )}` as never + ), + dispatch: archiveAction.result.stale_ratio_recheck_dispatch_id ?? "--", + })} +

+ {archiveAction.result.stale_ratio_snapshot ? ( +

+ {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, + })} +

+ ) : null} ) : null} diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 9bb3ef0f..7a3c7e96 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,66 @@ +## 2026-05-20|T94 KM archive 後 stale ratio recheck trace + +**觸發**: + +- T93 已讓 owner 可在 AwoooP 封存 duplicate KM review drafts,且會寫 archive audit dispatch。 +- 但封存完成後仍需要看見下一段治理驗證:KM stale ratio 是否真的下降、是否仍高於門檻、是否要繼續 owner review。 + +**修正**: + +- `POST /api/v1/ai/governance/km-review-drafts/dedupe/{governance_event_id}/archive-duplicates` response 新增: + - `stale_ratio_snapshot`:沿用 `GovernanceAgent.check_knowledge_degradation` 的定義,計算 non-archived KM 的 `stale_count / total_count / stale_ratio / threshold / stale_days`。 + - `stale_ratio_recheck_status`:`dry_run / completed / already_active / not_requested`。 + - `stale_ratio_recheck_dispatch_id`:archive 成功後的 recheck dispatch id。 +- Dry-run 行為: + - 只計算 snapshot。 + - 不寫 KM、不寫 governance audit、不建立 recheck dispatch。 +- Owner 實際封存成功後: + - 先 soft archive duplicate KM。 + - 立刻計算 stale ratio snapshot。 + - 寫入一筆 terminal `governance_remediation_dispatch`:`executor_type=hermes_km_stale_ratio_recheck`、`dispatch_status=succeeded`、`workflow.current_stage=stale_ratio_recheck`、`worker_result.status=stale_ratio_rechecked`。 + - 再寫 archive audit dispatch,並在 audit context 反向記錄 recheck dispatch id / status。 +- `/awooop/work-items` 的 archive result 顯示: + - stale ratio recheck 狀態與 dispatch id。 + - stale / total / ratio / threshold snapshot。 + - 新增 stage i18n:`owner_approved_duplicate_archive`、`km_duplicate_archive_after_owner_approval`、`km_governance_rechecked`、`km_governance_close_or_continue`。 + +**Local verification**: + +```text +python3 -m py_compile apps/api/src/models/governance.py apps/api/src/api/v1/ai_governance.py apps/api/src/services/governance_km_review_service.py + -> ok +/Users/ogt/awoooi/apps/api/.venv/bin/python -m ruff check apps/api/src/api/v1/ai_governance.py apps/api/src/models/governance.py apps/api/src/services/governance_km_review_service.py apps/api/tests/test_ai_governance_endpoints.py + -> All checks passed +DATABASE_URL=postgresql+asyncpg://test:test@localhost:5432/test /Users/ogt/awoooi/apps/api/.venv/bin/python -m pytest apps/api/tests/test_ai_governance_endpoints.py apps/api/tests/test_governance_dispatcher.py apps/api/tests/test_hermes_kb_growth_worker.py -q + -> 60 passed +pnpm --dir apps/web exec next lint --file 'src/app/[locale]/awooop/work-items/page.tsx' + -> No ESLint warnings or errors +pnpm --dir apps/web exec tsc --noEmit --pretty false + -> ok +cd apps/api && /Users/ogt/awoooi/apps/api/.venv/bin/python ../../scripts/generate-schemas.py && cd ../../packages/shared-types && pnpm generate:types + -> packages/shared-types 無 diff +git diff --check + -> pass +``` + +**待 production deploy / smoke**: + +- Gitea Actions:CD / Code Review / Type Sync 全綠。 +- Production dry-run archive endpoint 應回 `status=dry_run`、`stale_ratio_recheck_status=dry_run`、`stale_ratio_snapshot`,且 `writes_km=false`。 +- Dry-run 前後 `duplicate_draft_total` 必須不變。 +- Work Items production smoke 應看到 stale ratio recheck 文案與 snapshot 欄位,且 page/console error 為 0。 + +**目前整體進度**: + +- AwoooP 告警可觀測鏈:約 99.1%。 +- 低風險自動修復閉環:約 95%。 +- 前端 AI 自動化管理介面同步:約 97.8%。 +- 治理告警可讀性 / 可處置性:約 98.2%。 +- AI Agent ownership 可追溯性:約 96.8%。 +- KM healthcheck 派工可追蹤性:約 99.3%。 +- Hermes KB growth 草稿 / owner review 閉環:約 98.9%。 +- 完整 AI 自動化管理產品化:約 96.2%。 + ## 2026-05-20|T93 KM duplicate drafts owner archive action **觸發**: diff --git a/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md b/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md index b63e8ea6..ddaef585 100644 --- a/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md +++ b/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md @@ -2323,6 +2323,15 @@ Phase 6 完成後 - Production:`c8a995af feat(governance): archive duplicate km review drafts` 已推 Gitea main;deploy marker `3c9404d2 chore(cd): deploy c8a995a [skip ci]`;Gitea runs `1885` CD、`1886` Code Review、`1887` Type Sync 全 success。Production health healthy/prod/mock_mode=false。`GET /ai/governance/km-review-drafts/dedupe?limit=100` 回 `total_review_drafts=90`、`event_group_total=23`、`duplicate_draft_total=67`。Archive endpoint dry-run 對第一組 duplicate plan 回 `status=dry_run`、`would_archive_entry_ids=5`、`writes_km=false`、`writes_governance_audit=false`,dry-run 前後 duplicate total 仍為 67。Work Items Playwright smoke 顯示封存重複草稿按鈕與 owner guard,pageErrors=0 / consoleErrors=0。 - 目前進度更新:治理告警可讀性 / 可處置性約 98.0%;AI Agent ownership 可追溯性約 96.5%;KM healthcheck 派工可追蹤性約 99.2%;Hermes KB growth 草稿 / owner review 閉環約 98.6%;完整 AI 自動化管理產品化約 95.9%。 +**T94 KM archive 後 stale ratio recheck trace(2026-05-20 台北)**: +- 觸發:T93 已讓 owner 可在 AwoooP 封存 duplicate KM review drafts,但封存後仍需要直接看到治理回測:KM stale ratio 是否下降、是否仍高於門檻、是否要繼續 owner review。 +- 修正:archive endpoint response 新增 `stale_ratio_snapshot`、`stale_ratio_recheck_status`、`stale_ratio_recheck_dispatch_id`。snapshot 沿用 `GovernanceAgent.check_knowledge_degradation` 的 non-archived KM + `KM_STALE_DAYS` / `KM_STALE_RATIO` 定義,不另立規則。 +- 寫入行為:dry-run 只計算 snapshot,不寫 KM / audit / recheck dispatch。owner 實際封存成功後,先 soft archive duplicates,再立刻計算 stale ratio,寫一筆 terminal `governance_remediation_dispatch`:`executor_type=hermes_km_stale_ratio_recheck`、`dispatch_status=succeeded`、`workflow.current_stage=stale_ratio_recheck`、`worker_result.status=stale_ratio_rechecked`;archive audit context 反向記錄 recheck dispatch id / status。 +- UI:`/awooop/work-items` 的 archive result 顯示 stale ratio recheck status、dispatch id、stale / total / ratio / threshold snapshot;新增 `owner_approved_duplicate_archive`、`km_duplicate_archive_after_owner_approval`、`km_governance_rechecked`、`km_governance_close_or_continue` stage i18n。 +- 邊界:T94 不讓 AI 自動批量改寫高影響 KM;它只把 owner action 後的治理回測納入 AwoooP 可見 audit trail。production smoke 仍使用 dry-run,不直接改 production KM。 +- Local verification:`py_compile` ok;ruff ok;治理 endpoint / dispatcher / Hermes worker tests `60 passed`;Work Items Next lint ok;`tsc --noEmit` ok;shared-types regenerate 後無 diff;`git diff --check` pass。 +- 目前進度更新:治理告警可讀性 / 可處置性約 98.2%;AI Agent ownership 可追溯性約 96.8%;KM healthcheck 派工可追蹤性約 99.3%;Hermes KB growth 草稿 / owner review 閉環約 98.9%;完整 AI 自動化管理產品化約 96.2%。 + --- ### 2026-04-20 晚 (台北) — C1-C4 全流程串接 — Playbook 鏈路保護(commit de2d34d)