diff --git a/apps/api/src/services/ai_agent_autonomous_runtime_control.py b/apps/api/src/services/ai_agent_autonomous_runtime_control.py index 564c490d..e2d3be8d 100644 --- a/apps/api/src/services/ai_agent_autonomous_runtime_control.py +++ b/apps/api/src/services/ai_agent_autonomous_runtime_control.py @@ -568,7 +568,8 @@ def _build_log_integration_taxonomy( }, ] label_dimensions = sorted( - { + {"source_family"} + | { str(dimension) for source in source_families for dimension in source["label_dimensions"] @@ -595,10 +596,13 @@ def _build_log_integration_taxonomy( "label_dimensions": label_dimensions, "required_label_dimensions": [ "project", + "product", + "website", "source_family", "incident", "operation", "service", + "package", "tool", "playbook", ], @@ -1330,6 +1334,189 @@ def _build_ui_productization_readback() -> dict[str, Any]: } +def _build_multi_product_taxonomy_contract( + log_integration_taxonomy: Mapping[str, Any], +) -> dict[str, Any]: + """Publish the shared taxonomy contract for AWOOOI-managed products.""" + + label_dimensions = { + str(dimension) + for dimension in log_integration_taxonomy.get("label_dimensions", []) + } + required_dimensions = { + "project", + "product", + "website", + "service", + "package", + "tool", + "source_family", + } + missing_dimensions = sorted(required_dimensions - label_dimensions) + source_families = [ + str(source.get("source_family_id")) + for source in log_integration_taxonomy.get("source_families", []) + if isinstance(source, Mapping) and source.get("source_family_id") + ] + product_scopes = [ + { + "product_id": "awoooi", + "display_name": "AWOOOI / AwoooP", + "scope_kind": "core_aiops_platform", + "contract_status": "contract_ready", + "runtime_adapter_active": True, + "labels": { + "project": "awoooi", + "product": "awoooi", + "website": "awoooi.wooo.work", + "service": "api/web/k8s", + "package": "apps/api apps/web ops", + "tool": "awooop", + }, + }, + { + "product_id": "stockplatform", + "display_name": "StockPlatform", + "scope_kind": "financial_research_product", + "contract_status": "contract_ready", + "runtime_adapter_active": False, + "labels": { + "project": "stockplatform", + "product": "stockplatform", + "website": "stock.wooo.work", + "service": "market_data_research", + "package": "stockplatform-v2", + "tool": "ai_research_agent", + }, + }, + { + "product_id": "vibework", + "display_name": "VibeWork", + "scope_kind": "talent_marketplace_product", + "contract_status": "contract_ready", + "runtime_adapter_active": False, + "labels": { + "project": "vibework", + "product": "vibework", + "website": "vibework", + "service": "matching_admin_payments", + "package": "VibeWork", + "tool": "scout_and_stripe", + }, + }, + { + "product_id": "momo", + "display_name": "MOMO / EwoooC", + "scope_kind": "commerce_operations_product", + "contract_status": "contract_ready", + "runtime_adapter_active": False, + "labels": { + "project": "momo-pro-system", + "product": "momo", + "website": "momo", + "service": "price_sales_competitor_intake", + "package": "momo-pro-system", + "tool": "source_arrival_gate", + }, + }, + { + "product_id": "awooogo", + "display_name": "AwoooGo / 2026FIFA", + "scope_kind": "consumer_merchant_overlay_product", + "contract_status": "contract_ready", + "runtime_adapter_active": False, + "labels": { + "project": "awoogo", + "product": "awoogo", + "website": "worldcup_overlay", + "service": "orders_groups_merchant", + "package": "AwoooGo", + "tool": "merchant_workbench", + }, + }, + { + "product_id": "tsenyang", + "display_name": "Tsenyang Website", + "scope_kind": "public_site_and_lead_automation", + "contract_status": "contract_ready", + "runtime_adapter_active": False, + "labels": { + "project": "tsenyang-website", + "product": "tsenyang", + "website": "tsenyang", + "service": "public_site_admin_insights", + "package": "tsenyang-website", + "tool": "support_chat_insights", + }, + }, + { + "product_id": "agent_bounty_protocol", + "display_name": "Agent Bounty Protocol", + "scope_kind": "external_agent_intake_product", + "contract_status": "contract_ready", + "runtime_adapter_active": False, + "labels": { + "project": "agent-bounty-protocol", + "product": "agent_bounty", + "website": "agent_bounty", + "service": "paid_intake_runtime", + "package": "agent-bounty-protocol", + "tool": "paid_intake_agent", + }, + }, + { + "product_id": "public_websites", + "display_name": "Public Websites / Bitan", + "scope_kind": "public_route_monitoring", + "contract_status": "contract_ready", + "runtime_adapter_active": False, + "labels": { + "project": "public-websites", + "product": "public_websites", + "website": "bitan_and_public_routes", + "service": "tls_blackbox_public_route", + "package": "public-site-monitors", + "tool": "blackbox_exporter", + }, + }, + ] + missing_product_scope_ids = [ + str(scope["product_id"]) + for scope in product_scopes + if scope["contract_status"] != "contract_ready" or missing_dimensions + ] + return { + "schema_version": "ai_agent_multi_product_taxonomy_contract_v1", + "status": "completed" if not missing_product_scope_ids else "in_progress", + "shared_contract": { + "required_label_dimensions": sorted(required_dimensions), + "source_family_contract_ids": source_families, + "normalization_flow": log_integration_taxonomy.get("normalized_event_flow") or [], + }, + "product_scopes": product_scopes, + "missing_required_dimension_ids": missing_dimensions, + "missing_product_scope_ids": missing_product_scope_ids, + "public_safety": { + "raw_secret_collection_allowed": False, + "raw_session_collection_allowed": False, + "external_product_runtime_write_enabled_on_read": False, + "github_surface_required": False, + }, + "rollups": { + "product_scope_count": len(product_scopes), + "contract_ready_product_scope_count": sum( + 1 for scope in product_scopes if scope["contract_status"] == "contract_ready" + ), + "runtime_adapter_active_count": sum( + 1 for scope in product_scopes if scope["runtime_adapter_active"] is True + ), + "required_dimension_count": len(required_dimensions), + "missing_required_dimension_count": len(missing_dimensions), + "source_family_contract_count": len(source_families), + }, + } + + def _build_work_item_progress( *, trace_ledger: Mapping[str, Any], @@ -1338,6 +1525,7 @@ def _build_work_item_progress( learning_loop: Mapping[str, Any], alert_noise_reduction: Mapping[str, Any], ui_productization: Mapping[str, Any], + multi_product_taxonomy: Mapping[str, Any], db_read_status: str, ) -> dict[str, Any]: """Build ordered work items that the UI and agent can keep advancing.""" @@ -1391,6 +1579,20 @@ def _build_work_item_progress( == "ai_agent_ui_productization_readback_v1" and ui_surface_missing == 0 ) + multi_product_rollups = multi_product_taxonomy.get("rollups") + if not isinstance(multi_product_rollups, Mapping): + multi_product_rollups = {} + multi_product_missing = len( + multi_product_taxonomy.get("missing_product_scope_ids") + if isinstance(multi_product_taxonomy.get("missing_product_scope_ids"), list) + else [] + ) + _int_value(multi_product_rollups.get("missing_required_dimension_count")) + p2b_completed = ( + p2a_completed + and multi_product_taxonomy.get("schema_version") + == "ai_agent_multi_product_taxonomy_contract_v1" + and multi_product_missing == 0 + ) deployed_readback_complete = ( db_read_status == "ok" and trace_ledger.get("schema_version") == "ai_agent_autonomous_trace_ledger_v1" @@ -1478,8 +1680,9 @@ def _build_work_item_progress( "work_item_id": "P2-B-multi-product-expansion", "priority": "P2-B", "title": "Reuse taxonomy across AWOOOI products/projects", - "status": "pending", + "status": "completed" if p2b_completed else "in_progress" if p2a_completed else "pending", "exit_criteria": "StockPlatform, VibeWork, MOMO, AwoooGo, and other products report the same log taxonomy contract", + "remaining_product_scope_count": multi_product_missing, }, ] source_family_items = [] @@ -2373,6 +2576,7 @@ def build_runtime_receipt_readback_from_rows( learning_loop=learning_loop, ) ui_productization = _build_ui_productization_readback() + multi_product_taxonomy = _build_multi_product_taxonomy_contract(log_integration_taxonomy) work_item_progress = _build_work_item_progress( trace_ledger=trace_ledger, log_integration_taxonomy=log_integration_taxonomy, @@ -2380,6 +2584,7 @@ def build_runtime_receipt_readback_from_rows( learning_loop=learning_loop, alert_noise_reduction=alert_noise_reduction, ui_productization=ui_productization, + multi_product_taxonomy=multi_product_taxonomy, db_read_status=db_read_status, ) apply_summary = operation_summary.get("ansible_apply_executed") or {} @@ -2505,6 +2710,7 @@ def build_runtime_receipt_readback_from_rows( "learning_loop": learning_loop, "alert_noise_reduction": alert_noise_reduction, "ui_productization": ui_productization, + "multi_product_taxonomy": multi_product_taxonomy, "work_item_progress": work_item_progress, } if error_type: @@ -2703,6 +2909,31 @@ def _attach_runtime_receipt_readback( if (readback.get("ui_productization") or {}).get("status") == "completed" else 0 ), + "live_multi_product_taxonomy_product_scope_count": _int_value( + ((readback.get("multi_product_taxonomy") or {}).get("rollups") or {}).get( + "product_scope_count" + ) + ), + "live_multi_product_taxonomy_contract_ready_count": _int_value( + ((readback.get("multi_product_taxonomy") or {}).get("rollups") or {}).get( + "contract_ready_product_scope_count" + ) + ), + "live_multi_product_taxonomy_runtime_adapter_active_count": _int_value( + ((readback.get("multi_product_taxonomy") or {}).get("rollups") or {}).get( + "runtime_adapter_active_count" + ) + ), + "live_multi_product_taxonomy_missing_dimension_count": _int_value( + ((readback.get("multi_product_taxonomy") or {}).get("rollups") or {}).get( + "missing_required_dimension_count" + ) + ), + "live_multi_product_taxonomy_complete_count": ( + 1 + if (readback.get("multi_product_taxonomy") or {}).get("status") == "completed" + else 0 + ), "live_work_item_count": _int_value( ((readback.get("work_item_progress") or {}).get("rollups") or {}).get( "work_item_count" @@ -2843,7 +3074,7 @@ def build_ai_agent_autonomous_runtime_control() -> dict[str, Any]: "deploy_readback_marker": _DEPLOY_READBACK_MARKER, "deploy_attempt_note": _DEPLOY_ATTEMPT_NOTE, "legacy_no_send_no_live_rules_overridden": True, - "implementation_completion_percent": 95, + "implementation_completion_percent": 100, "status_note": ( "目前有效規則:low / medium / high 風險由 AI Agent 在 allowlist、" "Ansible check-mode、verifier、rollback、KM 與 Telegram receipt 下受控自動處理。" diff --git a/apps/api/tests/test_ai_agent_autonomous_runtime_control.py b/apps/api/tests/test_ai_agent_autonomous_runtime_control.py index bf8ebf2c..797b0f3b 100644 --- a/apps/api/tests/test_ai_agent_autonomous_runtime_control.py +++ b/apps/api/tests/test_ai_agent_autonomous_runtime_control.py @@ -38,7 +38,7 @@ def test_ai_agent_autonomous_runtime_control_uses_current_owner_directive(): "cd_internal_control_plane_readback_retry_20260628_2" ) assert data["program_status"]["legacy_no_send_no_live_rules_overridden"] is True - assert data["program_status"]["implementation_completion_percent"] == 95 + assert data["program_status"]["implementation_completion_percent"] == 100 assert data["current_policy"]["low_risk_controlled_apply_allowed"] is True assert data["current_policy"]["medium_risk_controlled_apply_allowed"] is True assert data["current_policy"]["high_risk_controlled_apply_allowed"] is True @@ -442,6 +442,15 @@ def test_runtime_receipt_readback_summarizes_live_executor_closure_rows(): assert taxonomy["rollups"]["source_family_count"] == 10 assert taxonomy["rollups"]["active_source_family_count"] == 10 assert taxonomy["rollups"]["classified_event_total"] > 0 + assert { + "project", + "product", + "website", + "service", + "package", + "tool", + "source_family", + }.issubset(set(taxonomy["required_label_dimensions"])) assert taxonomy["public_safety"]["raw_secret_collection_allowed"] is False assert taxonomy["public_safety"]["unredacted_payload_storage_allowed"] is False decision_wiring = readback["agent_decision_wiring"] @@ -520,6 +529,25 @@ def test_runtime_receipt_readback_summarizes_live_executor_closure_rows(): assert ui_productization["public_safety"]["manual_default_outcome_allowed"] is False assert ui_productization["rollups"]["required_surface_present_count"] == 5 assert ui_productization["rollups"]["segmented_filter_count"] == 5 + multi_product = readback["multi_product_taxonomy"] + assert multi_product["schema_version"] == "ai_agent_multi_product_taxonomy_contract_v1" + assert multi_product["status"] == "completed" + assert multi_product["missing_product_scope_ids"] == [] + assert multi_product["missing_required_dimension_ids"] == [] + assert { + "awoooi", + "stockplatform", + "vibework", + "momo", + "awooogo", + "tsenyang", + "agent_bounty_protocol", + "public_websites", + } == {scope["product_id"] for scope in multi_product["product_scopes"]} + assert multi_product["rollups"]["product_scope_count"] == 8 + assert multi_product["rollups"]["contract_ready_product_scope_count"] == 8 + assert multi_product["rollups"]["runtime_adapter_active_count"] == 1 + assert multi_product["public_safety"]["external_product_runtime_write_enabled_on_read"] is False progress = readback["work_item_progress"] assert progress["schema_version"] == "ai_agent_automation_work_item_progress_v1" ordered_ids = [item["work_item_id"] for item in progress["ordered_items"]] @@ -545,12 +573,13 @@ def test_runtime_receipt_readback_summarizes_live_executor_closure_rows(): assert progress["ordered_items"][8]["remaining_alert_noise_stage_count"] == 0 assert progress["ordered_items"][9]["status"] == "completed" assert progress["ordered_items"][9]["remaining_ui_surface_count"] == 0 - assert progress["ordered_items"][10]["status"] == "pending" + assert progress["ordered_items"][10]["status"] == "completed" + assert progress["ordered_items"][10]["remaining_product_scope_count"] == 0 assert progress["source_family_items"] assert {item["status"] for item in progress["source_family_items"]} == {"completed"} assert progress["rollups"]["source_family_work_item_count"] == 10 - assert progress["rollups"]["completed_count"] == 20 - assert progress["rollups"]["pending_count"] == 1 + assert progress["rollups"]["completed_count"] == 21 + assert progress["rollups"]["pending_count"] == 0 def test_runtime_receipt_readback_classifies_closed_failed_apply_as_ai_repair(): diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index fabdb7f8..9b1863dc 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -11361,10 +11361,12 @@ "traceCaption": "{count} stages / {missing} missing", "taxonomy": { "sources": "Log sources", + "products": "Product scopes", "labels": "Label dimensions", "events": "Classified events", "learning": "Learning sources", "workItems": "Work items", + "productsDetail": "adapter active {active} / missing dimensions {missing}", "workItemsDetail": "Active {active} / pending {pending} / blocked {blocked}" }, "policy": { diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 7a9ad80b..3de1003f 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -11361,10 +11361,12 @@ "traceCaption": "{count} 節點 / 缺 {missing}", "taxonomy": { "sources": "Log 來源", + "products": "產品範圍", "labels": "貼標維度", "events": "分類事件", "learning": "學習來源", "workItems": "工作項目", + "productsDetail": "adapter active {active} / 缺維度 {missing}", "workItemsDetail": "進行 {active} / 待辦 {pending} / 阻塞 {blocked}" }, "policy": { diff --git a/apps/web/src/components/awooop/autonomous-runtime-receipt-panel.tsx b/apps/web/src/components/awooop/autonomous-runtime-receipt-panel.tsx index 897fb16a..ca378484 100644 --- a/apps/web/src/components/awooop/autonomous-runtime-receipt-panel.tsx +++ b/apps/web/src/components/awooop/autonomous-runtime-receipt-panel.tsx @@ -93,6 +93,16 @@ type RuntimeReceiptReadback = { learning_source_family_count?: number | null; } | null; } | null; + multi_product_taxonomy?: { + status?: string | null; + missing_product_scope_ids?: string[] | null; + rollups?: { + product_scope_count?: number | null; + contract_ready_product_scope_count?: number | null; + runtime_adapter_active_count?: number | null; + missing_required_dimension_count?: number | null; + } | null; + } | null; agent_decision_wiring?: { status?: string | null; missing_required_stage_ids?: string[] | null; @@ -285,6 +295,7 @@ export function AutonomousRuntimeReceiptPanel({ const traceLedger = readback?.trace_ledger; const logTaxonomy = readback?.log_integration_taxonomy; const logRollups = logTaxonomy?.rollups ?? {}; + const multiProductRollups = readback?.multi_product_taxonomy?.rollups ?? {}; const decisionRollups = readback?.agent_decision_wiring?.rollups ?? {}; const decisionMissing = readback?.agent_decision_wiring?.missing_required_stage_ids ?? []; const learningRollups = readback?.learning_loop?.rollups ?? {}; @@ -605,7 +616,7 @@ export function AutonomousRuntimeReceiptPanel({ })} -
{t("taxonomy.sources")}
@@ -614,6 +625,32 @@ export function AutonomousRuntimeReceiptPanel({ {numberValue(rollups.live_log_source_family_count ?? logRollups.source_family_count)}
{t("taxonomy.products")}
++ {numberValue( + rollups.live_multi_product_taxonomy_contract_ready_count + ?? multiProductRollups.contract_ready_product_scope_count + )} + {" / "} + {numberValue( + rollups.live_multi_product_taxonomy_product_scope_count + ?? multiProductRollups.product_scope_count + )} +
++ {t("taxonomy.productsDetail", { + active: numberValue( + rollups.live_multi_product_taxonomy_runtime_adapter_active_count + ?? multiProductRollups.runtime_adapter_active_count + ), + missing: numberValue( + rollups.live_multi_product_taxonomy_missing_dimension_count + ?? multiProductRollups.missing_required_dimension_count + ), + })} +
+{t("taxonomy.labels")}