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