feat(governance): batch queue stale km reviews
All checks were successful
CD Pipeline / tests (push) Successful in 5m47s
Code Review / ai-code-review (push) Successful in 11s
Type Sync Check / check-type-sync (push) Successful in 27s
CD Pipeline / build-and-deploy (push) Successful in 4m13s
CD Pipeline / post-deploy-checks (push) Successful in 2m11s
All checks were successful
CD Pipeline / tests (push) Successful in 5m47s
Code Review / ai-code-review (push) Successful in 11s
Type Sync Check / check-type-sync (push) Successful in 27s
CD Pipeline / build-and-deploy (push) Successful in 4m13s
CD Pipeline / post-deploy-checks (push) Successful in 2m11s
This commit is contained in:
@@ -438,6 +438,57 @@ type KnowledgeStaleOwnerReviewAction = {
|
||||
error: string | null;
|
||||
};
|
||||
|
||||
type KnowledgeStaleOwnerReviewBatchItem = {
|
||||
entry_id: string;
|
||||
title: string;
|
||||
priority_tier: "P0" | "P1" | "P2";
|
||||
recommended_action: "refresh_with_evidence" | "owner_review" | "archive_or_supersede";
|
||||
status: "would_queue" | "queued" | "already_queued" | "skipped";
|
||||
reason?: string | null;
|
||||
governance_event_id?: string | null;
|
||||
dispatch_id?: string | null;
|
||||
workflow_stage: string;
|
||||
};
|
||||
|
||||
type KnowledgeStaleOwnerReviewBatchResponse = {
|
||||
schema_version?: string;
|
||||
project_id: string;
|
||||
status: "dry_run" | "queued" | "noop_already_queued";
|
||||
owner: string;
|
||||
owner_note?: string | null;
|
||||
dry_run: boolean;
|
||||
priority_tiers: string[];
|
||||
requested_limit: number;
|
||||
candidate_count: number;
|
||||
queued_count: number;
|
||||
already_queued_count: number;
|
||||
skipped_count: number;
|
||||
batch_governance_event_id?: string | null;
|
||||
batch_dispatch_id?: string | null;
|
||||
workflow_stage: string;
|
||||
writes_km: boolean;
|
||||
writes_governance_audit: boolean;
|
||||
stale_ratio_snapshot?: {
|
||||
stale_count: number;
|
||||
total_count: number;
|
||||
stale_ratio: number;
|
||||
threshold: number;
|
||||
stale_days: number;
|
||||
} | null;
|
||||
dry_run_plan_fingerprint?: string | null;
|
||||
items: KnowledgeStaleOwnerReviewBatchItem[];
|
||||
next_action: string;
|
||||
generated_at?: string | null;
|
||||
};
|
||||
|
||||
type KnowledgeStaleOwnerReviewBatchAction = {
|
||||
previewLoading: boolean;
|
||||
confirmLoading: boolean;
|
||||
previewResult: KnowledgeStaleOwnerReviewBatchResponse | null;
|
||||
result: KnowledgeStaleOwnerReviewBatchResponse | null;
|
||||
error: string | null;
|
||||
};
|
||||
|
||||
type KnowledgeStaleOwnerReviewCompleteResponse = {
|
||||
schema_version?: string;
|
||||
entry_id: string;
|
||||
@@ -872,6 +923,9 @@ function governanceKmStageKey(stage?: string | null) {
|
||||
stage === "detected" ||
|
||||
stage === "ai_analyzed" ||
|
||||
stage === "draft_km_updates" ||
|
||||
stage === "batch_owner_review_previewed" ||
|
||||
stage === "batch_owner_review_queued" ||
|
||||
stage === "batch_noop_already_queued" ||
|
||||
stage === "waiting_owner_review" ||
|
||||
stage === "owner_updates_or_archives_km" ||
|
||||
stage === "km_writeback_after_approval" ||
|
||||
@@ -1152,6 +1206,29 @@ function kmStaleReviewStatusKey(value: string | null | undefined) {
|
||||
}
|
||||
}
|
||||
|
||||
function kmStaleBatchStatusKey(value: string | null | undefined) {
|
||||
switch (value) {
|
||||
case "dry_run":
|
||||
case "queued":
|
||||
case "noop_already_queued":
|
||||
return value;
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
function kmStaleBatchItemStatusKey(value: string | null | undefined) {
|
||||
switch (value) {
|
||||
case "would_queue":
|
||||
case "queued":
|
||||
case "already_queued":
|
||||
case "skipped":
|
||||
return value;
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
function kmStaleReviewCompleteStatusKey(value: string | null | undefined) {
|
||||
switch (value) {
|
||||
case "dry_run":
|
||||
@@ -2170,6 +2247,13 @@ function KnowledgeGovernancePanel({
|
||||
const t = useTranslations("awooop.workItems.knowledgeGovernance");
|
||||
const [archiveActions, setArchiveActions] = useState<Record<string, KnowledgeReviewDraftArchiveAction>>({});
|
||||
const [staleReviewActions, setStaleReviewActions] = useState<Record<string, KnowledgeStaleOwnerReviewAction>>({});
|
||||
const [staleBatchAction, setStaleBatchAction] = useState<KnowledgeStaleOwnerReviewBatchAction>({
|
||||
previewLoading: false,
|
||||
confirmLoading: false,
|
||||
previewResult: null,
|
||||
result: null,
|
||||
error: null,
|
||||
});
|
||||
const [staleReviewCompletionActions, setStaleReviewCompletionActions] =
|
||||
useState<Record<string, KnowledgeStaleOwnerReviewCompleteAction>>({});
|
||||
const items = queue?.items ?? [];
|
||||
@@ -2275,6 +2359,79 @@ function KnowledgeGovernancePanel({
|
||||
}
|
||||
}, [archiveActions, onArchived, t]);
|
||||
|
||||
const previewStaleBatchQueue = useCallback(async () => {
|
||||
setStaleBatchAction((current) => ({
|
||||
previewLoading: true,
|
||||
confirmLoading: false,
|
||||
previewResult: current.previewResult,
|
||||
result: null,
|
||||
error: null,
|
||||
}));
|
||||
const result = await postJson<KnowledgeStaleOwnerReviewBatchResponse>(
|
||||
`${API_BASE}/api/v1/ai/governance/km-stale-candidates/batch-queue-review`,
|
||||
{
|
||||
project_id: staleCandidates?.project_id ?? "awoooi",
|
||||
priority_tiers: ["P0", "P1"],
|
||||
limit: 10,
|
||||
owner: "operator_console",
|
||||
owner_note: "p0_p1_stale_km_batch",
|
||||
dry_run: true,
|
||||
},
|
||||
15000
|
||||
);
|
||||
setStaleBatchAction({
|
||||
previewLoading: false,
|
||||
confirmLoading: false,
|
||||
previewResult: result,
|
||||
result: null,
|
||||
error: result ? null : t("staleCandidates.batchActions.previewFailed"),
|
||||
});
|
||||
}, [staleCandidates?.project_id, t]);
|
||||
|
||||
const confirmStaleBatchQueue = useCallback(async () => {
|
||||
const fingerprint = staleBatchAction.previewResult?.dry_run_plan_fingerprint;
|
||||
if (!fingerprint) {
|
||||
setStaleBatchAction((current) => ({
|
||||
previewLoading: false,
|
||||
confirmLoading: false,
|
||||
previewResult: current.previewResult,
|
||||
result: current.result,
|
||||
error: t("staleCandidates.batchActions.missingPreviewFingerprint"),
|
||||
}));
|
||||
return;
|
||||
}
|
||||
setStaleBatchAction((current) => ({
|
||||
previewLoading: false,
|
||||
confirmLoading: true,
|
||||
previewResult: current.previewResult,
|
||||
result: null,
|
||||
error: null,
|
||||
}));
|
||||
const result = await postJson<KnowledgeStaleOwnerReviewBatchResponse>(
|
||||
`${API_BASE}/api/v1/ai/governance/km-stale-candidates/batch-queue-review`,
|
||||
{
|
||||
project_id: staleCandidates?.project_id ?? "awoooi",
|
||||
priority_tiers: ["P0", "P1"],
|
||||
limit: 10,
|
||||
owner: "operator_console",
|
||||
owner_note: "p0_p1_stale_km_batch",
|
||||
dry_run: false,
|
||||
dry_run_plan_fingerprint: fingerprint,
|
||||
},
|
||||
15000
|
||||
);
|
||||
setStaleBatchAction((current) => ({
|
||||
previewLoading: false,
|
||||
confirmLoading: false,
|
||||
previewResult: current.previewResult,
|
||||
result,
|
||||
error: result ? null : t("staleCandidates.batchActions.confirmFailed"),
|
||||
}));
|
||||
if (result?.status === "queued" || result?.status === "noop_already_queued") {
|
||||
onArchived();
|
||||
}
|
||||
}, [onArchived, staleBatchAction.previewResult, staleCandidates?.project_id, t]);
|
||||
|
||||
const queueStaleOwnerReview = useCallback(async (candidate: KnowledgeStaleCandidate) => {
|
||||
setStaleReviewActions((current) => ({
|
||||
...current,
|
||||
@@ -2419,6 +2576,12 @@ function KnowledgeGovernancePanel({
|
||||
}
|
||||
}, [onArchived, staleReviewActions, staleReviewCompletionActions, t]);
|
||||
|
||||
const staleBatchPreview = staleBatchAction.previewResult;
|
||||
const staleBatchResult = staleBatchAction.result;
|
||||
const staleBatchPreviewReady = Boolean(staleBatchPreview?.dry_run_plan_fingerprint);
|
||||
const staleBatchPreviewStatusKey = kmStaleBatchStatusKey(staleBatchPreview?.status);
|
||||
const staleBatchResultStatusKey = kmStaleBatchStatusKey(staleBatchResult?.status);
|
||||
|
||||
return (
|
||||
<section className="border border-[#e0ddd4] bg-white">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-[#e0ddd4] bg-[#faf9f3] px-4 py-3">
|
||||
@@ -2557,6 +2720,115 @@ function KnowledgeGovernancePanel({
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 border border-[#e0ddd4] bg-white px-3 py-2 text-xs text-[#5f5b52]">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<p className="font-semibold text-[#141413]">
|
||||
{t("staleCandidates.batchActions.title")}
|
||||
</p>
|
||||
<p className="mt-1 text-[11px] leading-5 text-[#77736a]">
|
||||
{t("staleCandidates.batchActions.subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={previewStaleBatchQueue}
|
||||
disabled={staleBatchAction.previewLoading || staleBatchAction.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-[#9bc7a4] hover:bg-[#f0faf2] hover:text-[#17602a] disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
<SearchCheck className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
{staleBatchAction.previewLoading
|
||||
? t("staleCandidates.batchActions.previewing")
|
||||
: t("staleCandidates.batchActions.preview")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={confirmStaleBatchQueue}
|
||||
disabled={
|
||||
!staleBatchPreviewReady ||
|
||||
staleBatchAction.previewLoading ||
|
||||
staleBatchAction.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" />
|
||||
{staleBatchAction.confirmLoading
|
||||
? t("staleCandidates.batchActions.confirming")
|
||||
: t("staleCandidates.batchActions.confirm")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{staleBatchAction.error ? (
|
||||
<p className="mt-2 border border-[#e2a29b] bg-[#fff0ef] px-2 py-1 text-[11px] leading-5 text-[#9f2f25]">
|
||||
{staleBatchAction.error}
|
||||
</p>
|
||||
) : null}
|
||||
{staleBatchPreview ? (
|
||||
<div className="mt-2 border border-[#d9b36f] bg-[#fff7e8] px-2 py-1.5 text-[11px] leading-5 text-[#8a5a08]">
|
||||
<p className="font-semibold">
|
||||
{t(
|
||||
`staleCandidates.batchActions.statuses.${staleBatchPreviewStatusKey}` as never
|
||||
)}
|
||||
</p>
|
||||
<p className="mt-1 text-[#5f5b52]">
|
||||
{t("staleCandidates.batchActions.summary", {
|
||||
candidates: staleBatchPreview.candidate_count,
|
||||
queued: staleBatchPreview.queued_count,
|
||||
already: staleBatchPreview.already_queued_count,
|
||||
skipped: staleBatchPreview.skipped_count,
|
||||
writesKm: String(staleBatchPreview.writes_km),
|
||||
writesAudit: String(staleBatchPreview.writes_governance_audit),
|
||||
})}
|
||||
</p>
|
||||
<p className="mt-1 break-all font-mono text-[11px] text-[#5f5b52]">
|
||||
{t("staleCandidates.batchActions.planFingerprint", {
|
||||
fingerprint: staleBatchPreview.dry_run_plan_fingerprint ?? "--",
|
||||
})}
|
||||
</p>
|
||||
{staleBatchPreview.stale_ratio_snapshot ? (
|
||||
<p className="mt-1 text-[#5f5b52]">
|
||||
{t("staleCandidates.completeActions.snapshot", {
|
||||
stale: staleBatchPreview.stale_ratio_snapshot.stale_count,
|
||||
total: staleBatchPreview.stale_ratio_snapshot.total_count,
|
||||
ratio: formatStaleRatio(staleBatchPreview.stale_ratio_snapshot.stale_ratio),
|
||||
threshold: formatStaleRatio(staleBatchPreview.stale_ratio_snapshot.threshold),
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{staleBatchPreview.items.slice(0, 5).map((item) => (
|
||||
<span
|
||||
key={`${item.entry_id}-${item.status}`}
|
||||
className="border border-[#d8d3c7] bg-white px-2 py-0.5 font-mono text-[11px] leading-5 text-[#5f5b52]"
|
||||
>
|
||||
{item.priority_tier}:{t(
|
||||
`staleCandidates.batchActions.itemStatuses.${kmStaleBatchItemStatusKey(item.status)}` as never
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{staleBatchResult ? (
|
||||
<div className="mt-2 border border-[#9bc7a4] bg-[#f0faf2] px-2 py-1.5 text-[11px] leading-5 text-[#17602a]">
|
||||
<p className="font-semibold">
|
||||
{t(
|
||||
`staleCandidates.batchActions.statuses.${staleBatchResultStatusKey}` as never
|
||||
)}
|
||||
</p>
|
||||
<p className="mt-1 text-[#5f5b52]">
|
||||
{t("staleCandidates.batchActions.result", {
|
||||
batch: staleBatchResult.batch_dispatch_id ?? "--",
|
||||
event: staleBatchResult.batch_governance_event_id ?? "--",
|
||||
queued: staleBatchResult.queued_count,
|
||||
already: staleBatchResult.already_queued_count,
|
||||
skipped: staleBatchResult.skipped_count,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{staleCandidates === null ? (
|
||||
<p className="text-xs text-[#8a5a08]">
|
||||
{t("staleCandidates.unavailable")}
|
||||
|
||||
Reference in New Issue
Block a user