diff --git a/.gitea/workflows/cd.yaml b/.gitea/workflows/cd.yaml index 2b17b2123..e0b4bc5cb 100644 --- a/.gitea/workflows/cd.yaml +++ b/.gitea/workflows/cd.yaml @@ -431,6 +431,8 @@ jobs: ;; apps/api/src/api/v1/platform/events.py) ;; + apps/api/src/api/v1/platform/operator_runs.py) + ;; apps/api/src/jobs/ai_slo_watchdog_job.py) ;; apps/api/src/models/knowledge.py) @@ -874,6 +876,7 @@ jobs: src/services/awoooi_onboarding_source_contracts.py \ src/services/awoooi_priority_work_order_readback.py \ src/services/awoooi_product_onboarding_guard.py \ + src/api/v1/platform/operator_runs.py \ src/services/channel_hub.py \ src/services/p0_cicd_baseline_source_readiness.py \ src/services/product_awoooi_manifest_standard.py \ diff --git a/apps/api/src/api/v1/platform/operator_runs.py b/apps/api/src/api/v1/platform/operator_runs.py index 5859758c2..c681b80ba 100644 --- a/apps/api/src/api/v1/platform/operator_runs.py +++ b/apps/api/src/api/v1/platform/operator_runs.py @@ -138,6 +138,9 @@ class AiAlertCardDeliveryItem(BaseModel): controlled_playbook_queue: bool = False runtime_write_gate_state: str = "unknown" delivery_receipt_readback_required: bool + learning_writeback_ready: bool = False + learning_writeback_status: str = "not_evaluated" + learning_writeback_refs: dict[str, Any] = Field(default_factory=dict) source_refs: dict[str, Any] run_state: str | None = None agent_id: str | None = None @@ -159,6 +162,14 @@ class AiAlertCardDeliverySummary(BaseModel): delivery_receipt_required_total: int runtime_write_gate_open_count: int runtime_write_allowed: bool + learning_writeback_ready_total: int = 0 + learning_writeback_missing_total: int = 0 + km_writeback_ref_ready_total: int = 0 + playbook_writeback_ref_ready_total: int = 0 + rag_chunk_ref_ready_total: int = 0 + mcp_evidence_ref_ready_total: int = 0 + post_verifier_ref_ready_total: int = 0 + learning_writeback_next_action: str = "none" latest_sent_at: datetime | None = None latest_queued_at: datetime | None = None production_write_count: int = 0 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 db223302f..ad9528353 100644 --- a/apps/api/src/services/awoooi_priority_work_order_readback.py +++ b/apps/api/src/services/awoooi_priority_work_order_readback.py @@ -338,10 +338,11 @@ _COMMANDER_INSERTED_REQUIREMENT_WORK_ITEMS: list[dict[str, Any]] = [ "Telegram alert AI automation matrix endpoint 已實作;channel_hub API direct sender 已改走 " "TelegramGateway / AwoooP outbound receipt path;scripts/ops 4 條 legacy direct Bot API fallback " "已改為 AWOOI API / TelegramGateway receipt path;目前明確讀回 workflow direct send 0、" - "ops script direct gap 0、API direct gap 0。" + "ops script direct gap 0、API direct gap 0;/runs/ai-alert-cards 已補 KM / PlayBook / RAG / MCP " + "learning_writeback_refs read model。" ), - "acceptance": "Telegram alert matrix 顯示 total、DB receipt、AI route、controlled queue、manual/default gap 與 direct send gap。", - "next_action": "把 gateway AI alert card delivery receipt 接到 KM / PlayBook learning writeback readback,驗證 Telegram 不再是人工終局。", + "acceptance": "Telegram alert matrix 顯示 total、DB receipt、AI route、controlled queue、manual/default gap、direct send gap 與 learning refs。", + "next_action": "把 Telegram alert learning refs 接到 KM / PlayBook / RAG / MCP registry readback,讓 AI Loop Agent 可重用這些 receipt。", "mapped_workplan_id": "P0-006", }, { diff --git a/apps/api/src/services/platform_operator_service.py b/apps/api/src/services/platform_operator_service.py index a66a1593b..bc75c3b87 100644 --- a/apps/api/src/services/platform_operator_service.py +++ b/apps/api/src/services/platform_operator_service.py @@ -1378,6 +1378,23 @@ async def list_ai_alert_card_delivery_readback( '0' ) <> '0' ) AS runtime_write_gate_open_count, + COUNT(*) FILTER ( + WHERE COALESCE( + m.source_envelope #>> + '{{ai_automation_alert_card,delivery_receipt_readback_required}}', + '' + ) = 'true' + AND COALESCE( + m.source_envelope #>> + '{{ai_automation_alert_card,event_type}}', + '' + ) <> '' + AND COALESCE( + m.source_envelope #>> + '{{ai_automation_alert_card,lane}}', + '' + ) <> '' + ) AS learning_writeback_ready_total, MAX(m.sent_at) AS latest_sent_at, MAX(m.queued_at) AS latest_queued_at FROM awooop_outbound_message m @@ -1464,6 +1481,10 @@ def _ai_alert_card_delivery_summary_from_row( runtime_write_gate_open_count = _safe_int( row.get("runtime_write_gate_open_count") ) + learning_writeback_ready_total = _safe_int( + row.get("learning_writeback_ready_total") + ) + learning_writeback_missing_total = max(total - learning_writeback_ready_total, 0) status_value = "no_delivery_receipt" if total == 0 else "observed" if failed_total > 0: status_value = "delivery_failure_observed" @@ -1486,6 +1507,18 @@ def _ai_alert_card_delivery_summary_from_row( ), "runtime_write_gate_open_count": runtime_write_gate_open_count, "runtime_write_allowed": runtime_write_gate_open_count > 0, + "learning_writeback_ready_total": learning_writeback_ready_total, + "learning_writeback_missing_total": learning_writeback_missing_total, + "km_writeback_ref_ready_total": learning_writeback_ready_total, + "playbook_writeback_ref_ready_total": learning_writeback_ready_total, + "rag_chunk_ref_ready_total": learning_writeback_ready_total, + "mcp_evidence_ref_ready_total": learning_writeback_ready_total, + "post_verifier_ref_ready_total": learning_writeback_ready_total, + "learning_writeback_next_action": ( + "none" + if total > 0 and learning_writeback_missing_total == 0 + else "ensure_ai_alert_cards_include_event_type_lane_and_delivery_receipt" + ), "latest_sent_at": row.get("latest_sent_at"), "latest_queued_at": row.get("latest_queued_at"), "production_write_count": 0, @@ -1502,6 +1535,13 @@ def _ai_alert_card_delivery_item(row: Mapping[str, Any]) -> dict[str, Any]: alert_card.get("runtime_write_gate_count") ) event_at = row.get("sent_at") or row.get("queued_at") + learning_writeback_refs = _ai_alert_card_learning_writeback_refs( + alert_card=alert_card, + source_refs=source_refs, + message_id=row.get("message_id"), + run_id=run_id, + project_id=project_id, + ) return { "message_id": row.get("message_id"), "run_id": run_id, @@ -1527,6 +1567,11 @@ def _ai_alert_card_delivery_item(row: Mapping[str, Any]) -> dict[str, Any]: "delivery_receipt_readback_required": bool( alert_card.get("delivery_receipt_readback_required") ), + "learning_writeback_ready": ( + learning_writeback_refs["status"] == "ready" + ), + "learning_writeback_status": learning_writeback_refs["status"], + "learning_writeback_refs": learning_writeback_refs, "source_refs": source_refs, "run_state": row.get("run_state"), "agent_id": row.get("agent_id"), @@ -1539,6 +1584,77 @@ def _ai_alert_card_delivery_item(row: Mapping[str, Any]) -> dict[str, Any]: } +def _slug_ref(value: Any, fallback: str) -> str: + slug = re.sub(r"[^a-z0-9_.:-]+", "-", str(value or "").strip().lower()) + slug = slug.strip("-") + return slug or fallback + + +def _ai_alert_card_learning_writeback_refs( + *, + alert_card: Mapping[str, Any], + source_refs: Mapping[str, Any], + message_id: Any, + run_id: Any, + project_id: str, +) -> dict[str, Any]: + """Build deterministic KM / PlayBook / RAG / MCP refs for one alert receipt.""" + event_type = str(alert_card.get("event_type") or "") + lane = str(alert_card.get("lane") or "") + target = str(alert_card.get("target") or "") + delivery_required = bool(alert_card.get("delivery_receipt_readback_required")) + event_slug = _slug_ref(event_type, "unknown-event") + lane_slug = _slug_ref(lane, "unknown-lane") + target_slug = _slug_ref(target, "unknown-target") + message_ref = _slug_ref(message_id, "unknown-message") + run_ref = _slug_ref(run_id, "unknown-run") + missing = [ + key + for key, value in ( + ("event_type", event_type), + ("lane", lane), + ("delivery_receipt_readback_required", delivery_required), + ) + if value in ("", False, None) + ] + status = "ready" if not missing else "missing_required_alert_card_metadata" + + fingerprints = source_refs.get("fingerprints") if isinstance(source_refs, Mapping) else [] + alert_ids = source_refs.get("alert_ids") if isinstance(source_refs, Mapping) else [] + return { + "schema_version": "ai_alert_card_learning_writeback_refs_v1", + "status": status, + "work_item_id": "CIR-P0-TG-001", + "receipt_source": "awooop_outbound_message", + "receipt_id": str(message_id or ""), + "run_id": str(run_id or ""), + "project_id": project_id, + "km_entry_ref": f"km://{project_id}/telegram-ai-alert-card/{event_slug}", + "playbook_candidate_ref": ( + f"playbook://{project_id}/telegram-ai-alert-card/{lane_slug}/{target_slug}" + ), + "rag_chunk_ref": ( + f"rag://{project_id}/telegram-ai-alert-card/{event_slug}/{message_ref}" + ), + "mcp_evidence_ref": ( + f"mcp://{project_id}/awooop_outbound_message/{message_ref}" + ), + "post_verifier_ref": ( + f"verifier://{project_id}/telegram-alert-delivery-receipt/{run_ref}" + ), + "source_fingerprints": fingerprints if isinstance(fingerprints, list) else [], + "alert_ids": alert_ids if isinstance(alert_ids, list) else [], + "missing_required_fields": missing, + "production_write_performed": False, + "runtime_send_performed": False, + "next_action": ( + "none" + if status == "ready" + else "ensure_ai_alert_cards_include_event_type_lane_and_delivery_receipt" + ), + } + + async def _fetch_callback_reply_audit_summary( db: Any, *, diff --git a/apps/api/src/services/telegram_alert_ai_automation_matrix.py b/apps/api/src/services/telegram_alert_ai_automation_matrix.py index 792f22398..383221a15 100644 --- a/apps/api/src/services/telegram_alert_ai_automation_matrix.py +++ b/apps/api/src/services/telegram_alert_ai_automation_matrix.py @@ -81,6 +81,9 @@ def load_latest_telegram_alert_ai_automation_matrix( readability_summary = readability_guard["summary"] digest_rollups = digest_policy["rollups"] source_proof = _inspect_source_contract(repo_root) + learning_writeback_refs_present = bool( + source_proof["ai_alert_card_learning_writeback_refs_present"] + ) direct_gap_count = int(egress_summary["direct_bot_api_call_count"]) direct_gap_file_count = int(egress_summary["direct_bot_api_file_count"]) @@ -110,7 +113,7 @@ def load_latest_telegram_alert_ai_automation_matrix( "ai_route": "ready_ai_automation_alert_card_v1", "controlled_queue": "ready_controlled_playbook_queue", "post_verifier": "ready_telegram_alert_readability_guard", - "learning_writeback": "ready_delivery_receipt_readback_required", + "learning_writeback": "ready_learning_writeback_refs", "manual_default_gap_count": 0, "direct_send_gap_count": 0, "evidence_refs": [ @@ -231,7 +234,7 @@ def load_latest_telegram_alert_ai_automation_matrix( "ai_route": "ready_event_type_lane_target_filters", "controlled_queue": "read_only_query_no_send", "post_verifier": "ready_source_contract_present", - "learning_writeback": "ready_delivery_receipt_summary", + "learning_writeback": "ready_delivery_receipt_summary_and_learning_writeback_refs", "manual_default_gap_count": 0, "direct_send_gap_count": 0, "evidence_refs": [ @@ -245,6 +248,7 @@ def load_latest_telegram_alert_ai_automation_matrix( api_direct_gap_count=api_direct_gap_count, ops_script_gap_count=ops_script_gap_count, direct_gap_count=direct_gap_count, + learning_writeback_refs_present=learning_writeback_refs_present, ) next_controlled_actions = _ordered_controlled_actions(next_priority_action) summary = _build_summary( @@ -300,7 +304,14 @@ def load_latest_telegram_alert_ai_automation_matrix( else "Known direct Bot API surfaces remain the concrete AI automation gap; API direct senders are clear when api_direct_send_gap_count is 0." ), ( - "Manual/default semantics are only acceptable for critical/break-glass or historical evidence; the next P0 step is learning/writeback readback." + ( + ( + "Manual/default semantics are only acceptable for critical/break-glass or historical evidence; " + "learning refs are present, so the next P0 step is KM / PlayBook / RAG / MCP registry readback." + ) + if learning_writeback_refs_present + else "Manual/default semantics are only acceptable for critical/break-glass or historical evidence; the next P0 step is learning/writeback readback." + ) if direct_gap_closed else "Manual/default semantics are only acceptable for critical/break-glass or historical evidence; direct send gaps remain controlled migration candidates." ), @@ -329,15 +340,7 @@ def _ordered_controlled_actions(next_priority_action: dict[str, Any]) -> list[di "requires_runtime_send": False, "post_verifier": "egress inventory and owner acceptance matrix show no unreviewed direct sends.", }, - { - "action_id": "bind_ai_alert_cards_to_learning_writeback", - "scope": "ai_automation_alert_card_v1 outbound mirror metadata", - "priority": "P0", - "why": "Telegram must become an AI automation learning input, not a manual terminal state.", - "requires_secret": False, - "requires_runtime_send": False, - "post_verifier": "/api/v1/platform/runs/ai-alert-cards readback shows delivery_receipt_readback_required.", - }, + _legacy_bind_ai_alert_cards_action(), ] ordered: list[dict[str, Any]] = [] seen: set[str] = set() @@ -432,6 +435,14 @@ def _inspect_source_contract(repo_root: Path) -> dict[str, int | bool]: "apps/api/src/api/v1/platform/operator_runs.py", "/runs/ai-alert-cards", ), + "ai_alert_card_learning_writeback_refs_present": ( + "apps/api/src/api/v1/platform/operator_runs.py", + "learning_writeback_refs", + ), + "ai_alert_card_learning_writeback_summary_present": ( + "apps/api/src/api/v1/platform/operator_runs.py", + "learning_writeback_ready_total", + ), } result: dict[str, int | bool] = {} for key, (relative_path, marker) in source_checks.items(): @@ -440,6 +451,8 @@ def _inspect_source_contract(repo_root: Path) -> dict[str, int | bool]: result[key] = present if key == "ai_alert_card_delivery_readback_endpoint_present": result["ai_alert_card_delivery_readback_endpoint_count"] = 1 if present else 0 + if key == "ai_alert_card_learning_writeback_refs_present": + result["ai_alert_card_learning_writeback_refs_count"] = 1 if present else 0 required = [key for key, value in result.items() if key.endswith("_present") and value is not True] if required: @@ -464,6 +477,7 @@ def _next_priority_action( api_direct_gap_count: int, ops_script_gap_count: int, direct_gap_count: int, + learning_writeback_refs_present: bool, ) -> dict[str, Any]: if api_direct_gap_count > 0: return { @@ -495,6 +509,28 @@ def _next_priority_action( "requires_runtime_send": False, "post_verifier": "telegram-notification-egress-inventory direct gap count reaches 0.", } + if not learning_writeback_refs_present: + return { + "action_id": "bind_ai_alert_cards_to_learning_writeback", + "scope": "ai_automation_alert_card_v1 outbound mirror metadata", + "priority": "P0", + "why": "All direct Bot API gaps are clear; close the learning/writeback loop.", + "requires_secret": False, + "requires_runtime_send": False, + "post_verifier": "/api/v1/platform/runs/ai-alert-cards readback shows delivery_receipt_readback_required and learning writeback refs.", + } + return { + "action_id": "promote_telegram_alert_learning_refs_to_km_rag_mcp_registry", + "scope": "AI alert card receipt refs -> KM / PlayBook / RAG / MCP registry readback", + "priority": "P0", + "why": "Alert cards now expose learning refs; the next closure is registry/readback integration for AI Loop Agent reuse.", + "requires_secret": False, + "requires_runtime_send": False, + "post_verifier": "KM / PlayBook / RAG / MCP registry readback includes CIR-P0-TG-001 alert-card receipt refs.", + } + + +def _legacy_bind_ai_alert_cards_action() -> dict[str, Any]: return { "action_id": "bind_ai_alert_cards_to_learning_writeback", "scope": "ai_automation_alert_card_v1 outbound mirror metadata", @@ -550,6 +586,9 @@ def _build_summary( "ai_alert_card_delivery_readback_endpoint_count": source_proof[ "ai_alert_card_delivery_readback_endpoint_count" ], + "ai_alert_card_learning_writeback_refs_count": source_proof[ + "ai_alert_card_learning_writeback_refs_count" + ], "next_priority_action_id": next_priority_action["action_id"], "next_priority_work_item_id": "CIR-P0-TG-001", } diff --git a/apps/api/tests/test_awooop_operator_timeline_labels.py b/apps/api/tests/test_awooop_operator_timeline_labels.py index c45d3b670..d7fc86e1c 100644 --- a/apps/api/tests/test_awooop_operator_timeline_labels.py +++ b/apps/api/tests/test_awooop_operator_timeline_labels.py @@ -973,6 +973,24 @@ def test_ai_alert_card_delivery_item_uses_metadata_without_raw_content() -> None assert item["runtime_write_gate_count"] == 1 assert item["runtime_write_allowed"] is True assert item["delivery_receipt_readback_required"] is True + assert item["learning_writeback_ready"] is True + assert item["learning_writeback_status"] == "ready" + assert item["learning_writeback_refs"]["km_entry_ref"] == ( + "km://awoooi/telegram-ai-alert-card/wazuh_dashboard_api_readback_degraded" + ) + assert item["learning_writeback_refs"]["playbook_candidate_ref"] == ( + "playbook://awoooi/telegram-ai-alert-card/" + "siem_observability_readback_degraded/wazuh_dashboard_api" + ) + assert item["learning_writeback_refs"]["rag_chunk_ref"].startswith( + "rag://awoooi/telegram-ai-alert-card/" + ) + assert item["learning_writeback_refs"]["mcp_evidence_ref"].endswith( + "66cdb6ad-46a4-48f5-9d3b-b1ac9c0b2e92" + ) + assert item["learning_writeback_refs"]["post_verifier_ref"].endswith( + "5c0306e0-591a-5445-9a33-80f499426b38" + ) assert item["source_refs"]["alert_ids"] == [ "wazuh_dashboard_api_readback_degraded" ] @@ -991,6 +1009,7 @@ def test_ai_alert_card_delivery_summary_keeps_no_false_green_status() -> None: "shadow_total": 0, "delivery_receipt_required_total": 2, "runtime_write_gate_open_count": 1, + "learning_writeback_ready_total": 1, "latest_sent_at": datetime(2026, 6, 25, 9, 40, 5), "latest_queued_at": datetime(2026, 6, 25, 9, 40, 0), }, @@ -1004,6 +1023,16 @@ def test_ai_alert_card_delivery_summary_keeps_no_false_green_status() -> None: assert summary["delivery_receipt_required_total"] == 2 assert summary["runtime_write_gate_open_count"] == 1 assert summary["runtime_write_allowed"] is True + assert summary["learning_writeback_ready_total"] == 1 + assert summary["learning_writeback_missing_total"] == 1 + assert summary["km_writeback_ref_ready_total"] == 1 + assert summary["playbook_writeback_ref_ready_total"] == 1 + assert summary["rag_chunk_ref_ready_total"] == 1 + assert summary["mcp_evidence_ref_ready_total"] == 1 + assert summary["post_verifier_ref_ready_total"] == 1 + assert summary["learning_writeback_next_action"] == ( + "ensure_ai_alert_cards_include_event_type_lane_and_delivery_receipt" + ) assert summary["production_write_count"] == 0 @@ -1032,6 +1061,45 @@ def test_list_ai_alert_cards_response_preserves_delivery_metadata() -> None: "candidate_only": False, "controlled_playbook_queue": True, "delivery_receipt_readback_required": True, + "learning_writeback_ready": True, + "learning_writeback_status": "ready", + "learning_writeback_refs": { + "schema_version": "ai_alert_card_learning_writeback_refs_v1", + "status": "ready", + "work_item_id": "CIR-P0-TG-001", + "receipt_source": "awooop_outbound_message", + "receipt_id": str(message_id), + "run_id": str(run_id), + "project_id": "awoooi", + "km_entry_ref": ( + "km://awoooi/telegram-ai-alert-card/" + "wazuh_dashboard_api_readback_degraded" + ), + "playbook_candidate_ref": ( + "playbook://awoooi/telegram-ai-alert-card/" + "siem_observability_readback_degraded/" + "wazuh_dashboard_api" + ), + "rag_chunk_ref": ( + "rag://awoooi/telegram-ai-alert-card/" + "wazuh_dashboard_api_readback_degraded/" + f"{message_id}" + ), + "mcp_evidence_ref": ( + "mcp://awoooi/awooop_outbound_message/" + f"{message_id}" + ), + "post_verifier_ref": ( + "verifier://awoooi/telegram-alert-delivery-receipt/" + f"{run_id}" + ), + "source_fingerprints": [], + "alert_ids": ["wazuh_dashboard_api_readback_degraded"], + "missing_required_fields": [], + "production_write_performed": False, + "runtime_send_performed": False, + "next_action": "none", + }, "source_refs": { "alert_ids": ["wazuh_dashboard_api_readback_degraded"], }, @@ -1061,6 +1129,14 @@ def test_list_ai_alert_cards_response_preserves_delivery_metadata() -> None: "delivery_receipt_required_total": 1, "runtime_write_gate_open_count": 1, "runtime_write_allowed": True, + "learning_writeback_ready_total": 1, + "learning_writeback_missing_total": 0, + "km_writeback_ref_ready_total": 1, + "playbook_writeback_ref_ready_total": 1, + "rag_chunk_ref_ready_total": 1, + "mcp_evidence_ref_ready_total": 1, + "post_verifier_ref_ready_total": 1, + "learning_writeback_next_action": "none", "latest_sent_at": datetime(2026, 6, 25, 9, 40, 5), "latest_queued_at": datetime(2026, 6, 25, 9, 40, 0), "production_write_count": 0, @@ -1072,7 +1148,12 @@ def test_list_ai_alert_cards_response_preserves_delivery_metadata() -> None: "wazuh_dashboard_api_readback_degraded" ) assert dumped["items"][0]["runtime_write_allowed"] is True + assert dumped["items"][0]["learning_writeback_ready"] is True + assert dumped["items"][0]["learning_writeback_refs"]["work_item_id"] == ( + "CIR-P0-TG-001" + ) assert dumped["summary"]["delivery_receipt_required_total"] == 1 + assert dumped["summary"]["learning_writeback_ready_total"] == 1 assert dumped["summary"]["production_write_count"] == 0 diff --git a/apps/api/tests/test_telegram_alert_ai_automation_matrix_api.py b/apps/api/tests/test_telegram_alert_ai_automation_matrix_api.py index 01e6f95f1..8d1a4d9f3 100644 --- a/apps/api/tests/test_telegram_alert_ai_automation_matrix_api.py +++ b/apps/api/tests/test_telegram_alert_ai_automation_matrix_api.py @@ -52,7 +52,10 @@ def test_telegram_alert_ai_automation_matrix_loader_returns_gap_matrix(): assert summary["secret_value_read_count"] == 0 assert summary["runtime_write_performed_count"] == 0 assert summary["ai_alert_card_delivery_readback_endpoint_count"] == 1 - assert summary["next_priority_action_id"] == "bind_ai_alert_cards_to_learning_writeback" + assert summary["ai_alert_card_learning_writeback_refs_count"] == 1 + assert summary["next_priority_action_id"] == ( + "promote_telegram_alert_learning_refs_to_km_rag_mcp_registry" + ) matrix = {item["surface_id"]: item for item in payload["matrix"]} assert matrix["telegram_gateway_ai_alert_cards"]["status"] == ( @@ -75,6 +78,9 @@ def test_telegram_alert_ai_automation_matrix_loader_returns_gap_matrix(): assert matrix["telegram_ai_alert_card_delivery_readback_endpoint"][ "db_or_log_receipt" ] == "ready_awooop_outbound_message_query" + assert matrix["telegram_ai_alert_card_delivery_readback_endpoint"][ + "learning_writeback" + ] == "ready_delivery_receipt_summary_and_learning_writeback_refs" assert payload["direct_send_gap_refs"] == [] assert not any( @@ -108,7 +114,9 @@ def test_telegram_matrix_source_contract_supports_container_layout(tmp_path): encoding="utf-8", ) (tmp_path / "src/api/v1/platform/operator_runs.py").write_text( - '"/runs/ai-alert-cards"\n', + '"/runs/ai-alert-cards"\n' + "learning_writeback_refs\n" + "learning_writeback_ready_total\n", encoding="utf-8", ) @@ -120,6 +128,8 @@ def test_telegram_matrix_source_contract_supports_container_layout(tmp_path): assert proof["outbound_message_model_present"] is True assert proof["ai_alert_card_delivery_readback_endpoint_present"] is True assert proof["ai_alert_card_delivery_readback_endpoint_count"] == 1 + assert proof["ai_alert_card_learning_writeback_refs_present"] is True + assert proof["ai_alert_card_learning_writeback_refs_count"] == 1 def test_telegram_alert_ai_automation_matrix_endpoint_returns_readback(): diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 3a858d3ea..23c30e66c 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,19 @@ +## 2026-07-02 — 19:58 CIR-P0-TG-001 AI alert card learning writeback refs + +**完成內容**: +- `/api/v1/platform/runs/ai-alert-cards` 的 response model 已補 `learning_writeback_ready`、`learning_writeback_status` 與 `learning_writeback_refs`,每筆 AI alert card delivery receipt 會投影 deterministic KM entry、PlayBook candidate、RAG chunk、MCP evidence 與 post-verifier refs。 +- summary 已補 `learning_writeback_ready_total`、`learning_writeback_missing_total`、KM / PlayBook / RAG / MCP / verifier ready totals 與 `learning_writeback_next_action`,讓 Telegram 告警不只停在通知送達,而是可被 AI Loop Agent 當學習與後續自動化依據。 +- `telegram_alert_ai_automation_matrix` source contract 已要求 `learning_writeback_refs` / `learning_writeback_ready_total`;本地 matrix 讀回 direct gap `0`、learning refs count `1`,下一個 P0 action 推進為 `promote_telegram_alert_learning_refs_to_km_rag_mcp_registry`。 +- `awoooi-priority-work-order-readback` 與 commander inserted requirements ledger 已同步:`CIR-P0-TG-001` 不再停在 bind delivery receipt,下一步改為 KM / PlayBook / RAG / MCP registry readback。 + +**驗證**: +- `DATABASE_URL=postgresql+asyncpg://test:test@localhost/test PYTHONPATH=apps/api python3.11 -m pytest apps/api/tests/test_telegram_alert_ai_automation_matrix_api.py apps/api/tests/test_ai_agent_report_truth_actionability_review.py apps/api/tests/test_ai_agent_report_truth_actionability_review_api.py apps/api/tests/test_awoooi_priority_work_order_readback_api.py apps/api/tests/test_awooop_operator_timeline_labels.py -q -p no:cacheprovider`:`96 passed`。 +- `DATABASE_URL=postgresql+asyncpg://test:test@localhost/test PYTHONPATH=apps/api python3.11 -m pytest ops/runner/test_cd_controlled_runtime_profile.py -q -p no:cacheprovider`:`45 passed`。 +- `python3.11 -m py_compile apps/api/src/api/v1/platform/operator_runs.py apps/api/src/services/platform_operator_service.py apps/api/src/services/telegram_alert_ai_automation_matrix.py`、`git diff --check`:通過。 + +**仍維持**: +- 未送 Telegram、未呼叫 Bot API、未讀 secret / token / `.env` / raw sessions / SQLite / auth;未寫 production DB、未改 receiver route、未 workflow_dispatch、未重啟主機 / VM / Docker / Nginx / K3s / DB / firewall。 + ## 2026-07-02 — 19:23 CIR-P0-TG-001 ops direct Telegram sender 收斂完成 **完成內容**: diff --git a/docs/workplans/2026-07-02-commander-inserted-requirements-priority-ledger.md b/docs/workplans/2026-07-02-commander-inserted-requirements-priority-ledger.md index aab0ec8fa..206c7fc38 100644 --- a/docs/workplans/2026-07-02-commander-inserted-requirements-priority-ledger.md +++ b/docs/workplans/2026-07-02-commander-inserted-requirements-priority-ledger.md @@ -88,7 +88,7 @@ | 2 | CIR-P0-AILOOP-002 | P0 | 「所有 AI 自動化完整流程、節點必須完整且詳盡紀錄」 | 每次 AI 自動化事件記錄 trace_id、run_id、work_item_id、節點狀態、timestamp、evidence refs、verifier、rollback、KM/PlayBook trust writeback | 已升為 API/UI 可見 P0 工作項;節點 schema 待補 | 建立 AI automation node receipt schema,先套用 Telegram alert 與 P0 runtime lanes | | 3 | CIR-P0-LOG-001 | P0 | 「所有專案、產品、網站、服務、套件、工具的 LOG 都要收進去」 | 建立全域 LOG source registry 與 ingestion adapters,含 redaction、retention、freshness、last_success、owner lane | 已升為 API/UI 可見 P0 工作項 | 先做 metadata-only LOG source registry,不讀 secret / raw payload | | 4 | CIR-P0-LOG-002 | P0 | 「LOG 要貼標、分類、分群,讓 AI Agent 可以處理」 | LOG 自動貼 product/project/site/service/package/tool、severity、lane、confidence、AI route、cluster_id、fingerprint | 已升為 API/UI 可見 P0 工作項 | 定義 tag taxonomy 與 clustering readback,先覆蓋 Telegram、CPU、backup、freshness | -| 5 | CIR-P0-TG-001 | P0 | 「所有發送到 Telegram 的監控告警是否全面清查、完整寫入 DB 或日誌、符合 AI 自動化?」 | 每個 Telegram 告警映射 DB/log receipt、AI route、controlled queue、work item、post verifier、learning writeback;direct send / manual default 列缺口 | workflow direct send 已收斂到 0;API direct gap 仍需追 | 擴充 Telegram alert inventory/readback,人工預設只允許 critical / break-glass 或歷史證據 | +| 5 | CIR-P0-TG-001 | P0 | 「所有發送到 Telegram 的監控告警是否全面清查、完整寫入 DB 或日誌、符合 AI 自動化?」 | 每個 Telegram 告警映射 DB/log receipt、AI route、controlled queue、work item、post verifier、learning writeback;direct send / manual default 列缺口 | workflow / ops / API direct send 已收斂到 0;`/runs/ai-alert-cards` 已補 KM / PlayBook / RAG / MCP `learning_writeback_refs` read model | 接 KM / PlayBook / RAG / MCP registry readback,人工預設只允許 critical / break-glass 或歷史證據 | | 6 | CIR-P0-IA-001 | P0 | 「IwoooS 被移除、導航列菜單被移除,且沒有整合到相關頁面」 | 建立 IA/nav removal recovery ledger:被移除 menu/route/page 必須有保留、合併、替代目的地、redirect、權限邊界與 smoke | 已升為 API/UI 可見 P0 工作項;route/menu 盤點待補 | 盤點 navigation config、route list 與 removed-page destination,補 readback | | 7 | CIR-P1-AUTO-001 | P1 | 「不能偏離不代表一定要每一個工作完成,才能進行下一個」 | 建立主線並行推進規則:不離開 P0/P1 主線,但依賴允許時可同時推 API/UI、LOG、Telegram、KM/RAG/MCP/PlayBook、verifier | 已升為 API/UI 可見 P1 工作項 | 後續回報以 active lane + evidence 更新,不等待單一項完全結束 | | 8 | CIR-P1-UI-001 | P1 | 「UI/UX 工作項目不見;整個前端頁面一大堆文字,要主流專業產品做法」 | UI/UX 成為主線:first-viewport cockpit、cards/flow rows、少文字牆、route/nav 一致、desktop/mobile smoke | 已升為 API/UI spotlight | 先讓 AI Loop / LOG / Telegram / IA 工作項在 Work Items spotlight 可見,再排 UI smoke | diff --git a/ops/runner/test_cd_controlled_runtime_profile.py b/ops/runner/test_cd_controlled_runtime_profile.py index c47273ead..1707a37c6 100644 --- a/ops/runner/test_cd_controlled_runtime_profile.py +++ b/ops/runner/test_cd_controlled_runtime_profile.py @@ -199,6 +199,7 @@ def test_telegram_alert_ai_automation_matrix_stays_on_controlled_runtime_profile "docs/security/telegram-notification-egress-migration-plan-draft.snapshot.json)", "docs/security/telegram-notification-egress-no-new-bypass-guard.snapshot.json)", "docs/security/telegram-notification-egress-owner-response-acceptance.snapshot.json)", + "apps/api/src/api/v1/platform/operator_runs.py)", "apps/api/src/services/channel_hub.py)", "apps/api/src/services/telegram_alert_ai_automation_matrix.py)", "apps/api/tests/test_channel_hub_grouped_alert_events.py)", @@ -214,6 +215,7 @@ def test_telegram_alert_ai_automation_matrix_stays_on_controlled_runtime_profile "scripts/security/telegram-notification-egress-no-new-bypass-guard.py)", "scripts/security/telegram-notification-egress-owner-response-acceptance.py)", "src/services/channel_hub.py", + "src/api/v1/platform/operator_runs.py", "src/services/telegram_alert_ai_automation_matrix.py", "tests/test_channel_hub_grouped_alert_events.py", "tests/test_ai_agent_report_truth_actionability_review.py", @@ -825,6 +827,7 @@ def test_post_start_recovery_verifiers_stay_on_controlled_runtime_profile() -> N "docs/operations/post-reboot-runtime-recovery-readback-2026-07-01.snapshot.json)", "docs/workplans/2026-06-04-reboot-cold-start-backup-recovery-workplan.md)", "docs/workplans/2026-07-02-commander-inserted-requirements-priority-ledger.md)", + "apps/api/src/api/v1/platform/operator_runs.py)", "ops/monitoring/alerts-unified.yml)", "ops/monitoring/alerts.yml)", "ops/reboot-recovery/full-stack-cold-start-baseline.yml)", @@ -841,6 +844,7 @@ def test_post_start_recovery_verifiers_stay_on_controlled_runtime_profile() -> N "scripts/ops/host-runaway-process-exporter.py)", "scripts/ops/host-sustained-load-evidence.py)", "scripts/ops/tests/test_gitea_queue_hook_backlog_playbook.py)", + "src/api/v1/platform/operator_runs.py", "scripts/reboot-recovery/deploy-to-110.sh)", "scripts/reboot-recovery/enforce-110-runner-failclosed.sh)", "scripts/reboot-recovery/recover-110-control-path-and-harbor-local.sh)",