feat(governance): expose km draft dedupe plan
All checks were successful
Code Review / ai-code-review (push) Successful in 11s
Type Sync Check / check-type-sync (push) Successful in 33s
CD Pipeline / tests (push) Successful in 4m3s
E2E Health Check / e2e-health (push) Successful in 23s
CD Pipeline / build-and-deploy (push) Successful in 4m54s
CD Pipeline / post-deploy-checks (push) Successful in 2m9s

This commit is contained in:
Your Name
2026-05-19 23:56:03 +08:00
parent 65badab6fd
commit 0cd6301d0e
8 changed files with 507 additions and 8 deletions

View File

@@ -250,6 +250,30 @@ type KnowledgeReviewDraftGroup = {
duplicateCount: number;
};
type KnowledgeReviewDraftDedupeGroup = {
governance_event_id: string;
canonical_entry_id: string;
canonical_title: string;
canonical_updated_at?: string | null;
preferred_source: "dispatch_context" | "latest_review_draft";
duplicate_entry_ids: string[];
duplicate_count: number;
total_entries: number;
suggested_action: string;
owner_action: string;
writes_on_read: boolean;
can_archive_without_owner_approval: boolean;
};
type KnowledgeReviewDraftDedupeResponse = {
schema_version?: string;
total_review_drafts: number;
event_group_total: number;
duplicate_draft_total: number;
groups: KnowledgeReviewDraftDedupeGroup[];
generated_at?: string | null;
};
type DriftFingerprintState = {
schema_version?: string;
namespace?: string;
@@ -331,6 +355,7 @@ type Telemetry = {
governanceQueue: GovernanceQueueResponse | null;
governanceKnowledgeQueue: GovernanceQueueResponse | null;
knowledgeReviewDrafts: KnowledgeListResponse | null;
knowledgeReviewDedupe: KnowledgeReviewDraftDedupeResponse | null;
channelEvents: RecentEventsResponse | null;
eventRecurrence: RecurrenceResponse | null;
slo: SloResponse | null;
@@ -698,6 +723,23 @@ function draftGroupForQueueItem(
return groups.find((group) => group.eventId === item.governance_event_id) ?? null;
}
function draftDedupeGroupForQueueItem(
item: GovernanceQueueItem,
groups: KnowledgeReviewDraftDedupeGroup[]
) {
return groups.find((group) => group.governance_event_id === item.governance_event_id) ?? null;
}
function kmDedupeActionKey(action?: string | null) {
if (
action === "owner_review_canonical_then_archive_duplicates" ||
action === "review_canonical_and_archive_duplicate_drafts"
) {
return action;
}
return "unknown";
}
function buildWorkItems(
telemetry: Telemetry,
t: ReturnType<typeof useTranslations>
@@ -717,6 +759,8 @@ function buildWorkItems(
(sum, group) => sum + group.duplicateCount,
0
);
const knowledgeDedupeDuplicates =
telemetry.knowledgeReviewDedupe?.duplicate_draft_total ?? knowledgeDuplicateDrafts;
const remediationQueue = telemetry.slo?.adr100?.verification_coverage?.remediation_queue;
const remediationTotal = remediationQueue?.total ?? 0;
const remediationReadyForAi = remediationQueue?.ready_for_ai ?? 0;
@@ -939,8 +983,10 @@ function buildWorkItems(
action: latestKnowledgeDispatch.next_action ?? latestKnowledgeDispatch.proposed_action ?? "--",
}),
t("evidence.knowledgeDrafts", {
drafts: telemetry.knowledgeReviewDrafts?.total ?? 0,
duplicates: knowledgeDuplicateDrafts,
drafts: telemetry.knowledgeReviewDedupe?.total_review_drafts
?? telemetry.knowledgeReviewDrafts?.total
?? 0,
duplicates: knowledgeDedupeDuplicates,
}),
]
: [t("evidence.knowledgeEmpty")],
@@ -1381,14 +1427,21 @@ function RecurrenceWorkQueuePanel({
function KnowledgeGovernancePanel({
queue,
reviewDrafts,
dedupe,
}: {
queue: GovernanceQueueResponse | null;
reviewDrafts: KnowledgeListResponse | null;
dedupe: KnowledgeReviewDraftDedupeResponse | null;
}) {
const t = useTranslations("awooop.workItems.knowledgeGovernance");
const items = queue?.items ?? [];
const draftGroups = groupKnowledgeReviewDrafts(reviewDrafts, items);
const duplicateDraftCount = draftGroups.reduce((sum, group) => sum + group.duplicateCount, 0);
const dedupeGroups = dedupe?.groups ?? [];
const duplicateDraftCount = (
dedupe?.duplicate_draft_total
?? draftGroups.reduce((sum, group) => sum + group.duplicateCount, 0)
);
const draftTotal = dedupe?.total_review_drafts ?? reviewDrafts?.total ?? 0;
const activeCount = items.filter((item) =>
["pending", "dispatched", "executing"].includes(item.dispatch_status)
).length;
@@ -1417,7 +1470,7 @@ function KnowledgeGovernancePanel({
{t("review", { count: reviewCount })}
</span>
<span className="border border-[#d8d3c7] bg-white px-2 py-0.5">
{t("drafts", { count: reviewDrafts?.total ?? 0 })}
{t("drafts", { count: draftTotal })}
</span>
<span className="border border-[#d8d3c7] bg-white px-2 py-0.5">
{t("duplicates", { count: duplicateDraftCount })}
@@ -1444,9 +1497,10 @@ function KnowledgeGovernancePanel({
const stageKey = governanceKmStageKey(item.workflow_stage);
const steps = item.workflow_steps ?? [];
const draftGroup = draftGroupForQueueItem(item, draftGroups);
const dedupeGroup = draftDedupeGroupForQueueItem(item, dedupeGroups);
const draftId = item.kb_draft_entry_id ?? draftGroup?.canonical.id ?? null;
const workerStatus = item.worker_status ?? "--";
const duplicateCount = draftGroup?.duplicateCount ?? 0;
const duplicateCount = dedupeGroup?.duplicate_count ?? draftGroup?.duplicateCount ?? 0;
return (
<article key={item.id} className="bg-white px-4 py-3">
@@ -1507,10 +1561,65 @@ function KnowledgeGovernancePanel({
</div>
)}
{reviewDrafts === null ? (
{dedupe === null && reviewDrafts === null ? (
<div className="border-t border-[#e0ddd4] px-4 py-3 text-xs text-[#8a5a08]">
{t("draftsUnavailable")}
</div>
) : dedupeGroups.length > 0 ? (
<div className="border-t border-[#e0ddd4] bg-[#faf9f3] px-4 py-3">
<div className="mb-3 flex items-center gap-2">
<FileText className="h-4 w-4 text-[#5f5b52]" aria-hidden="true" />
<h4 className="text-xs font-semibold uppercase tracking-[0.08em] text-[#5f5b52]">
{t("draftSectionTitle")}
</h4>
</div>
<div className="grid gap-2 md:grid-cols-2">
{dedupeGroups.slice(0, 4).map((group) => (
<div
key={group.governance_event_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-mono font-semibold text-[#141413]">
{group.governance_event_id}
</p>
<p className="mt-1 truncate text-[#77736a]">{group.canonical_title}</p>
</div>
<span className="shrink-0 border border-[#d8d3c7] bg-[#faf9f3] px-2 py-0.5 font-mono">
{group.canonical_entry_id}
</span>
</div>
<div className="mt-2 grid gap-1 leading-5">
<p>
{t("draftGroup", {
count: group.total_entries,
duplicates: group.duplicate_count,
})}
</p>
<p>
{t("archiveProposal", {
count: group.duplicate_count,
})}
</p>
<p>
{t("ownerAction", {
action: t(
`ownerActions.${kmDedupeActionKey(group.owner_action)}` as never
),
})}
</p>
<p>
{t("readOnlyPlan", {
writes: String(group.writes_on_read),
blocked: String(!group.can_archive_without_owner_approval),
})}
</p>
</div>
</div>
))}
</div>
</div>
) : draftGroups.length > 0 ? (
<div className="border-t border-[#e0ddd4] bg-[#faf9f3] px-4 py-3">
<div className="mb-3 flex items-center gap-2">
@@ -1783,6 +1892,7 @@ export default function AwoooPWorkItemsPage() {
governanceQueue: null,
governanceKnowledgeQueue: null,
knowledgeReviewDrafts: null,
knowledgeReviewDedupe: null,
channelEvents: null,
eventRecurrence: null,
slo: null,
@@ -1801,6 +1911,7 @@ export default function AwoooPWorkItemsPage() {
const governanceQueueUrl = `${API_BASE}/api/v1/ai/governance/queue?dispatch_status=pending&size=10`;
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 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`;
@@ -1813,6 +1924,7 @@ export default function AwoooPWorkItemsPage() {
governanceQueue,
governanceKnowledgeQueue,
knowledgeReviewDrafts,
knowledgeReviewDedupe,
channelEvents,
eventRecurrence,
slo,
@@ -1824,6 +1936,7 @@ export default function AwoooPWorkItemsPage() {
fetchJson<GovernanceQueueResponse>(governanceQueueUrl),
fetchJson<GovernanceQueueResponse>(governanceKnowledgeQueueUrl),
fetchJson<KnowledgeListResponse>(knowledgeReviewDraftsUrl),
fetchJson<KnowledgeReviewDraftDedupeResponse>(knowledgeReviewDedupeUrl),
fetchJson<RecentEventsResponse>(channelEventsUrl),
fetchJson<RecurrenceResponse>(recurrenceUrl),
fetchJson<SloResponse>(sloUrl),
@@ -1852,6 +1965,7 @@ export default function AwoooPWorkItemsPage() {
governanceQueue,
governanceKnowledgeQueue,
knowledgeReviewDrafts,
knowledgeReviewDedupe,
channelEvents,
eventRecurrence,
slo,
@@ -1975,6 +2089,7 @@ export default function AwoooPWorkItemsPage() {
<KnowledgeGovernancePanel
queue={telemetry.governanceKnowledgeQueue}
reviewDrafts={telemetry.knowledgeReviewDrafts}
dedupe={telemetry.knowledgeReviewDedupe}
/>
<DriftFingerprintPanel