From c578bcdcfad7a6675b0d7d6c8a04f5f017c1ccd0 Mon Sep 17 00:00:00 2001 From: ogt Date: Fri, 10 Jul 2026 11:29:10 +0800 Subject: [PATCH] feat(work-items): surface AI runbook loop coverage --- .../awoooi_priority_work_order_readback.py | 311 ++++++++++++++++++ ...awoooi_priority_work_order_readback_api.py | 66 ++++ apps/web/messages/en.json | 26 ++ apps/web/messages/zh-TW.json | 26 ++ .../app/[locale]/awooop/work-items/page.tsx | 246 ++++++++++++++ 5 files changed, 675 insertions(+) diff --git a/apps/api/src/services/awoooi_priority_work_order_readback.py b/apps/api/src/services/awoooi_priority_work_order_readback.py index 3eb95607c..51aac1e49 100644 --- a/apps/api/src/services/awoooi_priority_work_order_readback.py +++ b/apps/api/src/services/awoooi_priority_work_order_readback.py @@ -84,6 +84,16 @@ _AI_AUTOMATION_NODE_RECEIPT_REQUIRED_FIELDS = [ "km_writeback_ref", "playbook_trust_writeback_ref", ] +_AI_CONTROLLED_REPAIR_LOOP_REQUIRED_FIELDS = [ + "target_selector", + "source_truth_diff", + "candidate_action", + "check_mode_or_dry_run", + "controlled_apply_boundary", + "rollback_or_no_write", + "post_verifier", + "km_playbook_trust_writeback", +] _COMMANDER_INSERTED_REQUIREMENT_WORK_ITEMS: list[dict[str, Any]] = [ { "id": "CIR-P0-001", @@ -6688,6 +6698,307 @@ def _apply_ai_automation_node_receipts(payload: dict[str, Any]) -> None: summary["ai_automation_node_receipt_metadata_only"] = True summary["ai_automation_node_receipt_lanes"] = lanes summary["ai_automation_node_receipt_work_items"] = work_items + _apply_ai_controlled_repair_loop_runbook_matrix(payload) + + +def _contains_any(value: str, needles: tuple[str, ...]) -> bool: + normalized = value.lower() + return any(needle.lower() in normalized for needle in needles) + + +def _apply_ai_controlled_repair_loop_runbook_matrix( + payload: dict[str, Any], +) -> None: + items = [ + _dict(item) + for item in payload.get("commander_inserted_requirement_work_items") or [] + if str(_dict(item).get("priority") or "") == "P0" + ] + receipts = [_dict(item) for item in payload.get("ai_automation_node_receipts") or []] + ready_receipt_work_items = { + str(item.get("work_item_id") or "") + for item in receipts + if str(item.get("node_status") or "") == "ready" + } + learning_receipt_ready = any( + str(item.get("node_id") or "") == "learning_writeback" + and str(item.get("node_status") or "") == "ready" + for item in receipts + ) + post_verifier_receipt_ready = any( + str(item.get("node_id") or "") == "post_verifier" + and str(item.get("node_status") or "") == "ready" + for item in receipts + ) + + rows: list[dict[str, Any]] = [] + for item in sorted(items, key=lambda raw: _int(raw.get("order"))): + item_id = str(item.get("id") or "") + lane = str(item.get("lane") or "") + status = str(item.get("status") or "") + text = " ".join( + str(item.get(key) or "") + for key in ( + "id", + "lane", + "request", + "normalized_work_item", + "current_state", + "acceptance", + "next_action", + "mapped_workplan_id", + "visible_surface", + ) + ) + break_glass_required = lane == "secret_safety" or _contains_any( + text, + ( + "secret plaintext", + "plaintext secret", + "private key", + "cookie", + "authorization header", + "明文", + "密碼", + "敏感值", + ), + ) + fields = { + "target_selector": bool( + item.get("mapped_workplan_id") + or item.get("visible_surface") + or item_id + ), + "source_truth_diff": _contains_any( + text, + ( + "gitea", + "source", + "repo", + "inventory", + "matrix", + "snapshot", + "evidence", + "readback", + "production", + "證據", + "讀回", + "正式環境", + ), + ), + "candidate_action": bool(item.get("next_action")) + or item_id in ready_receipt_work_items, + "check_mode_or_dry_run": _contains_any( + text, + ( + "check-mode", + "check mode", + "dry-run", + "dry run", + "verifier", + "hidden prompt", + "metadata verifier", + "non-secret", + "只跑", + "乾跑", + "驗證", + ), + ), + "controlled_apply_boundary": bool( + break_glass_required + or _contains_any( + text, + ( + "controlled", + "ai", + "fail-closed", + "no-write", + "no_secret", + "non-secret", + "不讀 secret", + "受控", + "不恢復 generic", + ), + ) + ), + "rollback_or_no_write": bool( + break_glass_required + or _contains_any( + text, + ( + "rollback", + "no-write", + "no write", + "no runtime write", + "不入檔", + "不重印", + "不保存", + "不建立", + "不恢復", + "fail-closed", + ), + ) + ), + "post_verifier": bool( + post_verifier_receipt_ready + or _contains_any( + text, + ( + "verifier", + "readback", + "smoke", + "api", + "production", + "deploy marker", + "metadata verifier", + "驗證", + "讀回", + "正式環境", + ), + ) + ), + "km_playbook_trust_writeback": bool( + learning_receipt_ready + or _contains_any( + text, + ( + "km", + "rag", + "mcp", + "playbook", + "trust", + "writeback", + "learning", + "學習", + "回寫", + ), + ) + ), + } + ready_field_count = sum(1 for ready in fields.values() if ready) + missing_fields = [ + field + for field in _AI_CONTROLLED_REPAIR_LOOP_REQUIRED_FIELDS + if not fields[field] + ] + controlled_apply_allowed = not break_glass_required and status not in { + "deferred", + } + if break_glass_required: + coverage_state = "break_glass" + elif not missing_fields: + coverage_state = "ready" + elif ready_field_count > 0: + coverage_state = "partial" + else: + coverage_state = "queued" + + rows.append( + { + "work_item_id": item_id, + "priority": "P0", + "order": _int(item.get("order")), + "status": status, + "lane": lane, + "mapped_workplan_id": str(item.get("mapped_workplan_id") or ""), + "coverage_state": coverage_state, + "ready_field_count": ready_field_count, + "required_field_count": len(_AI_CONTROLLED_REPAIR_LOOP_REQUIRED_FIELDS), + "missing_fields": missing_fields, + "target_selector_ready": fields["target_selector"], + "source_truth_diff_ready": fields["source_truth_diff"], + "candidate_action_ready": fields["candidate_action"], + "check_mode_or_dry_run_ready": fields["check_mode_or_dry_run"], + "controlled_apply_boundary_ready": fields[ + "controlled_apply_boundary" + ], + "controlled_apply_allowed": controlled_apply_allowed, + "break_glass_required": break_glass_required, + "rollback_or_no_write_ready": fields["rollback_or_no_write"], + "post_verifier_ready": fields["post_verifier"], + "km_playbook_trust_writeback_ready": fields[ + "km_playbook_trust_writeback" + ], + "manual_terminal_allowed": False, + "next_action": str(item.get("next_action") or ""), + } + ) + + required_total = len(rows) * len(_AI_CONTROLLED_REPAIR_LOOP_REQUIRED_FIELDS) + ready_total = sum(_int(row.get("ready_field_count")) for row in rows) + state_counts = { + state: sum(1 for row in rows if row["coverage_state"] == state) + for state in ("ready", "partial", "queued", "break_glass") + } + next_row = next( + ( + row + for row in rows + if row["coverage_state"] not in {"ready", "break_glass"} + and row["status"] not in {"done", "deferred"} + ), + next( + ( + row + for row in rows + if row["coverage_state"] not in {"ready", "break_glass"} + ), + rows[0] if rows else {}, + ), + ) + + matrix = { + "schema_version": "ai_controlled_repair_loop_runbook_matrix_v1", + "status": "ai_controlled_repair_loop_runbook_matrix_ready", + "metadata_only": True, + "source": _COMMANDER_INSERTED_REQUIREMENT_SOURCE, + "required_fields": _AI_CONTROLLED_REPAIR_LOOP_REQUIRED_FIELDS, + "manual_as_default_terminal_allowed": False, + "hard_blockers_preserved": [ + "secret_plaintext", + "destructive_data_change", + "reboot_or_node_drain", + "force_push_or_repo_ref_delete", + "paid_provider_route_change", + ], + "rows": rows, + "summary": { + "p0_runbook_count": len(rows), + "ready_runbook_count": state_counts["ready"], + "partial_runbook_count": state_counts["partial"], + "queued_runbook_count": state_counts["queued"], + "break_glass_runbook_count": state_counts["break_glass"], + "controlled_apply_allowed_count": sum( + 1 for row in rows if row["controlled_apply_allowed"] is True + ), + "manual_terminal_count": sum( + 1 for row in rows if row["manual_terminal_allowed"] is True + ), + "required_field_total": required_total, + "ready_field_total": ready_total, + "completion_percent": round( + (ready_total / required_total) * 100 + ) + if required_total + else 0, + "next_work_item_id": str(next_row.get("work_item_id") or ""), + "next_missing_fields": next_row.get("missing_fields") or [], + }, + } + + payload["ai_controlled_repair_loop_runbook_matrix"] = matrix + + summary = _dict(payload.setdefault("summary", {})) + rollups = _dict(payload.setdefault("rollups", {})) + for key, value in matrix["summary"].items(): + summary[f"ai_controlled_repair_loop_{key}"] = value + rollups[f"ai_controlled_repair_loop_{key}"] = value + summary["ai_controlled_repair_loop_schema_version"] = matrix["schema_version"] + summary["ai_controlled_repair_loop_metadata_only"] = True + summary["ai_controlled_repair_loop_manual_as_default_terminal_allowed"] = False + rollups["ai_controlled_repair_loop_schema_version"] = matrix["schema_version"] + rollups["ai_controlled_repair_loop_metadata_only"] = True + rollups["ai_controlled_repair_loop_manual_as_default_terminal_allowed"] = False def _mark_runtime_generated_at(payload: dict[str, Any]) -> None: diff --git a/apps/api/tests/test_awoooi_priority_work_order_readback_api.py b/apps/api/tests/test_awoooi_priority_work_order_readback_api.py index 3eae7357c..285a490b0 100644 --- a/apps/api/tests/test_awoooi_priority_work_order_readback_api.py +++ b/apps/api/tests/test_awoooi_priority_work_order_readback_api.py @@ -589,6 +589,53 @@ def test_awoooi_priority_work_order_readback_surfaces_gitea_bundle_truth(): assert "CIR-P0-AILOOP-002" in payload["summary"][ "ai_automation_node_receipt_work_items" ] + runbook_matrix = payload["ai_controlled_repair_loop_runbook_matrix"] + assert runbook_matrix["schema_version"] == ( + "ai_controlled_repair_loop_runbook_matrix_v1" + ) + assert runbook_matrix["metadata_only"] is True + assert runbook_matrix["manual_as_default_terminal_allowed"] is False + assert runbook_matrix["required_fields"] == [ + "target_selector", + "source_truth_diff", + "candidate_action", + "check_mode_or_dry_run", + "controlled_apply_boundary", + "rollback_or_no_write", + "post_verifier", + "km_playbook_trust_writeback", + ] + runbook_rows = runbook_matrix["rows"] + runbook_by_id = {item["work_item_id"]: item for item in runbook_rows} + runbook_summary = runbook_matrix["summary"] + assert runbook_summary["p0_runbook_count"] == sum( + 1 for item in inserted_items if item["priority"] == "P0" + ) + assert runbook_summary["manual_terminal_count"] == 0 + assert runbook_summary["controlled_apply_allowed_count"] == sum( + 1 for item in runbook_rows if item["controlled_apply_allowed"] is True + ) + assert runbook_summary["break_glass_runbook_count"] == sum( + 1 for item in runbook_rows if item["coverage_state"] == "break_glass" + ) + assert runbook_summary["next_work_item_id"] == "CIR-P0-007" + assert runbook_by_id["CIR-P0-006"]["break_glass_required"] is True + assert runbook_by_id["CIR-P0-006"]["controlled_apply_allowed"] is False + assert runbook_by_id["CIR-P0-007"]["controlled_apply_allowed"] is True + assert runbook_by_id["CIR-P0-007"]["coverage_state"] == "partial" + assert runbook_by_id["CIR-P0-007"]["manual_terminal_allowed"] is False + assert "km_playbook_trust_writeback" in runbook_by_id["CIR-P0-007"][ + "missing_fields" + ] + assert payload["summary"]["ai_controlled_repair_loop_p0_runbook_count"] == ( + runbook_summary["p0_runbook_count"] + ) + assert payload["summary"]["ai_controlled_repair_loop_next_work_item_id"] == ( + "CIR-P0-007" + ) + assert payload["summary"][ + "ai_controlled_repair_loop_manual_as_default_terminal_allowed" + ] is False def test_awoooi_priority_work_order_readback_routes_closed_backup_to_host_boot_and_windows99( @@ -1452,6 +1499,25 @@ def test_awoooi_priority_work_order_readback_endpoint_returns_snapshot( assert endpoint_receipts["learning_writeback"]["km_writeback_ref"] == ( "context_receipt:km=1" ) + endpoint_matrix = data["ai_controlled_repair_loop_runbook_matrix"] + endpoint_rows = endpoint_matrix["rows"] + endpoint_row_by_id = {item["work_item_id"]: item for item in endpoint_rows} + assert endpoint_matrix["status"] == ( + "ai_controlled_repair_loop_runbook_matrix_ready" + ) + assert endpoint_matrix["summary"]["p0_runbook_count"] == sum( + 1 for item in inserted_items if item["priority"] == "P0" + ) + assert endpoint_matrix["summary"]["manual_terminal_count"] == 0 + assert endpoint_matrix["summary"]["next_work_item_id"] == "CIR-P0-007" + assert endpoint_row_by_id["CIR-P0-006"]["coverage_state"] == "break_glass" + assert endpoint_row_by_id["CIR-P0-006"]["controlled_apply_allowed"] is False + assert endpoint_row_by_id["CIR-P0-007"]["coverage_state"] == "partial" + assert endpoint_row_by_id["CIR-P0-007"]["controlled_apply_allowed"] is True + assert data["summary"]["ai_controlled_repair_loop_next_work_item_id"] == ( + "CIR-P0-007" + ) + assert data["summary"]["ai_controlled_repair_loop_manual_terminal_count"] == 0 def test_controlled_cd_lane_live_metric_readback_falls_back_to_prometheus( diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index a5815d15e..389ed5cb9 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -9573,6 +9573,32 @@ "backup": "backup evidence" } }, + "aiControlledRunbookCoverage": { + "eyebrow": "AI Loop Coverage", + "title": "P0 Runbook AI Controlled Coverage", + "subtitle": "Turns each P0 runbook's target selector, source truth, candidate, check-mode, controlled apply, rollback, post verifier, and KM / PlayBook trust writeback into one matrix.", + "next": "Next gap", + "manualTerminal": "Manual terminal", + "allowed": "allowed", + "disabled": "disabled", + "source": "source", + "fieldProgress": "ready {ready}/{total}", + "metrics": { + "completion": "Field coverage", + "controlled": "controlled apply", + "controlledDetail": "Executable after break-glass exclusions", + "breakGlass": "break-glass", + "breakGlassDetail": "Incident-grade blockers such as secret, destructive, or reboot work", + "manual": "manual terminal", + "manualDetail": "Manual is not the default terminal state for low/medium/high work" + }, + "states": { + "ready": "ready", + "partial": "partial", + "queued": "queued", + "break_glass": "break-glass" + } + }, "commanderInsertedRequirements": { "eyebrow": "Mainline priority", "title": "Commander Inserted Requirement Work Items", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 7316e2266..f26602def 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -9573,6 +9573,32 @@ "backup": "backup evidence" } }, + "aiControlledRunbookCoverage": { + "eyebrow": "AI Loop Coverage", + "title": "P0 Runbook AI Controlled Coverage", + "subtitle": "把每個 P0 runbook 的 target selector、source truth、candidate、check-mode、controlled apply、rollback、post verifier 與 KM / PlayBook trust 回寫狀態收成矩陣。", + "next": "下一個缺口", + "manualTerminal": "人工終局", + "allowed": "allowed", + "disabled": "disabled", + "source": "source", + "fieldProgress": "ready {ready}/{total}", + "metrics": { + "completion": "欄位覆蓋", + "controlled": "controlled apply", + "controlledDetail": "排除 break-glass 後可受控推進", + "breakGlass": "break-glass", + "breakGlassDetail": "secret / destructive / reboot 等事故級硬阻擋", + "manual": "manual terminal", + "manualDetail": "人工不得是 low/medium/high 預設終局" + }, + "states": { + "ready": "ready", + "partial": "partial", + "queued": "queued", + "break_glass": "break-glass" + } + }, "commanderInsertedRequirements": { "eyebrow": "主線優先序", "title": "統帥插入需求工作項", 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 9ab9902e2..59fc75596 100644 --- a/apps/web/src/app/[locale]/awooop/work-items/page.tsx +++ b/apps/web/src/app/[locale]/awooop/work-items/page.tsx @@ -1067,6 +1067,56 @@ type CommanderInsertedRequirementWorkItem = { source?: string | null; }; +type AiControlledRepairLoopRunbookRow = { + work_item_id?: string | null; + priority?: string | null; + order?: number | null; + status?: string | null; + lane?: string | null; + mapped_workplan_id?: string | null; + coverage_state?: "ready" | "partial" | "queued" | "break_glass" | string | null; + ready_field_count?: number | null; + required_field_count?: number | null; + missing_fields?: string[] | null; + target_selector_ready?: boolean | null; + source_truth_diff_ready?: boolean | null; + candidate_action_ready?: boolean | null; + check_mode_or_dry_run_ready?: boolean | null; + controlled_apply_boundary_ready?: boolean | null; + controlled_apply_allowed?: boolean | null; + break_glass_required?: boolean | null; + rollback_or_no_write_ready?: boolean | null; + post_verifier_ready?: boolean | null; + km_playbook_trust_writeback_ready?: boolean | null; + manual_terminal_allowed?: boolean | null; + next_action?: string | null; +}; + +type AiControlledRepairLoopRunbookMatrix = { + schema_version?: string | null; + status?: string | null; + metadata_only?: boolean | null; + source?: string | null; + required_fields?: string[] | null; + manual_as_default_terminal_allowed?: boolean | null; + hard_blockers_preserved?: string[] | null; + rows?: AiControlledRepairLoopRunbookRow[] | null; + summary?: { + p0_runbook_count?: number | null; + ready_runbook_count?: number | null; + partial_runbook_count?: number | null; + queued_runbook_count?: number | null; + break_glass_runbook_count?: number | null; + controlled_apply_allowed_count?: number | null; + manual_terminal_count?: number | null; + required_field_total?: number | null; + ready_field_total?: number | null; + completion_percent?: number | null; + next_work_item_id?: string | null; + next_missing_fields?: string[] | null; + } | null; +}; + type PriorityWorkOrderResponse = { current_head?: { gitea_main_sha?: string | null; @@ -1434,6 +1484,7 @@ type PriorityWorkOrderResponse = { metadata_only?: boolean | null; hard_blockers_preserved?: string[] | null; } | null; + ai_controlled_repair_loop_runbook_matrix?: AiControlledRepairLoopRunbookMatrix | null; }; type SecurityCockpitRuntimeReadback = { @@ -8523,6 +8574,196 @@ function aiAutomationNodeStatusTone(status: string) { } } +function aiControlledRunbookCoverageTone(status: string) { + switch (status) { + case "ready": + return "border-[#b9d9c2] bg-[#f2fbf3] text-[#236332]"; + case "break_glass": + return "border-[#d4c1ef] bg-[#f8f4ff] text-[#5b3a8a]"; + case "queued": + return "border-[#ead5bd] bg-[#fff8ef] text-[#7a3d00]"; + default: + return "border-[#c9d8ea] bg-[#eef5ff] text-[#1f5b9b]"; + } +} + +function AiControlledRunbookCoveragePanel({ + priority, + loading, +}: { + priority: PriorityWorkOrderResponse | null; + loading: boolean; +}) { + const t = useTranslations("awooop.workItems.aiControlledRunbookCoverage"); + const matrix = priority?.ai_controlled_repair_loop_runbook_matrix; + const summary = matrix?.summary; + const rows = matrix?.rows ?? []; + const nextId = summary?.next_work_item_id ?? "--"; + const spotlightIds = new Set([ + nextId, + "CIR-P0-006", + "CIR-P0-007", + "CIR-P0-LOG-001", + "CIR-P0-TG-001", + ]); + const spotlightRows = rows + .filter((row) => row.work_item_id && spotlightIds.has(row.work_item_id)) + .slice(0, 6); + const metricCards = [ + { + key: "completion", + label: t("metrics.completion"), + value: loading ? "--" : `${summary?.completion_percent ?? 0}%`, + detail: loading + ? "--" + : `${summary?.ready_field_total ?? 0}/${summary?.required_field_total ?? 0}`, + icon: Gauge, + tone: "border-[#c9d8ea] bg-[#eef5ff] text-[#1f5b9b]", + }, + { + key: "controlled", + label: t("metrics.controlled"), + value: loading + ? "--" + : `${summary?.controlled_apply_allowed_count ?? 0}/${summary?.p0_runbook_count ?? 0}`, + detail: t("metrics.controlledDetail"), + icon: ShieldCheck, + tone: "border-[#b9d9c2] bg-[#f2fbf3] text-[#236332]", + }, + { + key: "breakGlass", + label: t("metrics.breakGlass"), + value: loading ? "--" : summary?.break_glass_runbook_count ?? 0, + detail: t("metrics.breakGlassDetail"), + icon: Lock, + tone: "border-[#d4c1ef] bg-[#f8f4ff] text-[#5b3a8a]", + }, + { + key: "manual", + label: t("metrics.manual"), + value: loading ? "--" : summary?.manual_terminal_count ?? 0, + detail: t("metrics.manualDetail"), + icon: CheckCircle2, + tone: "border-[#b9d9c2] bg-[#f2fbf3] text-[#236332]", + }, + ]; + + return ( +
+
+
+
+
+

+ {t("title")} +

+
+
+
+ {t("next")} +
+
+ {loading ? "--" : nextId} +
+
+
+
+ {t("manualTerminal")} +
+
+ {loading + ? "--" + : matrix?.manual_as_default_terminal_allowed + ? t("allowed") + : t("disabled")} +
+
+
+

+ {t("subtitle")} +

+
+ +
+
+ {metricCards.map((metric) => { + const Icon = metric.icon; + return ( +
+
+ {metric.label} +
+
+ {metric.value} +
+
+ {metric.detail} +
+
+ ); + })} +
+ +
+ {spotlightRows.map((row) => { + const state = String(row.coverage_state ?? "partial"); + return ( +
+
+
+
+ {row.work_item_id} +
+
+ {row.lane} +
+
+ + {t(`states.${state}` as never)} + +
+
+ {t("fieldProgress", { + ready: row.ready_field_count ?? 0, + total: row.required_field_count ?? 0, + })} +
+
+ {(row.missing_fields ?? []).slice(0, 4).map((field) => ( + + {field} + + ))} +
+
+ ); + })} +
+ +
+ {t("source")}{" "} + + {loading ? "--" : matrix?.source ?? "--"} + +
+
+
+
+ ); +} + function AiLoopLogSourceTagsPanel({ priority, loading, @@ -12325,6 +12566,11 @@ export default function AwoooPWorkItemsPage() { > + +