feat(governance): complete stale km owner review
All checks were successful
CD Pipeline / tests (push) Successful in 5m28s
Code Review / ai-code-review (push) Successful in 12s
Type Sync Check / check-type-sync (push) Successful in 26s
CD Pipeline / build-and-deploy (push) Successful in 5m12s
CD Pipeline / post-deploy-checks (push) Successful in 1m31s
All checks were successful
CD Pipeline / tests (push) Successful in 5m28s
Code Review / ai-code-review (push) Successful in 12s
Type Sync Check / check-type-sync (push) Successful in 26s
CD Pipeline / build-and-deploy (push) Successful in 5m12s
CD Pipeline / post-deploy-checks (push) Successful in 1m31s
This commit is contained in:
@@ -397,6 +397,10 @@ type KnowledgeStaleCandidate = {
|
||||
related_playbook_id?: string | null;
|
||||
related_approval_id?: string | null;
|
||||
tags: string[];
|
||||
owner_review_dispatch_id?: string | null;
|
||||
owner_review_status?: string | null;
|
||||
owner_review_stage?: string | null;
|
||||
owner_review_next_action?: string | null;
|
||||
};
|
||||
|
||||
type KnowledgeStaleCandidatesResponse = {
|
||||
@@ -434,6 +438,42 @@ type KnowledgeStaleOwnerReviewAction = {
|
||||
error: string | null;
|
||||
};
|
||||
|
||||
type KnowledgeStaleOwnerReviewCompleteResponse = {
|
||||
schema_version?: string;
|
||||
entry_id: string;
|
||||
project_id: string;
|
||||
status: "dry_run" | "completed" | "already_completed";
|
||||
review_outcome: "refresh_with_evidence" | "archive" | "supersede";
|
||||
governance_event_id: string;
|
||||
dispatch_id: string;
|
||||
audit_dispatch_id?: string | null;
|
||||
stale_ratio_recheck_dispatch_id?: string | null;
|
||||
workflow_stage: string;
|
||||
owner: string;
|
||||
owner_approved: boolean;
|
||||
dry_run: boolean;
|
||||
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;
|
||||
next_action: string;
|
||||
generated_at?: string | null;
|
||||
};
|
||||
|
||||
type KnowledgeStaleOwnerReviewCompleteAction = {
|
||||
previewLoading: boolean;
|
||||
confirmLoading: boolean;
|
||||
previewResult: KnowledgeStaleOwnerReviewCompleteResponse | null;
|
||||
result: KnowledgeStaleOwnerReviewCompleteResponse | null;
|
||||
error: string | null;
|
||||
};
|
||||
|
||||
type DriftFingerprintState = {
|
||||
schema_version?: string;
|
||||
namespace?: string;
|
||||
@@ -833,7 +873,10 @@ function governanceKmStageKey(stage?: string | null) {
|
||||
stage === "ai_analyzed" ||
|
||||
stage === "draft_km_updates" ||
|
||||
stage === "waiting_owner_review" ||
|
||||
stage === "owner_updates_or_archives_km" ||
|
||||
stage === "km_writeback_after_approval" ||
|
||||
stage === "km_archive_after_approval" ||
|
||||
stage === "km_supersede_after_approval" ||
|
||||
stage === "stale_ratio_recheck" ||
|
||||
stage === "owner_approved_duplicate_archive" ||
|
||||
stage === "km_duplicate_archive_after_owner_approval" ||
|
||||
@@ -1109,6 +1152,24 @@ function kmStaleReviewStatusKey(value: string | null | undefined) {
|
||||
}
|
||||
}
|
||||
|
||||
function kmStaleReviewCompleteStatusKey(value: string | null | undefined) {
|
||||
switch (value) {
|
||||
case "dry_run":
|
||||
case "completed":
|
||||
case "already_completed":
|
||||
return value;
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
function kmStaleReviewOutcomeForCandidate(
|
||||
candidate: KnowledgeStaleCandidate
|
||||
): "refresh_with_evidence" | "archive" | "supersede" {
|
||||
if (candidate.recommended_action === "archive_or_supersede") return "archive";
|
||||
return "refresh_with_evidence";
|
||||
}
|
||||
|
||||
function kmCorrelationSourceKey(value: string | null | undefined) {
|
||||
switch (value) {
|
||||
case "incident":
|
||||
@@ -2109,6 +2170,8 @@ function KnowledgeGovernancePanel({
|
||||
const t = useTranslations("awooop.workItems.knowledgeGovernance");
|
||||
const [archiveActions, setArchiveActions] = useState<Record<string, KnowledgeReviewDraftArchiveAction>>({});
|
||||
const [staleReviewActions, setStaleReviewActions] = useState<Record<string, KnowledgeStaleOwnerReviewAction>>({});
|
||||
const [staleReviewCompletionActions, setStaleReviewCompletionActions] =
|
||||
useState<Record<string, KnowledgeStaleOwnerReviewCompleteAction>>({});
|
||||
const items = queue?.items ?? [];
|
||||
const draftGroups = groupKnowledgeReviewDrafts(reviewDrafts, items);
|
||||
const dedupeGroups = dedupe?.groups ?? [];
|
||||
@@ -2243,6 +2306,119 @@ function KnowledgeGovernancePanel({
|
||||
}
|
||||
}, [onArchived, t]);
|
||||
|
||||
const previewStaleReviewCompletion = useCallback(async (candidate: KnowledgeStaleCandidate) => {
|
||||
const queuedDispatchId = (
|
||||
candidate.owner_review_dispatch_id
|
||||
?? staleReviewActions[candidate.entry_id]?.result?.dispatch_id
|
||||
?? null
|
||||
);
|
||||
if (!queuedDispatchId) {
|
||||
setStaleReviewCompletionActions((current) => ({
|
||||
...current,
|
||||
[candidate.entry_id]: {
|
||||
previewLoading: false,
|
||||
confirmLoading: false,
|
||||
previewResult: current[candidate.entry_id]?.previewResult ?? null,
|
||||
result: current[candidate.entry_id]?.result ?? null,
|
||||
error: t("staleCandidates.completeActions.missingDispatch"),
|
||||
},
|
||||
}));
|
||||
return;
|
||||
}
|
||||
setStaleReviewCompletionActions((current) => ({
|
||||
...current,
|
||||
[candidate.entry_id]: {
|
||||
previewLoading: true,
|
||||
confirmLoading: false,
|
||||
previewResult: current[candidate.entry_id]?.previewResult ?? null,
|
||||
result: null,
|
||||
error: null,
|
||||
},
|
||||
}));
|
||||
const result = await postJson<KnowledgeStaleOwnerReviewCompleteResponse>(
|
||||
`${API_BASE}/api/v1/ai/governance/km-stale-candidates/${encodeURIComponent(candidate.entry_id)}/complete-review`,
|
||||
{
|
||||
dispatch_id: queuedDispatchId,
|
||||
owner: "operator_console",
|
||||
owner_approved: false,
|
||||
dry_run: true,
|
||||
review_outcome: kmStaleReviewOutcomeForCandidate(candidate),
|
||||
owner_note: `operator_console_review:${candidate.priority_tier}:${candidate.recommended_action}`,
|
||||
},
|
||||
15000
|
||||
);
|
||||
setStaleReviewCompletionActions((current) => ({
|
||||
...current,
|
||||
[candidate.entry_id]: {
|
||||
previewLoading: false,
|
||||
confirmLoading: false,
|
||||
previewResult: result,
|
||||
result: null,
|
||||
error: result ? null : t("staleCandidates.completeActions.previewFailed"),
|
||||
},
|
||||
}));
|
||||
}, [staleReviewActions, t]);
|
||||
|
||||
const confirmStaleReviewCompletion = useCallback(async (candidate: KnowledgeStaleCandidate) => {
|
||||
const action = staleReviewCompletionActions[candidate.entry_id];
|
||||
const queuedDispatchId = (
|
||||
candidate.owner_review_dispatch_id
|
||||
?? staleReviewActions[candidate.entry_id]?.result?.dispatch_id
|
||||
?? action?.previewResult?.dispatch_id
|
||||
?? null
|
||||
);
|
||||
const fingerprint = action?.previewResult?.dry_run_plan_fingerprint;
|
||||
if (!queuedDispatchId || !fingerprint) {
|
||||
setStaleReviewCompletionActions((current) => ({
|
||||
...current,
|
||||
[candidate.entry_id]: {
|
||||
previewLoading: false,
|
||||
confirmLoading: false,
|
||||
previewResult: current[candidate.entry_id]?.previewResult ?? null,
|
||||
result: current[candidate.entry_id]?.result ?? null,
|
||||
error: t("staleCandidates.completeActions.missingPreviewFingerprint"),
|
||||
},
|
||||
}));
|
||||
return;
|
||||
}
|
||||
setStaleReviewCompletionActions((current) => ({
|
||||
...current,
|
||||
[candidate.entry_id]: {
|
||||
previewLoading: false,
|
||||
confirmLoading: true,
|
||||
previewResult: current[candidate.entry_id]?.previewResult ?? null,
|
||||
result: null,
|
||||
error: null,
|
||||
},
|
||||
}));
|
||||
const result = await postJson<KnowledgeStaleOwnerReviewCompleteResponse>(
|
||||
`${API_BASE}/api/v1/ai/governance/km-stale-candidates/${encodeURIComponent(candidate.entry_id)}/complete-review`,
|
||||
{
|
||||
dispatch_id: queuedDispatchId,
|
||||
owner: "operator_console",
|
||||
owner_approved: true,
|
||||
dry_run: false,
|
||||
review_outcome: kmStaleReviewOutcomeForCandidate(candidate),
|
||||
owner_note: `operator_console_review:${candidate.priority_tier}:${candidate.recommended_action}`,
|
||||
dry_run_plan_fingerprint: fingerprint,
|
||||
},
|
||||
15000
|
||||
);
|
||||
setStaleReviewCompletionActions((current) => ({
|
||||
...current,
|
||||
[candidate.entry_id]: {
|
||||
previewLoading: false,
|
||||
confirmLoading: false,
|
||||
previewResult: current[candidate.entry_id]?.previewResult ?? null,
|
||||
result,
|
||||
error: result ? null : t("staleCandidates.completeActions.confirmFailed"),
|
||||
},
|
||||
}));
|
||||
if (result?.status === "completed" || result?.status === "already_completed") {
|
||||
onArchived();
|
||||
}
|
||||
}, [onArchived, staleReviewActions, staleReviewCompletionActions, t]);
|
||||
|
||||
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">
|
||||
@@ -2395,6 +2571,15 @@ function KnowledgeGovernancePanel({
|
||||
const reviewAction = staleReviewActions[candidate.entry_id];
|
||||
const reviewResult = reviewAction?.result ?? null;
|
||||
const reviewStatusKey = kmStaleReviewStatusKey(reviewResult?.status);
|
||||
const queuedDispatchId = candidate.owner_review_dispatch_id ?? reviewResult?.dispatch_id ?? null;
|
||||
const persistedReviewStatusKey = governanceKmDispatchStatusKey(candidate.owner_review_status);
|
||||
const persistedReviewStageKey = governanceKmStageKey(candidate.owner_review_stage);
|
||||
const completionAction = staleReviewCompletionActions[candidate.entry_id];
|
||||
const completionPreview = completionAction?.previewResult ?? null;
|
||||
const completionResult = completionAction?.result ?? null;
|
||||
const completionPreviewReady = Boolean(completionPreview?.dry_run_plan_fingerprint);
|
||||
const completionPreviewStatusKey = kmStaleReviewCompleteStatusKey(completionPreview?.status);
|
||||
const completionResultStatusKey = kmStaleReviewCompleteStatusKey(completionResult?.status);
|
||||
return (
|
||||
<article
|
||||
key={candidate.entry_id}
|
||||
@@ -2446,6 +2631,15 @@ function KnowledgeGovernancePanel({
|
||||
approval: candidate.related_approval_id ?? "--",
|
||||
})}
|
||||
</p>
|
||||
{queuedDispatchId ? (
|
||||
<p>
|
||||
{t("staleCandidates.ownerReviewState", {
|
||||
status: t(`statuses.${persistedReviewStatusKey}` as never),
|
||||
stage: t(`stages.${persistedReviewStageKey}` as never),
|
||||
dispatch: queuedDispatchId,
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{candidate.reasons.slice(0, 5).map((reason) => (
|
||||
@@ -2469,6 +2663,37 @@ function KnowledgeGovernancePanel({
|
||||
? t("staleCandidates.queueingReview")
|
||||
: t("staleCandidates.queueReview")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => previewStaleReviewCompletion(candidate)}
|
||||
disabled={
|
||||
!queuedDispatchId ||
|
||||
completionAction?.previewLoading ||
|
||||
completionAction?.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" />
|
||||
{completionAction?.previewLoading
|
||||
? t("staleCandidates.completeActions.previewing")
|
||||
: t("staleCandidates.completeActions.preview")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => confirmStaleReviewCompletion(candidate)}
|
||||
disabled={
|
||||
!queuedDispatchId ||
|
||||
!completionPreviewReady ||
|
||||
completionAction?.previewLoading ||
|
||||
completionAction?.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"
|
||||
>
|
||||
<ShieldCheck className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
{completionAction?.confirmLoading
|
||||
? t("staleCandidates.completeActions.confirming")
|
||||
: t("staleCandidates.completeActions.confirm")}
|
||||
</button>
|
||||
<Link
|
||||
href={`/knowledge-base?q=${encodeURIComponent(candidate.title)}`}
|
||||
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-[#1f6feb] hover:bg-[#edf4ff] hover:text-[#0f4fa8]"
|
||||
@@ -2491,6 +2716,69 @@ function KnowledgeGovernancePanel({
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
{completionAction?.error ? (
|
||||
<p className="mt-2 border border-[#e2a29b] bg-[#fff0ef] px-2 py-1 text-[11px] leading-5 text-[#9f2f25]">
|
||||
{completionAction.error}
|
||||
</p>
|
||||
) : null}
|
||||
{completionPreview ? (
|
||||
<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.completeActions.statuses.${completionPreviewStatusKey}` as never
|
||||
)}
|
||||
</p>
|
||||
<p className="mt-1 text-[#5f5b52]">
|
||||
{t("staleCandidates.completeActions.previewResult", {
|
||||
outcome: t(
|
||||
`staleCandidates.completeActions.outcomes.${completionPreview.review_outcome}` as never
|
||||
),
|
||||
writesKm: String(completionPreview.writes_km),
|
||||
writesAudit: String(completionPreview.writes_governance_audit),
|
||||
})}
|
||||
</p>
|
||||
<p className="mt-1 break-all font-mono text-[11px] text-[#5f5b52]">
|
||||
{t("staleCandidates.completeActions.planFingerprint", {
|
||||
fingerprint: completionPreview.dry_run_plan_fingerprint ?? "--",
|
||||
})}
|
||||
</p>
|
||||
{completionPreview.stale_ratio_snapshot ? (
|
||||
<p className="mt-1 text-[#5f5b52]">
|
||||
{t("staleCandidates.completeActions.snapshot", {
|
||||
stale: completionPreview.stale_ratio_snapshot.stale_count,
|
||||
total: completionPreview.stale_ratio_snapshot.total_count,
|
||||
ratio: formatStaleRatio(completionPreview.stale_ratio_snapshot.stale_ratio),
|
||||
threshold: formatStaleRatio(completionPreview.stale_ratio_snapshot.threshold),
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{completionResult ? (
|
||||
<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.completeActions.statuses.${completionResultStatusKey}` as never
|
||||
)}
|
||||
</p>
|
||||
<p className="mt-1 text-[#5f5b52]">
|
||||
{t("staleCandidates.completeActions.result", {
|
||||
audit: completionResult.audit_dispatch_id ?? "--",
|
||||
recheck: completionResult.stale_ratio_recheck_dispatch_id ?? "--",
|
||||
})}
|
||||
</p>
|
||||
{completionResult.stale_ratio_snapshot ? (
|
||||
<p className="mt-1 text-[#5f5b52]">
|
||||
{t("staleCandidates.completeActions.snapshot", {
|
||||
stale: completionResult.stale_ratio_snapshot.stale_count,
|
||||
total: completionResult.stale_ratio_snapshot.total_count,
|
||||
ratio: formatStaleRatio(completionResult.stale_ratio_snapshot.stale_ratio),
|
||||
threshold: formatStaleRatio(completionResult.stale_ratio_snapshot.threshold),
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user