diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index e58a320a..8bd6911f 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -1921,9 +1921,17 @@ }, "archiveActions": { "archive": "Archive duplicate drafts", + "preview": "Dry-run preview", + "previewing": "Previewing", + "confirm": "Confirm archive", + "confirming": "Archiving", "archiving": "Archiving", "failed": "Archive action failed; refresh and verify the latest dedupe plan.", - "requiresOwner": "Owner review required; backend rechecks the latest plan.", + "previewFailed": "Dry-run preview failed; refresh and verify the latest dedupe plan.", + "confirmFailed": "Archive confirmation failed; the backend may have detected a changed dedupe plan.", + "requiresOwner": "Run the dry-run preview first, then owner-confirm the archive; the backend rechecks the latest plan.", + "previewResult": "Dry run would archive {count}; writes KM: {writesKm}; writes audit: {writesAudit}", + "previewNext": "Next: only after owner confirmation will duplicate KM be soft-archived and audit / stale-ratio recheck rows be written.", "result": "Archived {archived}; audit dispatch: {audit}", "recheck": "Stale-ratio recheck: {status}; dispatch: {dispatch}", "snapshot": "Current stale {stale} / total {total}; ratio {ratio}; threshold {threshold}", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 59dda199..2862268d 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -1922,9 +1922,17 @@ }, "archiveActions": { "archive": "封存重複草稿", + "preview": "乾跑預覽", + "previewing": "預覽中", + "confirm": "確認封存", + "confirming": "封存中", "archiving": "封存中", "failed": "封存動作失敗;請重新整理後確認最新 dedupe plan。", - "requiresOwner": "需要 owner 審核;後端會重新比對最新 plan。", + "previewFailed": "乾跑預覽失敗;請重新整理後確認最新 dedupe plan。", + "confirmFailed": "確認封存失敗;後端可能偵測到 dedupe plan 已變更。", + "requiresOwner": "必須先乾跑預覽,再由 owner 確認封存;後端會重新比對最新 plan。", + "previewResult": "乾跑將封存 {count} 份;寫 KM:{writesKm};寫稽核:{writesAudit}", + "previewNext": "下一步:owner 確認後才會 soft archive duplicate KM 並寫入 audit / stale ratio 回測。", "result": "已封存 {archived} 份;稽核 dispatch:{audit}", "recheck": "Stale ratio 回測:{status};dispatch:{dispatch}", "snapshot": "目前 stale {stale} / total {total};ratio {ratio};門檻 {threshold}", 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 7c2763cf..ef974569 100644 --- a/apps/web/src/app/[locale]/awooop/work-items/page.tsx +++ b/apps/web/src/app/[locale]/awooop/work-items/page.tsx @@ -307,6 +307,14 @@ type KnowledgeReviewDraftArchiveResponse = { generated_at?: string | null; }; +type KnowledgeReviewDraftArchiveAction = { + previewLoading: boolean; + confirmLoading: boolean; + previewResult: KnowledgeReviewDraftArchiveResponse | null; + result: KnowledgeReviewDraftArchiveResponse | null; + error: string | null; +}; + type DriftFingerprintState = { schema_version?: string; namespace?: string; @@ -800,6 +808,10 @@ function staleRatioRecheckStatusKey(status?: string | null) { return "unknown"; } +function formatStaleRatio(value: number) { + return `${(value * 100).toFixed(1)}%`; +} + function buildWorkItems( telemetry: Telemetry, t: ReturnType @@ -1496,11 +1508,7 @@ function KnowledgeGovernancePanel({ onArchived: () => void; }) { const t = useTranslations("awooop.workItems.knowledgeGovernance"); - const [archiveActions, setArchiveActions] = useState>({}); + const [archiveActions, setArchiveActions] = useState>({}); const items = queue?.items ?? []; const draftGroups = groupKnowledgeReviewDrafts(reviewDrafts, items); const dedupeGroups = dedupe?.groups ?? []; @@ -1515,10 +1523,50 @@ function KnowledgeGovernancePanel({ const reviewCount = items.filter((item) => item.dispatch_status === "skipped" || item.workflow_stage === "waiting_owner_review" ).length; - const archiveDuplicates = useCallback(async (group: KnowledgeReviewDraftDedupeGroup) => { + const previewArchiveDuplicates = useCallback(async (group: KnowledgeReviewDraftDedupeGroup) => { setArchiveActions((current) => ({ ...current, - [group.governance_event_id]: { loading: true, result: null, error: null }, + [group.governance_event_id]: { + previewLoading: true, + confirmLoading: false, + previewResult: current[group.governance_event_id]?.previewResult ?? null, + result: null, + error: null, + }, + })); + const result = await postJson( + `${API_BASE}/api/v1/ai/governance/km-review-drafts/dedupe/${encodeURIComponent(group.governance_event_id)}/archive-duplicates`, + { + canonical_entry_id: group.canonical_entry_id, + duplicate_entry_ids: group.duplicate_entry_ids, + owner: "operator_console", + owner_approved: false, + dry_run: true, + }, + 15000 + ); + setArchiveActions((current) => ({ + ...current, + [group.governance_event_id]: { + previewLoading: false, + confirmLoading: false, + previewResult: result, + result: null, + error: result ? null : t("archiveActions.previewFailed"), + }, + })); + }, [t]); + + const confirmArchiveDuplicates = useCallback(async (group: KnowledgeReviewDraftDedupeGroup) => { + setArchiveActions((current) => ({ + ...current, + [group.governance_event_id]: { + previewLoading: false, + confirmLoading: true, + previewResult: current[group.governance_event_id]?.previewResult ?? null, + result: null, + error: null, + }, })); const result = await postJson( `${API_BASE}/api/v1/ai/governance/km-review-drafts/dedupe/${encodeURIComponent(group.governance_event_id)}/archive-duplicates`, @@ -1534,9 +1582,11 @@ function KnowledgeGovernancePanel({ setArchiveActions((current) => ({ ...current, [group.governance_event_id]: { - loading: false, + previewLoading: false, + confirmLoading: false, + previewResult: current[group.governance_event_id]?.previewResult ?? null, result, - error: result ? null : t("archiveActions.failed"), + error: result ? null : t("archiveActions.confirmFailed"), }, })); if (result?.status === "archived" || result?.status === "noop_already_archived") { @@ -1671,7 +1721,11 @@ function KnowledgeGovernancePanel({
{dedupeGroups.slice(0, 4).map((group) => { const archiveAction = archiveActions[group.governance_event_id]; - const resultKey = groupArchiveStatusKey(archiveAction?.result?.status); + const previewResult = archiveAction?.previewResult ?? null; + const finalResult = archiveAction?.result ?? null; + const previewReady = previewResult?.status === "dry_run"; + const previewKey = groupArchiveStatusKey(previewResult?.status); + const finalResultKey = groupArchiveStatusKey(finalResult?.status); return (
+ {t("archiveActions.requiresOwner")} @@ -1735,34 +1809,61 @@ function KnowledgeGovernancePanel({ {archiveAction.error}
) : null} - {archiveAction?.result ? ( + {previewResult ? ( +
+

+ {t(`archiveActions.statuses.${previewKey}` as never)} +

+

+ {t("archiveActions.previewResult", { + count: previewResult.would_archive_entry_ids.length, + writesKm: String(previewResult.writes_km), + writesAudit: String(previewResult.writes_governance_audit), + })} +

+

+ {t("archiveActions.previewNext")} +

+ {previewResult.stale_ratio_snapshot ? ( +

+ {t("archiveActions.snapshot", { + stale: previewResult.stale_ratio_snapshot.stale_count, + total: previewResult.stale_ratio_snapshot.total_count, + ratio: formatStaleRatio(previewResult.stale_ratio_snapshot.stale_ratio), + threshold: formatStaleRatio(previewResult.stale_ratio_snapshot.threshold), + })} +

+ ) : null} +
+ ) : null} + {finalResult ? (

- {t(`archiveActions.statuses.${resultKey}` as never)} + {t(`archiveActions.statuses.${finalResultKey}` as never)}

{t("archiveActions.result", { - archived: archiveAction.result.archived_entry_ids.length, - audit: archiveAction.result.audit_dispatch_id ?? "--", + archived: finalResult.archived_entry_ids.length, + audit: finalResult.audit_dispatch_id ?? "--", })}

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

- {archiveAction.result.stale_ratio_snapshot ? ( + {finalResult.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, + stale: finalResult.stale_ratio_snapshot.stale_count, + total: finalResult.stale_ratio_snapshot.total_count, + ratio: formatStaleRatio(finalResult.stale_ratio_snapshot.stale_ratio), + threshold: formatStaleRatio(finalResult.stale_ratio_snapshot.threshold), })}

) : null} diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 10821766..2b0dd02b 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,76 @@ +## 2026-05-20|T95 KM duplicate archive two-step safety + +**觸發**: + +- T93 / T94 已讓 AwoooP 可以看見 KM duplicate drafts、owner archive action、archive 後 stale ratio recheck trace。 +- 但 Work Items 仍是單一「封存重複草稿」按鈕,雖然後端有 owner approval / latest plan guard,Operator 仍可能誤點直接送 `dry_run=false`。 + +**修正**: + +- `/awooop/work-items` 的 KM 草稿去重 action 改為兩段式: + - 第一段:`乾跑預覽`,送 `dry_run=true`、`owner_approved=false`。 + - 第二段:`確認封存`,只有 preview 成功後才解除 disabled,送 `dry_run=false`、`owner_approved=true`。 +- Preview 結果直接顯示: + - `would_archive_entry_ids` 數量。 + - `writes_km=false` / `writes_governance_audit=false`。 + - stale / total / ratio / threshold snapshot,ratio 改成百分比顯示。 +- Confirm 結果保留 T94 的 archive audit dispatch 與 stale ratio recheck dispatch 顯示。 +- i18n 已同步 `zh-TW` / `en`,沒有硬編碼 UI 文案。 + +**Local verification**: + +```text +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 +node -e 'JSON.parse(...)' + -> i18n json ok +pnpm --dir apps/web run build + -> expected guard: missing NEXT_PUBLIC_API_URL blocks prerender +NEXT_PUBLIC_API_URL=https://awoooi.wooo.work pnpm --dir apps/web run build + -> compiled and generated 90/90 static pages +``` + +**Local interactive smoke(live production API bridge,無寫入)**: + +```text +Local dev: + NEXT_PUBLIC_API_URL=https://awoooi.wooo.work pnpm --dir apps/web dev --hostname 127.0.0.1 --port 3030 + +Playwright checks: + -> confirmDisabledBefore=true + -> previewVisible=true + -> confirmVisible=true + -> previewResultVisible=true + -> previewShowsNoWrites=true + -> dryRunRequestOnly=true + -> confirmEnabledAfterPreview=true + -> blockedNoWrites=true + -> pageErrors=0 / consoleErrors=0 + +Observed POST: + /api/v1/ai/governance/km-review-drafts/dedupe/{event}/archive-duplicates + body dry_run=true, owner_approved=false, owner=operator_console + +Blocked writes: + -> 0 + +Screenshot: + /tmp/awoooi-t95-km-archive-two-step-local.png +``` + +**目前整體進度**: + +- AwoooP 告警可觀測鏈:約 99.1%。 +- 低風險自動修復閉環:約 95%。 +- 前端 AI 自動化管理介面同步:約 98.2%。 +- 治理告警可讀性 / 可處置性:約 98.5%。 +- AI Agent ownership 可追溯性:約 97.3%。 +- KM healthcheck 派工可追蹤性:約 99.5%。 +- Hermes KB growth 草稿 / owner review 閉環:約 99.1%。 +- 完整 AI 自動化管理產品化:約 96.7%。 + ## 2026-05-20|T94 KM archive 後 stale ratio recheck trace **觸發**: 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 280e92ac..e160d053 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 @@ -2333,6 +2333,13 @@ Phase 6 完成後 - Production:`d283e653 feat(governance): trace km stale ratio rechecks` 已推 Gitea main;deploy marker `b7eb3f7d chore(cd): deploy d283e65 [skip ci]`;Gitea runs `1888` CD、`1889` Code Review、`1890` Type Sync 全 success。Production health healthy/prod/mock_mode=false。Dry-run archive endpoint 回 `status=dry_run`、`writes_km=false`、`writes_governance_audit=false`、`stale_ratio_recheck_status=dry_run`、`stale_ratio_snapshot stale_count=1454 total_count=1966 stale_ratio=0.74 threshold=0.2 stale_days=7`;dry-run 前後 duplicate total 仍為 67。Work Items Playwright smoke 顯示 nav、KM healthcheck panel、KM 草稿去重視圖、封存重複草稿按鈕、owner guard、stale ratio recheck 文案,pageErrors=0 / consoleErrors=0,截圖 `/tmp/awoooi-t94-km-stale-ratio-recheck.png`。 - 目前進度更新:前端 AI 自動化管理介面同步約 97.9%;治理告警可讀性 / 可處置性約 98.3%;AI Agent ownership 可追溯性約 97.0%;KM healthcheck 派工可追蹤性約 99.4%;Hermes KB growth 草稿 / owner review 閉環約 99.0%;完整 AI 自動化管理產品化約 96.4%。 +**T95 KM duplicate archive two-step safety(2026-05-20 台北)**: +- 觸發:T93 / T94 已打通 duplicate KM review drafts 的 owner archive 與 stale ratio recheck trace,但 Work Items 仍是單一封存按鈕;需要把危險寫入前的 dry-run preview 做成產品流程,而不是只依賴後端 guard。 +- 修正:`/awooop/work-items` 的 KM 草稿去重 action 改為兩段式。第一段 `乾跑預覽` 送 `dry_run=true`、`owner_approved=false`;第二段 `確認封存` 只有 preview 成功後才解除 disabled,送 `dry_run=false`、`owner_approved=true`。Preview 顯示 would-archive 數量、`writes_km=false`、`writes_governance_audit=false`、stale ratio snapshot;confirm 結果保留 archive audit dispatch 與 stale ratio recheck dispatch。 +- 邊界:T95 沒有實際封存 production KM;local smoke 用 live production API bridge 只允許 `dry_run=true` POST,測試層阻擋任何非 dry-run archive request。這仍符合「高影響 KM 必須 owner review」與「破壞性動作先 dry-run」鐵律。 +- Local verification:Work Items Next lint ok;`tsc --noEmit` ok;i18n JSON parse ok;未設 `NEXT_PUBLIC_API_URL` 的 build 仍被既有 guard 阻擋;`NEXT_PUBLIC_API_URL=https://awoooi.wooo.work pnpm --dir apps/web run build` 成功產出 90/90 static pages。Local Playwright smoke 確認 confirm button preview 前 disabled、preview 後 enabled、唯一 POST 為 `dry_run=true` + `owner_approved=false`、`writes_km=false` / `writes_governance_audit=false` 可見、blockedWrites=0、pageErrors=0 / consoleErrors=0,截圖 `/tmp/awoooi-t95-km-archive-two-step-local.png`。 +- 目前進度更新:前端 AI 自動化管理介面同步約 98.2%;治理告警可讀性 / 可處置性約 98.5%;AI Agent ownership 可追溯性約 97.3%;KM healthcheck 派工可追蹤性約 99.5%;Hermes KB growth 草稿 / owner review 閉環約 99.1%;完整 AI 自動化管理產品化約 96.7%。 + --- ### 2026-04-20 晚 (台北) — C1-C4 全流程串接 — Playbook 鏈路保護(commit de2d34d)