feat(governance): complete stale km owner review
All checks were successful
CD Pipeline / tests (push) Successful in 5m28s
Code Review / ai-code-review (push) Successful in 12s
Type Sync Check / check-type-sync (push) Successful in 26s
CD Pipeline / build-and-deploy (push) Successful in 5m12s
CD Pipeline / post-deploy-checks (push) Successful in 1m31s

This commit is contained in:
Your Name
2026-05-24 18:28:10 +08:00
parent 00cf6f009d
commit 630cd5381c
8 changed files with 1435 additions and 2 deletions

View File

@@ -397,6 +397,10 @@ type KnowledgeStaleCandidate = {
related_playbook_id?: string | null;
related_approval_id?: string | null;
tags: string[];
owner_review_dispatch_id?: string | null;
owner_review_status?: string | null;
owner_review_stage?: string | null;
owner_review_next_action?: string | null;
};
type KnowledgeStaleCandidatesResponse = {
@@ -434,6 +438,42 @@ type KnowledgeStaleOwnerReviewAction = {
error: string | null;
};
type KnowledgeStaleOwnerReviewCompleteResponse = {
schema_version?: string;
entry_id: string;
project_id: string;
status: "dry_run" | "completed" | "already_completed";
review_outcome: "refresh_with_evidence" | "archive" | "supersede";
governance_event_id: string;
dispatch_id: string;
audit_dispatch_id?: string | null;
stale_ratio_recheck_dispatch_id?: string | null;
workflow_stage: string;
owner: string;
owner_approved: boolean;
dry_run: boolean;
writes_km: boolean;
writes_governance_audit: boolean;
stale_ratio_snapshot?: {
stale_count: number;
total_count: number;
stale_ratio: number;
threshold: number;
stale_days: number;
} | null;
dry_run_plan_fingerprint?: string | null;
next_action: string;
generated_at?: string | null;
};
type KnowledgeStaleOwnerReviewCompleteAction = {
previewLoading: boolean;
confirmLoading: boolean;
previewResult: KnowledgeStaleOwnerReviewCompleteResponse | null;
result: KnowledgeStaleOwnerReviewCompleteResponse | null;
error: string | null;
};
type DriftFingerprintState = {
schema_version?: string;
namespace?: string;
@@ -833,7 +873,10 @@ function governanceKmStageKey(stage?: string | null) {
stage === "ai_analyzed" ||
stage === "draft_km_updates" ||
stage === "waiting_owner_review" ||
stage === "owner_updates_or_archives_km" ||
stage === "km_writeback_after_approval" ||
stage === "km_archive_after_approval" ||
stage === "km_supersede_after_approval" ||
stage === "stale_ratio_recheck" ||
stage === "owner_approved_duplicate_archive" ||
stage === "km_duplicate_archive_after_owner_approval" ||
@@ -1109,6 +1152,24 @@ function kmStaleReviewStatusKey(value: string | null | undefined) {
}
}
function kmStaleReviewCompleteStatusKey(value: string | null | undefined) {
switch (value) {
case "dry_run":
case "completed":
case "already_completed":
return value;
default:
return "unknown";
}
}
function kmStaleReviewOutcomeForCandidate(
candidate: KnowledgeStaleCandidate
): "refresh_with_evidence" | "archive" | "supersede" {
if (candidate.recommended_action === "archive_or_supersede") return "archive";
return "refresh_with_evidence";
}
function kmCorrelationSourceKey(value: string | null | undefined) {
switch (value) {
case "incident":
@@ -2109,6 +2170,8 @@ function KnowledgeGovernancePanel({
const t = useTranslations("awooop.workItems.knowledgeGovernance");
const [archiveActions, setArchiveActions] = useState<Record<string, KnowledgeReviewDraftArchiveAction>>({});
const [staleReviewActions, setStaleReviewActions] = useState<Record<string, KnowledgeStaleOwnerReviewAction>>({});
const [staleReviewCompletionActions, setStaleReviewCompletionActions] =
useState<Record<string, KnowledgeStaleOwnerReviewCompleteAction>>({});
const items = queue?.items ?? [];
const draftGroups = groupKnowledgeReviewDrafts(reviewDrafts, items);
const dedupeGroups = dedupe?.groups ?? [];
@@ -2243,6 +2306,119 @@ function KnowledgeGovernancePanel({
}
}, [onArchived, t]);
const previewStaleReviewCompletion = useCallback(async (candidate: KnowledgeStaleCandidate) => {
const queuedDispatchId = (
candidate.owner_review_dispatch_id
?? staleReviewActions[candidate.entry_id]?.result?.dispatch_id
?? null
);
if (!queuedDispatchId) {
setStaleReviewCompletionActions((current) => ({
...current,
[candidate.entry_id]: {
previewLoading: false,
confirmLoading: false,
previewResult: current[candidate.entry_id]?.previewResult ?? null,
result: current[candidate.entry_id]?.result ?? null,
error: t("staleCandidates.completeActions.missingDispatch"),
},
}));
return;
}
setStaleReviewCompletionActions((current) => ({
...current,
[candidate.entry_id]: {
previewLoading: true,
confirmLoading: false,
previewResult: current[candidate.entry_id]?.previewResult ?? null,
result: null,
error: null,
},
}));
const result = await postJson<KnowledgeStaleOwnerReviewCompleteResponse>(
`${API_BASE}/api/v1/ai/governance/km-stale-candidates/${encodeURIComponent(candidate.entry_id)}/complete-review`,
{
dispatch_id: queuedDispatchId,
owner: "operator_console",
owner_approved: false,
dry_run: true,
review_outcome: kmStaleReviewOutcomeForCandidate(candidate),
owner_note: `operator_console_review:${candidate.priority_tier}:${candidate.recommended_action}`,
},
15000
);
setStaleReviewCompletionActions((current) => ({
...current,
[candidate.entry_id]: {
previewLoading: false,
confirmLoading: false,
previewResult: result,
result: null,
error: result ? null : t("staleCandidates.completeActions.previewFailed"),
},
}));
}, [staleReviewActions, t]);
const confirmStaleReviewCompletion = useCallback(async (candidate: KnowledgeStaleCandidate) => {
const action = staleReviewCompletionActions[candidate.entry_id];
const queuedDispatchId = (
candidate.owner_review_dispatch_id
?? staleReviewActions[candidate.entry_id]?.result?.dispatch_id
?? action?.previewResult?.dispatch_id
?? null
);
const fingerprint = action?.previewResult?.dry_run_plan_fingerprint;
if (!queuedDispatchId || !fingerprint) {
setStaleReviewCompletionActions((current) => ({
...current,
[candidate.entry_id]: {
previewLoading: false,
confirmLoading: false,
previewResult: current[candidate.entry_id]?.previewResult ?? null,
result: current[candidate.entry_id]?.result ?? null,
error: t("staleCandidates.completeActions.missingPreviewFingerprint"),
},
}));
return;
}
setStaleReviewCompletionActions((current) => ({
...current,
[candidate.entry_id]: {
previewLoading: false,
confirmLoading: true,
previewResult: current[candidate.entry_id]?.previewResult ?? null,
result: null,
error: null,
},
}));
const result = await postJson<KnowledgeStaleOwnerReviewCompleteResponse>(
`${API_BASE}/api/v1/ai/governance/km-stale-candidates/${encodeURIComponent(candidate.entry_id)}/complete-review`,
{
dispatch_id: queuedDispatchId,
owner: "operator_console",
owner_approved: true,
dry_run: false,
review_outcome: kmStaleReviewOutcomeForCandidate(candidate),
owner_note: `operator_console_review:${candidate.priority_tier}:${candidate.recommended_action}`,
dry_run_plan_fingerprint: fingerprint,
},
15000
);
setStaleReviewCompletionActions((current) => ({
...current,
[candidate.entry_id]: {
previewLoading: false,
confirmLoading: false,
previewResult: current[candidate.entry_id]?.previewResult ?? null,
result,
error: result ? null : t("staleCandidates.completeActions.confirmFailed"),
},
}));
if (result?.status === "completed" || result?.status === "already_completed") {
onArchived();
}
}, [onArchived, staleReviewActions, staleReviewCompletionActions, t]);
return (
<section className="border border-[#e0ddd4] bg-white">
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-[#e0ddd4] bg-[#faf9f3] px-4 py-3">
@@ -2395,6 +2571,15 @@ function KnowledgeGovernancePanel({
const reviewAction = staleReviewActions[candidate.entry_id];
const reviewResult = reviewAction?.result ?? null;
const reviewStatusKey = kmStaleReviewStatusKey(reviewResult?.status);
const queuedDispatchId = candidate.owner_review_dispatch_id ?? reviewResult?.dispatch_id ?? null;
const persistedReviewStatusKey = governanceKmDispatchStatusKey(candidate.owner_review_status);
const persistedReviewStageKey = governanceKmStageKey(candidate.owner_review_stage);
const completionAction = staleReviewCompletionActions[candidate.entry_id];
const completionPreview = completionAction?.previewResult ?? null;
const completionResult = completionAction?.result ?? null;
const completionPreviewReady = Boolean(completionPreview?.dry_run_plan_fingerprint);
const completionPreviewStatusKey = kmStaleReviewCompleteStatusKey(completionPreview?.status);
const completionResultStatusKey = kmStaleReviewCompleteStatusKey(completionResult?.status);
return (
<article
key={candidate.entry_id}
@@ -2446,6 +2631,15 @@ function KnowledgeGovernancePanel({
approval: candidate.related_approval_id ?? "--",
})}
</p>
{queuedDispatchId ? (
<p>
{t("staleCandidates.ownerReviewState", {
status: t(`statuses.${persistedReviewStatusKey}` as never),
stage: t(`stages.${persistedReviewStageKey}` as never),
dispatch: queuedDispatchId,
})}
</p>
) : null}
</div>
<div className="mt-2 flex flex-wrap gap-1">
{candidate.reasons.slice(0, 5).map((reason) => (
@@ -2469,6 +2663,37 @@ function KnowledgeGovernancePanel({
? t("staleCandidates.queueingReview")
: t("staleCandidates.queueReview")}
</button>
<button
type="button"
onClick={() => previewStaleReviewCompletion(candidate)}
disabled={
!queuedDispatchId ||
completionAction?.previewLoading ||
completionAction?.confirmLoading
}
className="inline-flex items-center gap-1.5 border border-[#d8d3c7] bg-white px-2 py-1 text-xs font-semibold text-[#2e2b26] hover:border-[#9bc7a4] hover:bg-[#f0faf2] hover:text-[#17602a] disabled:cursor-not-allowed disabled:opacity-60"
>
<SearchCheck className="h-3.5 w-3.5" aria-hidden="true" />
{completionAction?.previewLoading
? t("staleCandidates.completeActions.previewing")
: t("staleCandidates.completeActions.preview")}
</button>
<button
type="button"
onClick={() => confirmStaleReviewCompletion(candidate)}
disabled={
!queuedDispatchId ||
!completionPreviewReady ||
completionAction?.previewLoading ||
completionAction?.confirmLoading
}
className="inline-flex items-center gap-1.5 border border-[#d8d3c7] bg-white px-2 py-1 text-xs font-semibold text-[#2e2b26] hover:border-[#d9b36f] hover:bg-[#fff7e8] hover:text-[#8a5a08] disabled:cursor-not-allowed disabled:opacity-60"
>
<ShieldCheck className="h-3.5 w-3.5" aria-hidden="true" />
{completionAction?.confirmLoading
? t("staleCandidates.completeActions.confirming")
: t("staleCandidates.completeActions.confirm")}
</button>
<Link
href={`/knowledge-base?q=${encodeURIComponent(candidate.title)}`}
className="inline-flex items-center gap-1.5 border border-[#d8d3c7] bg-white px-2 py-1 text-xs font-semibold text-[#2e2b26] hover:border-[#1f6feb] hover:bg-[#edf4ff] hover:text-[#0f4fa8]"
@@ -2491,6 +2716,69 @@ function KnowledgeGovernancePanel({
})}
</p>
) : null}
{completionAction?.error ? (
<p className="mt-2 border border-[#e2a29b] bg-[#fff0ef] px-2 py-1 text-[11px] leading-5 text-[#9f2f25]">
{completionAction.error}
</p>
) : null}
{completionPreview ? (
<div className="mt-2 border border-[#d9b36f] bg-[#fff7e8] px-2 py-1.5 text-[11px] leading-5 text-[#8a5a08]">
<p className="font-semibold">
{t(
`staleCandidates.completeActions.statuses.${completionPreviewStatusKey}` as never
)}
</p>
<p className="mt-1 text-[#5f5b52]">
{t("staleCandidates.completeActions.previewResult", {
outcome: t(
`staleCandidates.completeActions.outcomes.${completionPreview.review_outcome}` as never
),
writesKm: String(completionPreview.writes_km),
writesAudit: String(completionPreview.writes_governance_audit),
})}
</p>
<p className="mt-1 break-all font-mono text-[11px] text-[#5f5b52]">
{t("staleCandidates.completeActions.planFingerprint", {
fingerprint: completionPreview.dry_run_plan_fingerprint ?? "--",
})}
</p>
{completionPreview.stale_ratio_snapshot ? (
<p className="mt-1 text-[#5f5b52]">
{t("staleCandidates.completeActions.snapshot", {
stale: completionPreview.stale_ratio_snapshot.stale_count,
total: completionPreview.stale_ratio_snapshot.total_count,
ratio: formatStaleRatio(completionPreview.stale_ratio_snapshot.stale_ratio),
threshold: formatStaleRatio(completionPreview.stale_ratio_snapshot.threshold),
})}
</p>
) : null}
</div>
) : null}
{completionResult ? (
<div className="mt-2 border border-[#9bc7a4] bg-[#f0faf2] px-2 py-1.5 text-[11px] leading-5 text-[#17602a]">
<p className="font-semibold">
{t(
`staleCandidates.completeActions.statuses.${completionResultStatusKey}` as never
)}
</p>
<p className="mt-1 text-[#5f5b52]">
{t("staleCandidates.completeActions.result", {
audit: completionResult.audit_dispatch_id ?? "--",
recheck: completionResult.stale_ratio_recheck_dispatch_id ?? "--",
})}
</p>
{completionResult.stale_ratio_snapshot ? (
<p className="mt-1 text-[#5f5b52]">
{t("staleCandidates.completeActions.snapshot", {
stale: completionResult.stale_ratio_snapshot.stale_count,
total: completionResult.stale_ratio_snapshot.total_count,
ratio: formatStaleRatio(completionResult.stale_ratio_snapshot.stale_ratio),
threshold: formatStaleRatio(completionResult.stale_ratio_snapshot.threshold),
})}
</p>
) : null}
</div>
) : null}
</article>
);
})}