fix(api): 補修復候選 coverage gap 契約
This commit is contained in:
@@ -478,6 +478,15 @@ class RepairCandidateService:
|
||||
"approve_no_action_as_repair",
|
||||
"generic_fallback_repair",
|
||||
]
|
||||
coverage_gap = self._build_coverage_gap(
|
||||
blockers=blockers,
|
||||
lane=lane,
|
||||
alertname=alertname,
|
||||
target_resource=target_resource,
|
||||
namespace=namespace,
|
||||
evidence=evidence,
|
||||
playbook=playbook,
|
||||
)
|
||||
incident_id = getattr(incident, "incident_id", "") if incident else ""
|
||||
project_id = str(getattr(incident, "project_id", "awoooi") or "awoooi")
|
||||
work_item: dict[str, Any] | None = None
|
||||
@@ -507,6 +516,7 @@ class RepairCandidateService:
|
||||
"reason": ",".join(blockers),
|
||||
"next_step": next_step,
|
||||
"required_fields": required_fields,
|
||||
"coverage_gap": coverage_gap,
|
||||
"blocked_operations": blocked_operations,
|
||||
"target_href": f"/awooop/runs?{run_query}",
|
||||
"work_item_href": f"/awooop/work-items?{work_item_query}",
|
||||
@@ -531,12 +541,110 @@ class RepairCandidateService:
|
||||
"matched_playbook_name": playbook.name if playbook else None,
|
||||
"evidence_snapshot_id": evidence_ref,
|
||||
"required_fields": required_fields,
|
||||
"coverage_gap": coverage_gap,
|
||||
"blocked_operations": blocked_operations,
|
||||
}
|
||||
if work_item:
|
||||
package["awooop_work_item"] = work_item
|
||||
return package
|
||||
|
||||
def _build_coverage_gap(
|
||||
self,
|
||||
*,
|
||||
blockers: list[str],
|
||||
lane: str,
|
||||
alertname: str,
|
||||
target_resource: str,
|
||||
namespace: str,
|
||||
evidence: EvidenceSnapshot | None,
|
||||
playbook: Playbook | None,
|
||||
) -> dict[str, Any]:
|
||||
"""Describe why this alert is not yet covered by an executable PlayBook."""
|
||||
|
||||
blocker_set = set(blockers)
|
||||
coverage_key = self._coverage_key(alertname=alertname, target_resource=target_resource)
|
||||
target_kind = self._infer_target_kind(target_resource=target_resource, namespace=namespace)
|
||||
if "mcp_evidence_missing" in blocker_set:
|
||||
blocking_stage = "mcp_evidence"
|
||||
elif {
|
||||
"playbook_not_matched",
|
||||
"playbook_not_found",
|
||||
"playbook_generic_fallback_not_repair",
|
||||
"playbook_observe_only",
|
||||
"playbook_has_no_repair_steps",
|
||||
"playbook_has_no_executable_step",
|
||||
} & blocker_set:
|
||||
blocking_stage = "service_playbook_coverage"
|
||||
elif {"playbook_not_approved", "playbook_trust_below_gate"} & blocker_set:
|
||||
blocking_stage = "playbook_trust_gate"
|
||||
elif "playbook_command_not_safely_routable" in blocker_set:
|
||||
blocking_stage = "safe_execution_route"
|
||||
else:
|
||||
blocking_stage = "owner_review"
|
||||
|
||||
evidence_refs = [
|
||||
"alertmanager_event",
|
||||
"incident_timeline",
|
||||
"mcp_health_snapshot",
|
||||
"k8s_or_host_status",
|
||||
"metrics_or_logs_window",
|
||||
"recurrence_fingerprint",
|
||||
]
|
||||
if target_kind == "k8s_workload":
|
||||
evidence_refs.extend(["k8s_events", "rollout_status"])
|
||||
elif target_kind == "host_service":
|
||||
evidence_refs.extend(["systemd_or_container_status", "host_resource_window"])
|
||||
|
||||
return {
|
||||
"schema_version": "repair_candidate_coverage_gap_v1",
|
||||
"coverage_key": coverage_key,
|
||||
"target_kind": target_kind,
|
||||
"blocking_stage": blocking_stage,
|
||||
"next_owner_lane": lane,
|
||||
"alertname": alertname or None,
|
||||
"namespace": namespace or None,
|
||||
"target_resource": target_resource or None,
|
||||
"matched_playbook_id": playbook.playbook_id if playbook else None,
|
||||
"evidence_snapshot_id": evidence.snapshot_id if evidence else None,
|
||||
"mcp_evidence_ready": bool(evidence and evidence.sensors_succeeded > 0),
|
||||
"required_mcp_evidence_refs": list(dict.fromkeys(evidence_refs)),
|
||||
"playbook_template_required": True,
|
||||
"playbook_template_fields": [
|
||||
"symptom_pattern.alert_names",
|
||||
"symptom_pattern.affected_services",
|
||||
"mcp_evidence_refs",
|
||||
"repair_steps.command_or_ansible_ref",
|
||||
"repair_steps.rollback_command",
|
||||
"verifier_plan",
|
||||
"owner_review_record",
|
||||
"trust_score_update_policy",
|
||||
],
|
||||
"blocked_operations": [
|
||||
"auto_execute",
|
||||
"mark_repaired_without_execution",
|
||||
"approve_no_action_as_repair",
|
||||
],
|
||||
"runtime_execution_authorized": False,
|
||||
"writes_runtime_state": False,
|
||||
}
|
||||
|
||||
def _coverage_key(self, *, alertname: str, target_resource: str) -> str:
|
||||
raw_alert = (alertname or "unknown-alert").strip().lower()
|
||||
raw_target = (target_resource or "unknown-target").strip().lower()
|
||||
safe_alert = "-".join(raw_alert.replace("_", "-").split())
|
||||
safe_target = "-".join(raw_target.replace("_", "-").split())
|
||||
return f"{safe_alert}:{safe_target}"
|
||||
|
||||
def _infer_target_kind(self, *, target_resource: str, namespace: str) -> str:
|
||||
target = (target_resource or "").lower()
|
||||
if any(marker in target for marker in ("node-exporter", "host", "188", "111", "168")):
|
||||
return "host_service"
|
||||
if namespace or any(marker in target for marker in ("deployment/", "pod/", "svc/", "api")):
|
||||
return "k8s_workload"
|
||||
if target:
|
||||
return "service"
|
||||
return "unknown"
|
||||
|
||||
def _build_description(
|
||||
self,
|
||||
*,
|
||||
|
||||
Reference in New Issue
Block a user