feat(governance): surface stale km completion queue
Some checks failed
CD Pipeline / tests (push) Successful in 5m28s
Code Review / ai-code-review (push) Successful in 11s
Type Sync Check / check-type-sync (push) Successful in 25s
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled

This commit is contained in:
Your Name
2026-05-24 22:42:59 +08:00
parent bd2762e76c
commit 0a8a15075a
7 changed files with 765 additions and 20 deletions

View File

@@ -12,6 +12,7 @@ import {
Activity,
Archive,
ArrowRight,
CheckCircle2,
ClipboardList,
Database,
Fingerprint,
@@ -575,6 +576,60 @@ type KnowledgeStaleOwnerReviewBurnDownResponse = {
generated_at?: string | null;
};
type KnowledgeStaleOwnerReviewCompletionQueueItem = {
dispatch_id: string;
governance_event_id: string;
entry_id: string;
project_id: string;
title: string;
dispatch_status: string;
workflow_stage: string;
readiness: "ready" | "blocked" | "completed" | "failed";
recommended_completion_outcome: "refresh_with_evidence" | "archive" | "supersede";
next_action: string;
blockers: string[];
required_owner_fields: string[];
can_preview: boolean;
can_confirm_after_preview: boolean;
writes_km_on_confirm: boolean;
owner?: string | null;
owner_note?: string | null;
batch_governance_event_id?: string | null;
batch_dispatch_id?: string | null;
priority_tier: "P0" | "P1" | "P2";
priority_score: number;
recommended_action: "refresh_with_evidence" | "owner_review" | "archive_or_supersede";
stale_days: number;
view_count: number;
correlation_sources: string[];
reasons: string[];
related_incident_id?: string | null;
related_playbook_id?: string | null;
related_approval_id?: string | null;
dry_run_plan_fingerprint?: string | null;
queued_at?: string | null;
started_at?: string | null;
completed_at?: string | null;
};
type KnowledgeStaleOwnerReviewCompletionQueueResponse = {
schema_version?: string;
project_id: string;
status_bucket: "all" | "ready" | "blocked" | "completed" | "failed" | "pending";
total: number;
returned: number;
pending_count: number;
ready_count: number;
blocked_count: number;
completed_count: number;
failed_count: number;
writes_on_read: boolean;
manual_review_required: boolean;
batch_writes_allowed: boolean;
items: KnowledgeStaleOwnerReviewCompletionQueueItem[];
generated_at?: string | null;
};
type KnowledgeStaleOwnerReviewCompleteResponse = {
schema_version?: string;
entry_id: string;
@@ -690,6 +745,7 @@ type Telemetry = {
knowledgeStaleCandidates: KnowledgeStaleCandidatesResponse | null;
knowledgeStaleOwnerReviews: KnowledgeStaleOwnerReviewInboxResponse | null;
knowledgeStaleOwnerReviewBurnDown: KnowledgeStaleOwnerReviewBurnDownResponse | null;
knowledgeStaleOwnerReviewCompletionQueue: KnowledgeStaleOwnerReviewCompletionQueueResponse | null;
channelEvents: RecentEventsResponse | null;
eventRecurrence: RecurrenceResponse | null;
slo: SloResponse | null;
@@ -1324,6 +1380,18 @@ function kmStaleBatchItemStatusKey(value: string | null | undefined) {
}
}
function kmStaleCompletionReadinessKey(value: string | null | undefined) {
switch (value) {
case "ready":
case "blocked":
case "completed":
case "failed":
return value;
default:
return "blocked";
}
}
function kmStaleReviewCompleteStatusKey(value: string | null | undefined) {
switch (value) {
case "dry_run":
@@ -1343,7 +1411,7 @@ function kmStaleReviewOutcomeForCandidate(
}
function staleCandidateFromOwnerReviewItem(
item: KnowledgeStaleOwnerReviewInboxItem
item: KnowledgeStaleOwnerReviewInboxItem | KnowledgeStaleOwnerReviewCompletionQueueItem
): KnowledgeStaleCandidate {
return {
entry_id: item.entry_id,
@@ -2360,6 +2428,7 @@ function KnowledgeGovernancePanel({
staleCandidates,
ownerReviewInbox,
burnDown,
completionQueue,
onArchived,
}: {
queue: GovernanceQueueResponse | null;
@@ -2368,6 +2437,7 @@ function KnowledgeGovernancePanel({
staleCandidates: KnowledgeStaleCandidatesResponse | null;
ownerReviewInbox: KnowledgeStaleOwnerReviewInboxResponse | null;
burnDown: KnowledgeStaleOwnerReviewBurnDownResponse | null;
completionQueue: KnowledgeStaleOwnerReviewCompletionQueueResponse | null;
onArchived: () => void;
}) {
const t = useTranslations("awooop.workItems.knowledgeGovernance");
@@ -2393,6 +2463,7 @@ function KnowledgeGovernancePanel({
const ownerReviewItems = ownerReviewInbox?.items ?? [];
const burnDownItems = burnDown?.items ?? [];
const burnDownSnapshot = burnDown?.current_snapshot ?? null;
const completionQueueItems = completionQueue?.items ?? [];
const draftTotal = dedupe?.total_review_drafts ?? reviewDrafts?.total ?? 0;
const activeCount = items.filter((item) =>
["pending", "dispatched", "executing"].includes(item.dispatch_status)
@@ -3109,6 +3180,185 @@ function KnowledgeGovernancePanel({
</>
)}
</div>
<div className="mb-3 border border-[#e0ddd4] bg-white px-3 py-2 text-xs text-[#5f5b52]">
<div className="mb-2 flex flex-wrap items-center justify-between gap-2">
<div className="flex items-center gap-2">
<CheckCircle2 className="h-4 w-4 text-[#5f5b52]" aria-hidden="true" />
<div>
<p className="font-semibold text-[#141413]">
{t("staleCandidates.completionQueue.title")}
</p>
<p className="mt-1 text-[11px] leading-5 text-[#77736a]">
{t("staleCandidates.completionQueue.subtitle")}
</p>
</div>
</div>
<div className="flex flex-wrap gap-2 font-mono text-[11px] text-[#5f5b52]">
<span className="border border-[#d8d3c7] bg-[#faf9f3] px-2 py-0.5">
{t("staleCandidates.completionQueue.ready", {
count: completionQueue?.ready_count ?? 0,
})}
</span>
<span className="border border-[#d8d3c7] bg-[#faf9f3] px-2 py-0.5">
{t("staleCandidates.completionQueue.blocked", {
count: completionQueue?.blocked_count ?? 0,
})}
</span>
<span className="border border-[#d8d3c7] bg-[#faf9f3] px-2 py-0.5">
{t("staleCandidates.completionQueue.completed", {
count: completionQueue?.completed_count ?? 0,
})}
</span>
<span className="border border-[#d8d3c7] bg-[#faf9f3] px-2 py-0.5">
{t("staleCandidates.completionQueue.failed", {
count: completionQueue?.failed_count ?? 0,
})}
</span>
</div>
</div>
{completionQueue === null ? (
<p className="text-[11px] leading-5 text-[#8a5a08]">
{t("staleCandidates.completionQueue.unavailable")}
</p>
) : completionQueueItems.length === 0 ? (
<p className="text-[11px] leading-5 text-[#5f5b52]">
{t("staleCandidates.completionQueue.empty")}
</p>
) : (
<>
<div className="mb-2 flex flex-wrap gap-2 font-mono text-[11px] text-[#5f5b52]">
<span className="border border-[#d8d3c7] bg-[#faf9f3] px-2 py-0.5">
{t("staleCandidates.completionQueue.pending", {
count: completionQueue.pending_count,
})}
</span>
<span className="border border-[#d8d3c7] bg-[#faf9f3] px-2 py-0.5">
{t("staleCandidates.completionQueue.guardrail", {
writes: String(completionQueue.writes_on_read),
review: String(completionQueue.manual_review_required),
batch: String(completionQueue.batch_writes_allowed),
})}
</span>
</div>
<div className="grid gap-2 md:grid-cols-2">
{completionQueueItems.slice(0, 6).map((item) => {
const completionCandidate = staleCandidateFromOwnerReviewItem(item);
const completionAction = staleReviewCompletionActions[item.entry_id];
const completionPreview = completionAction?.previewResult ?? null;
const completionResult = completionAction?.result ?? null;
const readinessKey = kmStaleCompletionReadinessKey(item.readiness);
return (
<article
key={item.dispatch_id}
className="border border-[#e0ddd4] bg-[#faf9f3] px-3 py-2 text-[11px] leading-5 text-[#5f5b52]"
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="truncate font-semibold text-[#141413]">
{item.title}
</p>
<p className="mt-1 truncate font-mono text-[#77736a]">
{item.entry_id}
</p>
</div>
<span className="shrink-0 border border-[#d8d3c7] bg-white px-2 py-0.5 font-mono font-semibold text-[#2e2b26]">
{item.priority_tier}
</span>
</div>
<p className="mt-2">
{t("staleCandidates.completionQueue.state", {
readiness: t(`staleCandidates.completionQueue.readiness.${readinessKey}` as never),
stage: t(`stages.${governanceKmStageKey(item.workflow_stage)}` as never),
})}
</p>
<p>
{t("staleCandidates.completionQueue.next", {
action: item.next_action,
outcome: t(`staleCandidates.completeActions.outcomes.${item.recommended_completion_outcome}` as never),
})}
</p>
<p>
{t("staleCandidates.ownerReviewInbox.meta", {
days: item.stale_days,
score: item.priority_score,
views: item.view_count,
})}
</p>
<p className="truncate">
{t("staleCandidates.ownerReviewInbox.batch", {
batch: item.batch_dispatch_id ?? "--",
})}
</p>
<p className="truncate">
{t("staleCandidates.completionQueue.required", {
fields: item.required_owner_fields.length > 0
? item.required_owner_fields.join(", ")
: "--",
})}
</p>
{item.blockers.length > 0 ? (
<p className="truncate text-[#9f2f25]">
{t("staleCandidates.completionQueue.blockers", {
blockers: item.blockers.join(", "),
})}
</p>
) : null}
{item.can_preview ? (
<div className="mt-2 flex flex-wrap gap-2">
<button
type="button"
onClick={() => previewStaleReviewCompletion(completionCandidate)}
disabled={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"
>
<ClipboardList 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(completionCandidate)}
disabled={
completionAction?.confirmLoading
|| !completionPreview?.dry_run_plan_fingerprint
}
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"
>
<CheckCircle2 className="h-3.5 w-3.5" aria-hidden="true" />
{completionAction?.confirmLoading
? t("staleCandidates.completeActions.confirming")
: t("staleCandidates.completeActions.confirm")}
</button>
</div>
) : null}
{completionAction?.error ? (
<p className="mt-2 border border-[#e2a29b] bg-[#fff0ef] px-2 py-1 text-[11px] text-[#9f2f25]">
{completionAction.error}
</p>
) : null}
{completionPreview ? (
<p className="mt-2 border border-[#d9b36f] bg-[#fff7e8] px-2 py-1 font-mono text-[11px] text-[#8a5a08]">
{t("staleCandidates.completeActions.planFingerprint", {
fingerprint: completionPreview.dry_run_plan_fingerprint ?? "--",
})}
</p>
) : null}
{completionResult ? (
<p className="mt-2 border border-[#9bc7a4] bg-[#f0faf2] px-2 py-1 text-[11px] text-[#17602a]">
{t("staleCandidates.completeActions.result", {
audit: completionResult.audit_dispatch_id ?? "--",
recheck: completionResult.stale_ratio_recheck_dispatch_id ?? "--",
})}
</p>
) : null}
</article>
);
})}
</div>
</>
)}
</div>
<div className="mb-3 border border-[#e0ddd4] bg-white px-3 py-2 text-xs text-[#5f5b52]">
<div className="mb-2 flex flex-wrap items-center justify-between gap-2">
<div className="flex items-center gap-2">
@@ -4008,6 +4258,7 @@ export default function AwoooPWorkItemsPage() {
knowledgeStaleCandidates: null,
knowledgeStaleOwnerReviews: null,
knowledgeStaleOwnerReviewBurnDown: null,
knowledgeStaleOwnerReviewCompletionQueue: null,
channelEvents: null,
eventRecurrence: null,
slo: null,
@@ -4030,6 +4281,7 @@ export default function AwoooPWorkItemsPage() {
const knowledgeStaleCandidatesUrl = `${API_BASE}/api/v1/ai/governance/km-stale-candidates?project_id=${encodedProjectId}&limit=20`;
const knowledgeStaleOwnerReviewsUrl = `${API_BASE}/api/v1/ai/governance/km-stale-owner-reviews?project_id=${encodedProjectId}&dispatch_status=pending&limit=30`;
const knowledgeStaleOwnerReviewBurnDownUrl = `${API_BASE}/api/v1/ai/governance/km-stale-owner-review-burndown?project_id=${encodedProjectId}&limit=20`;
const knowledgeStaleOwnerReviewCompletionQueueUrl = `${API_BASE}/api/v1/ai/governance/km-stale-owner-review-completion-queue?project_id=${encodedProjectId}&status_bucket=all&limit=30`;
const channelEventsUrl = `${API_BASE}/api/v1/platform/events/recent?project_id=${encodedProjectId}&provider_prefix=alertmanager&limit=20`;
const recurrenceUrl = `${API_BASE}/api/v1/platform/events/dossier/recurrence?project_id=${encodedProjectId}&limit=100`;
const sloUrl = `${API_BASE}/api/v1/ai/slo`;
@@ -4046,6 +4298,7 @@ export default function AwoooPWorkItemsPage() {
knowledgeStaleCandidates,
knowledgeStaleOwnerReviews,
knowledgeStaleOwnerReviewBurnDown,
knowledgeStaleOwnerReviewCompletionQueue,
channelEvents,
eventRecurrence,
slo,
@@ -4061,6 +4314,7 @@ export default function AwoooPWorkItemsPage() {
fetchJson<KnowledgeStaleCandidatesResponse>(knowledgeStaleCandidatesUrl),
fetchJson<KnowledgeStaleOwnerReviewInboxResponse>(knowledgeStaleOwnerReviewsUrl),
fetchJson<KnowledgeStaleOwnerReviewBurnDownResponse>(knowledgeStaleOwnerReviewBurnDownUrl),
fetchJson<KnowledgeStaleOwnerReviewCompletionQueueResponse>(knowledgeStaleOwnerReviewCompletionQueueUrl),
fetchJson<RecentEventsResponse>(channelEventsUrl),
fetchJson<RecurrenceResponse>(recurrenceUrl),
fetchJson<SloResponse>(sloUrl),
@@ -4093,6 +4347,7 @@ export default function AwoooPWorkItemsPage() {
knowledgeStaleCandidates,
knowledgeStaleOwnerReviews,
knowledgeStaleOwnerReviewBurnDown,
knowledgeStaleOwnerReviewCompletionQueue,
channelEvents,
eventRecurrence,
slo,
@@ -4222,6 +4477,7 @@ export default function AwoooPWorkItemsPage() {
staleCandidates={telemetry.knowledgeStaleCandidates}
ownerReviewInbox={telemetry.knowledgeStaleOwnerReviews}
burnDown={telemetry.knowledgeStaleOwnerReviewBurnDown}
completionQueue={telemetry.knowledgeStaleOwnerReviewCompletionQueue}
onArchived={fetchTelemetry}
/>