diff --git a/apps/web/src/app/[locale]/awooop/work-items/page.tsx b/apps/web/src/app/[locale]/awooop/work-items/page.tsx index 678efcb17..53c8c4e9b 100644 --- a/apps/web/src/app/[locale]/awooop/work-items/page.tsx +++ b/apps/web/src/app/[locale]/awooop/work-items/page.tsx @@ -33,6 +33,7 @@ import { import { Link } from "@/i18n/routing"; import { AutonomousRuntimeReceiptPanel } from "@/components/awooop/autonomous-runtime-receipt-panel"; import { IncidentEvidenceHeader } from "@/components/awooop/incident-evidence-header"; +import { isMonotonicPriorityWorkOrderProjection } from "@/lib/priority-work-order-projection"; import { getRuntimeApiBaseUrl } from "@/lib/runtime-api-base"; import { AwoooPStatusChainPanel, @@ -12809,7 +12810,7 @@ export default function AwoooPWorkItemsPage() { for (const url of urls) { const nextPriorityWorkOrder = await fetchJson(url, 12000); - if (nextPriorityWorkOrder?.commander_inserted_requirement_work_items?.length) { + if (isMonotonicPriorityWorkOrderProjection(nextPriorityWorkOrder)) { setPriorityWorkOrderReadback(nextPriorityWorkOrder); setPriorityWorkOrderLoading(false); return nextPriorityWorkOrder; @@ -12817,7 +12818,6 @@ export default function AwoooPWorkItemsPage() { } await new Promise((resolve) => window.setTimeout(resolve, 1200)); } - setPriorityWorkOrderLoading(false); return null; }, []); @@ -12964,7 +12964,23 @@ export default function AwoooPWorkItemsPage() { }, [effectiveFocusedIncidentId, projectId]); useEffect(() => { - fetchPriorityWorkOrderReadback(); + let cancelled = false; + let retryTimer: number | null = null; + + const loadPriorityWorkOrder = async () => { + const readback = await fetchPriorityWorkOrderReadback(); + if (!cancelled && !readback) { + retryTimer = window.setTimeout(loadPriorityWorkOrder, 4000); + } + }; + + void loadPriorityWorkOrder(); + return () => { + cancelled = true; + if (retryTimer !== null) { + window.clearTimeout(retryTimer); + } + }; }, [fetchPriorityWorkOrderReadback]); useEffect(() => { diff --git a/apps/web/src/lib/__tests__/priority-work-order-projection.test.ts b/apps/web/src/lib/__tests__/priority-work-order-projection.test.ts new file mode 100644 index 000000000..7587ae21f --- /dev/null +++ b/apps/web/src/lib/__tests__/priority-work-order-projection.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from "vitest"; + +import { isMonotonicPriorityWorkOrderProjection } from "../priority-work-order-projection"; + +const closedWorkItems = [ + "AIA-P0-001", + "AIA-P0-011", + "AIA-P0-002", + "AIA-P0-003", + "AIA-P0-004", + "AIA-P0-005", +].map((id) => ({ id, status: "done" })); + +describe("isMonotonicPriorityWorkOrderProjection", () => { + it("accepts the production-closed 30 percent projection", () => { + expect( + isMonotonicPriorityWorkOrderProjection({ + commander_inserted_requirement_work_items: [{}], + ai_automation_program_ledger: { + work_items: [...closedWorkItems, { id: "AIA-P0-006", status: "in_progress" }], + summary: { done_count: 6, completion_percent: 30 }, + }, + }) + ).toBe(true); + }); + + it("rejects a transient zero projection that reopens closed work", () => { + expect( + isMonotonicPriorityWorkOrderProjection({ + commander_inserted_requirement_work_items: [{}], + ai_automation_program_ledger: { + work_items: closedWorkItems.map((item) => ({ + ...item, + status: "in_progress", + })), + summary: { done_count: 0, completion_percent: 0 }, + }, + }) + ).toBe(false); + }); + + it("accepts future progress above the current closure floor", () => { + expect( + isMonotonicPriorityWorkOrderProjection({ + commander_inserted_requirement_work_items: [{}], + ai_automation_program_ledger: { + work_items: [...closedWorkItems, { id: "AIA-P0-006", status: "done" }], + summary: { done_count: 7, completion_percent: 35 }, + }, + }) + ).toBe(true); + }); +}); diff --git a/apps/web/src/lib/priority-work-order-projection.ts b/apps/web/src/lib/priority-work-order-projection.ts new file mode 100644 index 000000000..6d42b2b95 --- /dev/null +++ b/apps/web/src/lib/priority-work-order-projection.ts @@ -0,0 +1,47 @@ +const MONOTONIC_CLOSED_PROGRAM_WORK_ITEM_IDS = [ + "AIA-P0-001", + "AIA-P0-011", + "AIA-P0-002", + "AIA-P0-003", + "AIA-P0-004", + "AIA-P0-005", +] as const; + +type PriorityWorkOrderProjection = { + commander_inserted_requirement_work_items?: unknown[] | null; + ai_automation_program_ledger?: { + work_items?: Array<{ + id?: string | null; + status?: string | null; + }> | null; + summary?: { + done_count?: number | null; + completion_percent?: number | null; + } | null; + } | null; +}; + +export function isMonotonicPriorityWorkOrderProjection( + projection: PriorityWorkOrderProjection | null | undefined +): boolean { + if (!projection?.commander_inserted_requirement_work_items?.length) { + return false; + } + + const program = projection.ai_automation_program_ledger; + const workItems = program?.work_items ?? []; + const statusById = new Map( + workItems.map((item) => [item.id ?? "", item.status ?? ""]) + ); + const closureFloorSatisfied = MONOTONIC_CLOSED_PROGRAM_WORK_ITEM_IDS.every( + (workItemId) => statusById.get(workItemId) === "done" + ); + const doneCount = Number(program?.summary?.done_count ?? 0); + const completionPercent = Number(program?.summary?.completion_percent ?? 0); + + return ( + closureFloorSatisfied && + doneCount >= MONOTONIC_CLOSED_PROGRAM_WORK_ITEM_IDS.length && + completionPercent >= 30 + ); +}