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
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:
@@ -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}
|
||||
/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user