feat(governance): surface stale km priority queue
Some checks failed
CD Pipeline / tests (push) Successful in 5m29s
Code Review / ai-code-review (push) Successful in 11s
Type Sync Check / check-type-sync (push) Successful in 32s
CD Pipeline / build-and-deploy (push) Failing after 5m43s
CD Pipeline / post-deploy-checks (push) Has been skipped

This commit is contained in:
Your Name
2026-05-24 16:46:14 +08:00
parent b87090be01
commit 841b057ada
7 changed files with 680 additions and 1 deletions

View File

@@ -2021,6 +2021,7 @@
"knowledgeOwner": "Lead: {lead}; human review: {human}",
"knowledgeNext": "Next action: {action}",
"knowledgeDrafts": "KM review drafts: {drafts}; duplicate drafts: {duplicates}",
"knowledgeStaleCandidates": "Stale KM priority queue: {total}; top {top} / {tier}",
"knowledgeEmpty": "No recent knowledge_degradation dispatch trail",
"frontendConsole": "This page now reads production APIs instead of a static list",
"mcpReady": "MCP Gateway gate is not currently a top gap",
@@ -2074,6 +2075,49 @@
"archiveProposal": "Archive candidates: {count} duplicate drafts",
"ownerAction": "Owner action: {action}",
"readOnlyPlan": "Writes on read: {writes}; archive blocked before review: {blocked}",
"staleCandidates": {
"title": "Stale KM Priority Queue",
"total": "Stale {count}",
"returned": "Shown {count}",
"threshold": "Threshold {days}d",
"unavailable": "The stale candidates API has not responded; only the aggregate count is visible.",
"empty": "No KM entries are currently past the stale threshold.",
"meta": "Stale {days}d; score {score}; views {views}",
"action": "Recommended: {action}",
"sources": "Sources: {sources}",
"refs": "Incident: {incident}; PlayBook: {playbook}; Approval: {approval}",
"noSources": "No Incident / Sentry / SigNoz / PlayBook link yet",
"openKnowledge": "Open KM",
"guardrail": "Guardrail: writes on read={writes}; manual review={review}",
"actions": {
"refresh_with_evidence": "Refresh with Incident / Sentry / SigNoz / PlayBook evidence",
"owner_review": "Route to owner review",
"archive_or_supersede": "Archive or supersede"
},
"correlationSources": {
"incident": "Incident",
"approval": "Approval",
"playbook": "PlayBook",
"sentry": "Sentry",
"signoz": "SigNoz",
"unknown": "Unknown source"
},
"reasons": {
"linked_incident": "Linked Incident",
"linked_approval": "Linked Approval",
"linked_playbook": "Linked PlayBook",
"sentry_context": "Sentry context",
"signoz_context": "SigNoz context",
"anti_pattern_priority": "Anti-Pattern priority",
"auto_runbook_review_needed": "Auto-runbook review",
"ai_extracted_needs_owner_check": "AI extraction needs review",
"already_waiting_review": "Already waiting review",
"viewed_by_operator": "Viewed by operator",
"older_than_30_days": "Older than 30 days",
"stale_by_age": "Past stale threshold",
"unknown": "Unknown reason"
}
},
"openEventHistory": "Open Event History",
"ownerActions": {
"owner_review_canonical_then_archive_duplicates": "Review the canonical draft, then archive duplicates",

View File

@@ -2022,6 +2022,7 @@
"knowledgeOwner": "主責:{lead};人工覆核:{human}",
"knowledgeNext": "下一步:{action}",
"knowledgeDrafts": "KM 審核草稿:{drafts};重複草稿:{duplicates}",
"knowledgeStaleCandidates": "陳舊 KM 優先清單:{total} 筆;最高 {top} / {tier}",
"knowledgeEmpty": "近期沒有 knowledge_degradation dispatch trail",
"frontendConsole": "本頁已改讀 production API而非靜態清單",
"mcpReady": "MCP Gateway gate 目前未列為主要缺口",
@@ -2075,6 +2076,49 @@
"archiveProposal": "封存候選:{count} 份重複草稿",
"ownerAction": "Owner 動作:{action}",
"readOnlyPlan": "讀取不寫入:{writes};未審核不封存:{blocked}",
"staleCandidates": {
"title": "陳舊 KM 優先處理清單",
"total": "陳舊 {count}",
"returned": "顯示 {count}",
"threshold": "門檻 {days} 天",
"unavailable": "stale candidates API 尚未回應;目前只能看到總數,無法排序處理。",
"empty": "目前沒有超過門檻的陳舊 KM。",
"meta": "陳舊 {days} 天;分數 {score};瀏覽 {views}",
"action": "建議:{action}",
"sources": "關聯來源:{sources}",
"refs": "Incident{incident}PlayBook{playbook}Approval{approval}",
"noSources": "尚無 Incident / Sentry / SigNoz / PlayBook 關聯",
"openKnowledge": "開啟 KM",
"guardrail": "防護:讀取不寫入={writes};人工覆核={review}",
"actions": {
"refresh_with_evidence": "依 Incident / Sentry / SigNoz / PlayBook 證據刷新",
"owner_review": "交由 owner 審核內容",
"archive_or_supersede": "封存或以新條目取代"
},
"correlationSources": {
"incident": "Incident",
"approval": "Approval",
"playbook": "PlayBook",
"sentry": "Sentry",
"signoz": "SigNoz",
"unknown": "未知來源"
},
"reasons": {
"linked_incident": "關聯 Incident",
"linked_approval": "關聯 Approval",
"linked_playbook": "關聯 PlayBook",
"sentry_context": "含 Sentry 脈絡",
"signoz_context": "含 SigNoz 脈絡",
"anti_pattern_priority": "Anti-Pattern 優先",
"auto_runbook_review_needed": "自動 Runbook 待審",
"ai_extracted_needs_owner_check": "AI 萃取需覆核",
"already_waiting_review": "已在審核狀態",
"viewed_by_operator": "近期有人查看",
"older_than_30_days": "超過 30 天",
"stale_by_age": "超過陳舊門檻",
"unknown": "未知原因"
}
},
"openEventHistory": "開啟事件歷史",
"ownerActions": {
"owner_review_canonical_then_archive_duplicates": "審核 canonical 草稿後封存 duplicates",

View File

@@ -377,6 +377,40 @@ type KnowledgeReviewDraftArchiveAction = {
error: string | null;
};
type KnowledgeStaleCandidate = {
entry_id: string;
project_id: string;
title: string;
entry_type: string;
category?: string | null;
status: string;
source?: string | null;
updated_at?: string | null;
stale_days: number;
view_count: number;
priority_score: number;
priority_tier: "P0" | "P1" | "P2";
recommended_action: "refresh_with_evidence" | "owner_review" | "archive_or_supersede";
reasons: string[];
correlation_sources: string[];
related_incident_id?: string | null;
related_playbook_id?: string | null;
related_approval_id?: string | null;
tags: string[];
};
type KnowledgeStaleCandidatesResponse = {
schema_version?: string;
project_id: string;
total_stale: number;
returned: number;
threshold_days: number;
writes_on_read: boolean;
manual_review_required: boolean;
items: KnowledgeStaleCandidate[];
generated_at?: string | null;
};
type DriftFingerprintState = {
schema_version?: string;
namespace?: string;
@@ -459,6 +493,7 @@ type Telemetry = {
governanceKnowledgeQueue: GovernanceQueueResponse | null;
knowledgeReviewDrafts: KnowledgeListResponse | null;
knowledgeReviewDedupe: KnowledgeReviewDraftDedupeResponse | null;
knowledgeStaleCandidates: KnowledgeStaleCandidatesResponse | null;
channelEvents: RecentEventsResponse | null;
eventRecurrence: RecurrenceResponse | null;
slo: SloResponse | null;
@@ -1009,6 +1044,50 @@ function formatStaleRatio(value: number) {
return `${(value * 100).toFixed(1)}%`;
}
function kmStaleReasonKey(value: string | null | undefined) {
switch (value) {
case "linked_incident":
case "linked_approval":
case "linked_playbook":
case "sentry_context":
case "signoz_context":
case "anti_pattern_priority":
case "auto_runbook_review_needed":
case "ai_extracted_needs_owner_check":
case "already_waiting_review":
case "viewed_by_operator":
case "older_than_30_days":
case "stale_by_age":
return value;
default:
return "unknown";
}
}
function kmStaleActionKey(value: string | null | undefined) {
switch (value) {
case "refresh_with_evidence":
case "owner_review":
case "archive_or_supersede":
return value;
default:
return "owner_review";
}
}
function kmCorrelationSourceKey(value: string | null | undefined) {
switch (value) {
case "incident":
case "approval":
case "playbook":
case "sentry":
case "signoz":
return value;
default:
return "unknown";
}
}
function governanceEventHistoryHref(eventId: string) {
return `/governance?tab=events&event_id=${encodeURIComponent(eventId)}`;
}
@@ -1034,6 +1113,8 @@ function buildWorkItems(
);
const knowledgeDedupeDuplicates =
telemetry.knowledgeReviewDedupe?.duplicate_draft_total ?? knowledgeDuplicateDrafts;
const knowledgeStaleTotal = telemetry.knowledgeStaleCandidates?.total_stale ?? 0;
const topKnowledgeStaleCandidate = telemetry.knowledgeStaleCandidates?.items?.[0] ?? null;
const remediationQueue = telemetry.slo?.adr100?.verification_coverage?.remediation_queue;
const remediationTotal = remediationQueue?.total ?? 0;
const remediationReadyForAi = remediationQueue?.ready_for_ai ?? 0;
@@ -1282,8 +1363,20 @@ function buildWorkItems(
?? 0,
duplicates: knowledgeDedupeDuplicates,
}),
t("evidence.knowledgeStaleCandidates", {
total: knowledgeStaleTotal,
top: topKnowledgeStaleCandidate?.entry_id ?? "--",
tier: topKnowledgeStaleCandidate?.priority_tier ?? "--",
}),
]
: [t("evidence.knowledgeEmpty")],
: [
t("evidence.knowledgeEmpty"),
t("evidence.knowledgeStaleCandidates", {
total: knowledgeStaleTotal,
top: topKnowledgeStaleCandidate?.entry_id ?? "--",
tier: topKnowledgeStaleCandidate?.priority_tier ?? "--",
}),
],
href: "/awooop/work-items",
},
{
@@ -1970,11 +2063,13 @@ function KnowledgeGovernancePanel({
queue,
reviewDrafts,
dedupe,
staleCandidates,
onArchived,
}: {
queue: GovernanceQueueResponse | null;
reviewDrafts: KnowledgeListResponse | null;
dedupe: KnowledgeReviewDraftDedupeResponse | null;
staleCandidates: KnowledgeStaleCandidatesResponse | null;
onArchived: () => void;
}) {
const t = useTranslations("awooop.workItems.knowledgeGovernance");
@@ -1986,6 +2081,7 @@ function KnowledgeGovernancePanel({
dedupe?.duplicate_draft_total
?? draftGroups.reduce((sum, group) => sum + group.duplicateCount, 0)
);
const staleCandidateItems = staleCandidates?.items ?? [];
const draftTotal = dedupe?.total_review_drafts ?? reviewDrafts?.total ?? 0;
const activeCount = items.filter((item) =>
["pending", "dispatched", "executing"].includes(item.dispatch_status)
@@ -2193,6 +2289,123 @@ function KnowledgeGovernancePanel({
</div>
)}
<div className="border-t border-[#e0ddd4] bg-[#faf9f3] px-4 py-3">
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
<div className="flex items-center gap-2">
<Database className="h-4 w-4 text-[#5f5b52]" aria-hidden="true" />
<h4 className="text-xs font-semibold uppercase tracking-[0.08em] text-[#5f5b52]">
{t("staleCandidates.title")}
</h4>
</div>
<div className="flex flex-wrap items-center gap-2 font-mono text-[11px] text-[#5f5b52]">
<span className="border border-[#d8d3c7] bg-white px-2 py-0.5">
{t("staleCandidates.total", {
count: staleCandidates?.total_stale ?? 0,
})}
</span>
<span className="border border-[#d8d3c7] bg-white px-2 py-0.5">
{t("staleCandidates.returned", {
count: staleCandidates?.returned ?? 0,
})}
</span>
<span className="border border-[#d8d3c7] bg-white px-2 py-0.5">
{t("staleCandidates.threshold", {
days: staleCandidates?.threshold_days ?? 7,
})}
</span>
</div>
</div>
{staleCandidates === null ? (
<p className="text-xs text-[#8a5a08]">
{t("staleCandidates.unavailable")}
</p>
) : staleCandidateItems.length === 0 ? (
<p className="text-xs text-[#5f5b52]">
{t("staleCandidates.empty")}
</p>
) : (
<div className="grid gap-2 md:grid-cols-2">
{staleCandidateItems.slice(0, 6).map((candidate) => (
<article
key={candidate.entry_id}
className="border border-[#e0ddd4] bg-white px-3 py-2 text-xs text-[#5f5b52]"
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="truncate font-semibold text-[#141413]">
{candidate.title}
</p>
<p className="mt-1 truncate font-mono text-[11px] text-[#77736a]">
{candidate.entry_id}
</p>
</div>
<span className="shrink-0 border border-[#d9b36f] bg-[#fff7e8] px-2 py-0.5 font-mono font-semibold text-[#8a5a08]">
{candidate.priority_tier}
</span>
</div>
<div className="mt-2 grid gap-1 leading-5">
<p>
{t("staleCandidates.meta", {
days: candidate.stale_days,
score: candidate.priority_score,
views: candidate.view_count,
})}
</p>
<p>
{t("staleCandidates.action", {
action: t(
`staleCandidates.actions.${kmStaleActionKey(candidate.recommended_action)}` as never
),
})}
</p>
<p>
{t("staleCandidates.sources", {
sources: candidate.correlation_sources.length
? candidate.correlation_sources
.map((source) => t(
`staleCandidates.correlationSources.${kmCorrelationSourceKey(source)}` as never
))
.join(" / ")
: t("staleCandidates.noSources"),
})}
</p>
<p>
{t("staleCandidates.refs", {
incident: candidate.related_incident_id ?? "--",
playbook: candidate.related_playbook_id ?? "--",
approval: candidate.related_approval_id ?? "--",
})}
</p>
</div>
<div className="mt-2 flex flex-wrap gap-1">
{candidate.reasons.slice(0, 5).map((reason) => (
<span
key={`${candidate.entry_id}-${reason}`}
className="border border-[#d8d3c7] bg-[#faf9f3] px-2 py-0.5 leading-5"
>
{t(`staleCandidates.reasons.${kmStaleReasonKey(reason)}` as never)}
</span>
))}
</div>
<Link
href={`/knowledge-base?q=${encodeURIComponent(candidate.title)}`}
className="mt-2 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]"
>
<ArrowRight className="h-3.5 w-3.5" aria-hidden="true" />
{t("staleCandidates.openKnowledge")}
</Link>
</article>
))}
</div>
)}
<p className="mt-3 text-[11px] leading-5 text-[#77736a]">
{t("staleCandidates.guardrail", {
writes: String(staleCandidates?.writes_on_read ?? false),
review: String(staleCandidates?.manual_review_required ?? true),
})}
</p>
</div>
{dedupe === null && reviewDrafts === null ? (
<div className="border-t border-[#e0ddd4] px-4 py-3 text-xs text-[#8a5a08]">
{t("draftsUnavailable")}
@@ -2705,6 +2918,7 @@ export default function AwoooPWorkItemsPage() {
governanceKnowledgeQueue: null,
knowledgeReviewDrafts: null,
knowledgeReviewDedupe: null,
knowledgeStaleCandidates: null,
channelEvents: null,
eventRecurrence: null,
slo: null,
@@ -2724,6 +2938,7 @@ export default function AwoooPWorkItemsPage() {
const governanceKnowledgeQueueUrl = `${API_BASE}/api/v1/ai/governance/queue?dispatch_status=all&event_type=knowledge_degradation&size=20`;
const knowledgeReviewDraftsUrl = `${API_BASE}/api/v1/knowledge?entry_type=auto_runbook&status=review&q=${encodeURIComponent("KM healthcheck")}&limit=100`;
const knowledgeReviewDedupeUrl = `${API_BASE}/api/v1/ai/governance/km-review-drafts/dedupe?limit=100`;
const knowledgeStaleCandidatesUrl = `${API_BASE}/api/v1/ai/governance/km-stale-candidates?project_id=${encodedProjectId}&limit=20`;
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`;
@@ -2737,6 +2952,7 @@ export default function AwoooPWorkItemsPage() {
governanceKnowledgeQueue,
knowledgeReviewDrafts,
knowledgeReviewDedupe,
knowledgeStaleCandidates,
channelEvents,
eventRecurrence,
slo,
@@ -2749,6 +2965,7 @@ export default function AwoooPWorkItemsPage() {
fetchJson<GovernanceQueueResponse>(governanceKnowledgeQueueUrl),
fetchJson<KnowledgeListResponse>(knowledgeReviewDraftsUrl),
fetchJson<KnowledgeReviewDraftDedupeResponse>(knowledgeReviewDedupeUrl),
fetchJson<KnowledgeStaleCandidatesResponse>(knowledgeStaleCandidatesUrl),
fetchJson<RecentEventsResponse>(channelEventsUrl),
fetchJson<RecurrenceResponse>(recurrenceUrl),
fetchJson<SloResponse>(sloUrl),
@@ -2778,6 +2995,7 @@ export default function AwoooPWorkItemsPage() {
governanceKnowledgeQueue,
knowledgeReviewDrafts,
knowledgeReviewDedupe,
knowledgeStaleCandidates,
channelEvents,
eventRecurrence,
slo,
@@ -2904,6 +3122,7 @@ export default function AwoooPWorkItemsPage() {
queue={telemetry.governanceKnowledgeQueue}
reviewDrafts={telemetry.knowledgeReviewDrafts}
dedupe={telemetry.knowledgeReviewDedupe}
staleCandidates={telemetry.knowledgeStaleCandidates}
onArchived={fetchTelemetry}
/>