Files
awoooi/apps/api/src/services/drift_fingerprint_state_service.py
Your Name 64b34828a7
All checks were successful
Code Review / ai-code-review (push) Successful in 11s
CD Pipeline / tests (push) Successful in 1m10s
CD Pipeline / build-and-deploy (push) Successful in 3m42s
CD Pipeline / post-deploy-checks (push) Successful in 1m18s
feat(drift): record remediation evidence
2026-05-19 09:13:58 +08:00

959 lines
34 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Config Drift fingerprint FSM read model.
The drift scanner creates a new report_id on every scan, so the operator-facing
state must be keyed by the stable drift fingerprint instead of the volatile
report id. This service is intentionally conservative: it can record human
handoff and remediation breadcrumbs, but it does not adopt, merge, roll back,
or mutate incident state.
"""
from __future__ import annotations
from datetime import datetime
from typing import Any, Literal
import httpx
import structlog
from sqlalchemy import text
from src.core.config import get_settings
from src.db.base import get_db_context
from src.models.drift import DriftReport
from src.repositories.drift_repository import get_drift_repository
from src.services.drift_repeat_state import build_drift_fingerprint
from src.utils.timezone import now_taipei
logger = structlog.get_logger(__name__)
SCHEMA_VERSION = "drift_fingerprint_state_v1"
HANDOFF_SCHEMA_VERSION = "drift_fingerprint_handoff_history_v1"
REMEDIATION_SCHEMA_VERSION = "drift_fingerprint_remediation_history_v1"
SERVICE_ACTOR = "drift_fingerprint_state_service"
DEFAULT_NAMESPACE = "awoooi-prod"
DriftFingerprintHandoffKind = Literal[
"open_pr_review",
"manual_investigation",
"zero_diff_pr_cleanup",
]
DriftFingerprintRemediationKind = Literal[
"live_env_rollback",
"git_adopted",
"git_rollback",
"zero_diff_pr_cleanup",
"manual_noop",
]
DriftFingerprintRemediationStatus = Literal[
"executed_unverified",
"verified_no_drift",
"verification_failed",
]
class DriftFingerprintStateNotFoundError(Exception):
"""Raised when no drift report can be found for the requested selector."""
def _enum_value(value: Any) -> Any:
return getattr(value, "value", value)
def _iso(value: Any) -> str | None:
if value is None:
return None
if isinstance(value, datetime):
return value.isoformat()
return str(value)
def _report_summary(report: DriftReport) -> str:
return (
f"HIGH×{report.high_count}, "
f"MEDIUM×{report.medium_count}, "
f"INFO×{report.info_count}"
)
def _interpretation_summary(report: DriftReport) -> dict[str, Any] | None:
if report.interpretation is None:
return None
return {
"intent": _enum_value(report.interpretation.intent),
"risk": report.interpretation.risk,
"confidence": report.interpretation.confidence,
"explanation": report.interpretation.explanation,
}
def _pr_refs(pr: dict[str, Any] | None) -> list[str]:
if not pr:
return []
refs = [pr.get("html_url"), pr.get("url")]
number = pr.get("number")
if number is not None:
refs.append(f"pull/{number}")
return [str(ref) for ref in refs if ref]
def _derive_fsm_state(
report: DriftReport,
repeat_state: dict[str, Any],
open_pr: dict[str, Any] | None,
latest_handoff: dict[str, Any] | None,
latest_remediation: dict[str, Any] | None,
) -> str:
status = str(_enum_value(report.status))
remediation_status = (latest_remediation or {}).get("remediation_status")
if remediation_status == "verified_no_drift":
if report.high_count == 0 and report.medium_count == 0 and report.info_count == 0:
return "no_drift_verified"
return "remediated_verified"
if remediation_status == "executed_unverified":
return "remediation_executed_unverified"
if remediation_status == "verification_failed":
return "remediation_verification_failed"
if status == "adopted":
return "adopted_unverified"
if status == "rolled_back":
return "rolled_back"
if status in {"acknowledged", "ignored"}:
return status
if open_pr:
if open_pr.get("merged"):
return "pr_merged_unverified"
if open_pr.get("state") == "open" and open_pr.get("is_zero_diff"):
return "pr_open_zero_diff"
if open_pr.get("state") == "open":
return "pr_open_waiting_review"
if latest_handoff:
return "handoff_recorded"
if int(repeat_state.get("occurrences_12h") or 0) > 1:
return "pending_human_repeated"
return "pending_human"
def _next_step_for_state(state: str, open_pr: dict[str, Any] | None) -> str:
if state in {"no_drift_verified", "remediated_verified"}:
return "monitor_for_recurrence"
if state == "remediation_executed_unverified":
return "run_verification_scan_then_record_result"
if state == "remediation_verification_failed":
return "open_manual_investigation_with_failed_verification"
if state == "pr_open_zero_diff":
return "close_zero_diff_pr_and_prepare_real_yaml_patch"
if state == "pr_open_waiting_review":
return "review_pr_then_merge_or_reject"
if state == "pr_merged_unverified":
return "verify_git_baseline_then_mark_adopted"
if state == "handoff_recorded":
return "operator_review_handoff_and_execute_manual_plan"
if state == "adopted_unverified":
return "verify_k8s_matches_git_baseline"
if state == "rolled_back":
return "confirm_no_repeat_after_rollback"
if state in {"acknowledged", "ignored"}:
return "monitor_for_recurrence"
if open_pr and open_pr.get("lookup_error"):
return "retry_pr_lookup_then_review_drift"
return "manual_investigation_or_ansible_check_mode"
def build_drift_fingerprint_state(
report: DriftReport,
repeat_state: dict[str, Any],
*,
open_pr: dict[str, Any] | None = None,
latest_handoff: dict[str, Any] | None = None,
latest_remediation: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Build the operator-facing Config Drift fingerprint state payload."""
fingerprint = repeat_state.get("fingerprint") or build_drift_fingerprint(
report.namespace,
report.items,
)
fsm_state = _derive_fsm_state(
report,
repeat_state,
open_pr,
latest_handoff,
latest_remediation,
)
next_step = _next_step_for_state(fsm_state, open_pr)
return {
"schema_version": SCHEMA_VERSION,
"namespace": report.namespace,
"fingerprint": fingerprint,
"latest_report_id": report.report_id,
"latest_status": str(_enum_value(report.status)),
"latest_scanned_at": _iso(report.scanned_at),
"latest_created_at": _iso(report.created_at),
"summary": _report_summary(report),
"high_count": report.high_count,
"medium_count": report.medium_count,
"info_count": report.info_count,
"interpretation": _interpretation_summary(report),
"repeat_state": repeat_state,
"occurrences_12h": repeat_state.get("occurrences_12h", 0),
"matching_strategy": repeat_state.get("matching_strategy"),
"operator_stage": fsm_state,
"fsm_state": fsm_state,
"next_step": next_step,
"open_pr": open_pr,
"latest_handoff": latest_handoff,
"latest_remediation": latest_remediation,
"strict_fingerprint": repeat_state.get("strict_fingerprint"),
"p0_escalation": {
"suppresses_repeated_p0": True,
"dedup_key_strategy": "semantic_drift_fingerprint",
"dedup_window_hours": 24,
},
"read_model_route": {
"agent_id": "openclaw",
"tool_name": "drift_fingerprint_state",
"required_scope": "read:drift read:gitea",
"flywheel_node": (
"drift_scanned>ai_analyzed>fingerprint_fsm>"
"operator_review"
),
},
"writes_incident_state": False,
"writes_auto_repair_result": False,
"writes_drift_status": False,
"writes_ticket": False,
"writes_remediation_record": False,
"creates_external_ticket": False,
}
def _handoff_context(
state: dict[str, Any],
*,
handoff_kind: DriftFingerprintHandoffKind,
pr_url: str | None,
note: str | None,
) -> dict[str, Any]:
return {
"schema_version": HANDOFF_SCHEMA_VERSION,
"fingerprint": state.get("fingerprint"),
"namespace": state.get("namespace"),
"latest_report_id": state.get("latest_report_id"),
"handoff_kind": handoff_kind,
"handoff_status": "recorded",
"pr_url": pr_url,
"note": note,
"fsm_state": state.get("fsm_state"),
"next_step": state.get("next_step"),
"open_pr": state.get("open_pr"),
"writes_incident_state": False,
"writes_auto_repair_result": False,
"writes_drift_status": False,
"writes_ticket": False,
"creates_external_ticket": False,
"recorded_at": now_taipei().isoformat(),
}
def _handoff_description(context: dict[str, Any]) -> str:
return (
f"fingerprint={context.get('fingerprint')} "
f"state={context.get('fsm_state')} "
f"kind={context.get('handoff_kind')} "
f"next={context.get('next_step')} "
f"pr={context.get('pr_url') or '--'}"
)[:500]
def _is_no_drift_report(report: DriftReport | None) -> bool:
if report is None:
return False
return report.high_count == 0 and report.medium_count == 0 and report.info_count == 0
def _remediation_context(
state: dict[str, Any],
*,
remediation_kind: DriftFingerprintRemediationKind,
remediation_status: DriftFingerprintRemediationStatus,
verification_report: DriftReport | None,
note: str | None,
commands_summary: list[str] | None,
) -> dict[str, Any]:
verification_summary = None
if verification_report is not None:
verification_summary = {
"report_id": verification_report.report_id,
"status": str(_enum_value(verification_report.status)),
"summary": _report_summary(verification_report),
"high_count": verification_report.high_count,
"medium_count": verification_report.medium_count,
"info_count": verification_report.info_count,
"scanned_at": _iso(verification_report.scanned_at),
"is_no_drift": _is_no_drift_report(verification_report),
}
return {
"schema_version": REMEDIATION_SCHEMA_VERSION,
"fingerprint": state.get("fingerprint"),
"namespace": state.get("namespace"),
"remediated_report_id": state.get("latest_report_id"),
"verification_report_id": getattr(verification_report, "report_id", None),
"verification_summary": verification_summary,
"remediation_kind": remediation_kind,
"remediation_status": remediation_status,
"note": note,
"commands_summary": [
str(command)[:180] for command in (commands_summary or [])[:12]
],
"fsm_state_before": state.get("fsm_state"),
"next_step_before": state.get("next_step"),
"writes_incident_state": False,
"writes_auto_repair_result": False,
"writes_drift_status": False,
"writes_ticket": False,
"writes_remediation_record": True,
"creates_external_ticket": False,
"recorded_at": now_taipei().isoformat(),
}
def _remediation_description(context: dict[str, Any]) -> str:
verification = context.get("verification_summary") or {}
return (
f"fingerprint={context.get('fingerprint')} "
f"kind={context.get('remediation_kind')} "
f"status={context.get('remediation_status')} "
f"remediated_report={context.get('remediated_report_id')} "
f"verification_report={context.get('verification_report_id') or '--'} "
f"verification={verification.get('summary') or '--'}"
)[:500]
class DriftFingerprintStateService:
"""Read and record the state of a stable Config Drift fingerprint."""
async def get_state(
self,
*,
report_id: str | None = None,
namespace: str | None = None,
) -> dict[str, Any]:
report = await self._load_report(report_id=report_id, namespace=namespace)
repo = get_drift_repository()
repeat_state = await repo.get_repeat_state(report, include_values=False)
fingerprint = repeat_state["fingerprint"]
open_pr = await self._lookup_open_pr(report)
latest_handoff = await self._fetch_latest_handoff(
fingerprint,
alternate_fingerprints=_repeat_state_fingerprint_aliases(repeat_state),
report_ids=_repeat_state_report_ids(repeat_state),
)
latest_remediation = await self._fetch_latest_remediation(
fingerprint,
alternate_fingerprints=_repeat_state_fingerprint_aliases(repeat_state),
report_ids={
report.report_id,
*_repeat_state_report_ids(repeat_state),
},
namespace=report.namespace,
allow_namespace_fallback=_is_no_drift_report(report),
)
return build_drift_fingerprint_state(
report,
repeat_state,
open_pr=open_pr,
latest_handoff=latest_handoff,
latest_remediation=latest_remediation,
)
async def record_handoff(
self,
*,
report_id: str | None = None,
namespace: str | None = None,
handoff_kind: DriftFingerprintHandoffKind = "open_pr_review",
pr_url: str | None = None,
note: str | None = None,
) -> dict[str, Any]:
state = await self.get_state(report_id=report_id, namespace=namespace)
context = _handoff_context(
state,
handoff_kind=handoff_kind,
pr_url=pr_url or _default_pr_url(state.get("open_pr")),
note=note,
)
history = {
"recorded": False,
"alert_operation_id": None,
"timeline_event_id": None,
"reason": None,
}
incident_id = str(state.get("latest_report_id") or "")
try:
from src.repositories.alert_operation_log_repository import (
get_alert_operation_log_repository,
)
record = await get_alert_operation_log_repository().append(
"ESCALATED",
incident_id=incident_id or None,
actor=SERVICE_ACTOR,
action_detail=f"drift_fingerprint_handoff:{handoff_kind}"[:200],
success=True,
context=context,
)
if record is not None:
history["alert_operation_id"] = getattr(record, "id", None)
except Exception as exc:
logger.warning(
"drift_fingerprint_handoff_aol_failed",
report_id=incident_id,
error=str(exc),
)
try:
from src.services.approval_db import get_timeline_service
event = await get_timeline_service().add_event(
event_type="human",
status="warning",
title="AwoooP drift fingerprint handoff",
description=_handoff_description(context),
actor=SERVICE_ACTOR,
actor_role=handoff_kind,
incident_id=incident_id or None,
)
if event:
history["timeline_event_id"] = event.get("id")
except Exception as exc:
logger.warning(
"drift_fingerprint_handoff_timeline_failed",
report_id=incident_id,
error=str(exc),
)
history["recorded"] = bool(
history.get("alert_operation_id") or history.get("timeline_event_id")
)
if not history["recorded"]:
history["reason"] = "history_sink_unavailable"
updated_state = {
**state,
"latest_handoff": {
"handoff_kind": handoff_kind,
"handoff_status": "recorded" if history["recorded"] else "record_failed",
"pr_url": context.get("pr_url"),
"note": note,
"created_at": context.get("recorded_at"),
"source": "current_request",
},
}
updated_state["operator_stage"] = (
"handoff_recorded" if history["recorded"] else state["operator_stage"]
)
updated_state["fsm_state"] = updated_state["operator_stage"]
updated_state["next_step"] = _next_step_for_state(
updated_state["fsm_state"],
state.get("open_pr"),
)
return {
**updated_state,
"handoff_kind": handoff_kind,
"handoff_status": updated_state["latest_handoff"]["handoff_status"],
"history": history,
}
async def record_remediation(
self,
*,
report_id: str | None = None,
namespace: str | None = None,
remediation_kind: DriftFingerprintRemediationKind = "live_env_rollback",
remediation_status: DriftFingerprintRemediationStatus | None = None,
verification_report_id: str | None = None,
note: str | None = None,
commands_summary: list[str] | None = None,
) -> dict[str, Any]:
state = await self.get_state(report_id=report_id, namespace=namespace)
verification_report = await self._load_verification_report(
report_id=verification_report_id,
namespace=state.get("namespace") or namespace,
)
resolved_status: DriftFingerprintRemediationStatus = (
remediation_status
or (
"verified_no_drift"
if _is_no_drift_report(verification_report)
else "executed_unverified"
)
)
context = _remediation_context(
state,
remediation_kind=remediation_kind,
remediation_status=resolved_status,
verification_report=verification_report,
note=note,
commands_summary=commands_summary,
)
history = {
"recorded": False,
"alert_operation_id": None,
"timeline_event_id": None,
"reason": None,
}
incident_id = str(state.get("latest_report_id") or "")
success = resolved_status != "verification_failed"
try:
from src.repositories.alert_operation_log_repository import (
get_alert_operation_log_repository,
)
record = await get_alert_operation_log_repository().append(
"CHANGE_APPLIED",
incident_id=incident_id or None,
actor=SERVICE_ACTOR,
action_detail=(
f"drift_fingerprint_remediation:{remediation_kind}"
)[:200],
success=success,
context=context,
)
if record is not None:
history["alert_operation_id"] = getattr(record, "id", None)
except Exception as exc:
logger.warning(
"drift_fingerprint_remediation_aol_failed",
report_id=incident_id,
error=str(exc),
)
try:
from src.services.approval_db import get_timeline_service
event = await get_timeline_service().add_event(
event_type="exec",
status="success" if success else "error",
title="AwoooP drift remediation verified",
description=_remediation_description(context),
actor=SERVICE_ACTOR,
actor_role=remediation_kind,
incident_id=incident_id or None,
)
if event:
history["timeline_event_id"] = event.get("id")
except Exception as exc:
logger.warning(
"drift_fingerprint_remediation_timeline_failed",
report_id=incident_id,
error=str(exc),
)
history["recorded"] = bool(
history.get("alert_operation_id") or history.get("timeline_event_id")
)
if not history["recorded"]:
history["reason"] = "history_sink_unavailable"
latest_remediation = {
"remediation_kind": remediation_kind,
"remediation_status": (
resolved_status if history["recorded"] else "record_failed"
),
"verification_report_id": context.get("verification_report_id"),
"verification_summary": context.get("verification_summary"),
"note": note,
"commands_summary": context.get("commands_summary"),
"created_at": context.get("recorded_at"),
"source": "current_request",
}
updated_state = build_drift_fingerprint_state(
await self._load_report(
report_id=str(state.get("latest_report_id") or ""),
namespace=state.get("namespace"),
),
{
**(state.get("repeat_state") or {}),
"fingerprint": state.get("fingerprint"),
},
open_pr=state.get("open_pr"),
latest_handoff=state.get("latest_handoff"),
latest_remediation=latest_remediation,
)
return {
**updated_state,
"remediation_kind": remediation_kind,
"remediation_status": latest_remediation["remediation_status"],
"history": history,
}
async def _load_report(
self,
*,
report_id: str | None,
namespace: str | None,
) -> DriftReport:
repo = get_drift_repository()
if report_id:
report = await repo.get(report_id)
if report is None:
raise DriftFingerprintStateNotFoundError(report_id)
return report
desired_namespace = namespace or DEFAULT_NAMESPACE
for report in await repo.list_recent(limit=50):
if report.namespace == desired_namespace:
return report
raise DriftFingerprintStateNotFoundError(desired_namespace)
async def _load_verification_report(
self,
*,
report_id: str | None,
namespace: str | None,
) -> DriftReport | None:
repo = get_drift_repository()
if report_id:
return await repo.get(report_id)
desired_namespace = namespace or DEFAULT_NAMESPACE
for report in await repo.list_recent(limit=50):
if report.namespace == desired_namespace and _is_no_drift_report(report):
return report
return None
async def _fetch_latest_handoff(
self,
fingerprint: str,
*,
alternate_fingerprints: set[str] | None = None,
report_ids: set[str] | None = None,
) -> dict[str, Any] | None:
fingerprints = {fingerprint, *(alternate_fingerprints or set())}
fingerprints = {value for value in fingerprints if value}
report_ids = {value for value in (report_ids or set()) if value}
try:
async with get_db_context() as db:
result = await db.execute(
text(
"""
SELECT id, action_detail, success, context, created_at
FROM alert_operation_log
WHERE actor = :actor
AND action_detail LIKE 'drift_fingerprint_handoff:%'
ORDER BY created_at DESC
LIMIT 100
"""
),
{"actor": SERVICE_ACTOR},
)
rows = result.mappings().all()
except Exception as exc:
logger.warning(
"drift_fingerprint_handoff_lookup_failed",
fingerprint=fingerprint,
error=str(exc),
)
return {
"lookup_error": str(exc)[:160],
"handoff_status": "lookup_failed",
}
row = None
for candidate in rows:
context = candidate.get("context") or {}
if not isinstance(context, dict):
continue
if context.get("fingerprint") in fingerprints:
row = candidate
break
if context.get("latest_report_id") in report_ids:
row = candidate
break
if row is None:
return None
context = row.get("context") or {}
return {
"alert_operation_id": row.get("id"),
"action_detail": row.get("action_detail"),
"success": row.get("success"),
"created_at": _iso(row.get("created_at")),
"handoff_kind": context.get("handoff_kind"),
"handoff_status": context.get("handoff_status") or "recorded",
"pr_url": context.get("pr_url"),
"note": context.get("note"),
}
async def _fetch_latest_remediation(
self,
fingerprint: str,
*,
alternate_fingerprints: set[str] | None = None,
report_ids: set[str] | None = None,
namespace: str | None = None,
allow_namespace_fallback: bool = False,
) -> dict[str, Any] | None:
fingerprints = {fingerprint, *(alternate_fingerprints or set())}
fingerprints = {value for value in fingerprints if value}
report_ids = {value for value in (report_ids or set()) if value}
try:
async with get_db_context() as db:
result = await db.execute(
text(
"""
SELECT id, action_detail, success, context, created_at
FROM alert_operation_log
WHERE actor = :actor
AND action_detail LIKE 'drift_fingerprint_remediation:%'
ORDER BY created_at DESC
LIMIT 100
"""
),
{"actor": SERVICE_ACTOR},
)
rows = result.mappings().all()
except Exception as exc:
logger.warning(
"drift_fingerprint_remediation_lookup_failed",
fingerprint=fingerprint,
error=str(exc),
)
return {
"lookup_error": str(exc)[:160],
"remediation_status": "lookup_failed",
}
namespace_fallback = None
row = None
for candidate in rows:
context = candidate.get("context") or {}
if not isinstance(context, dict):
continue
if context.get("fingerprint") in fingerprints:
row = candidate
break
if context.get("remediated_report_id") in report_ids:
row = candidate
break
if context.get("verification_report_id") in report_ids:
row = candidate
break
if (
allow_namespace_fallback
and namespace
and context.get("namespace") == namespace
and namespace_fallback is None
):
namespace_fallback = candidate
row = row or namespace_fallback
if row is None:
return None
context = row.get("context") or {}
return {
"alert_operation_id": row.get("id"),
"action_detail": row.get("action_detail"),
"success": row.get("success"),
"created_at": _iso(row.get("created_at")),
"remediation_kind": context.get("remediation_kind"),
"remediation_status": context.get("remediation_status") or "recorded",
"remediated_report_id": context.get("remediated_report_id"),
"verification_report_id": context.get("verification_report_id"),
"verification_summary": context.get("verification_summary"),
"note": context.get("note"),
"commands_summary": context.get("commands_summary"),
}
async def _lookup_open_pr(self, report: DriftReport) -> dict[str, Any] | None:
settings = get_settings()
api_url = settings.GITEA_API_URL.rstrip("/")
if not api_url:
return None
headers = {"Accept": "application/json"}
if settings.GITEA_API_TOKEN:
headers["Authorization"] = f"token {settings.GITEA_API_TOKEN}"
try:
async with httpx.AsyncClient(timeout=5.0) as client:
pulls = await client.get(
(
f"{api_url}/api/v1/repos/"
f"{settings.GITEA_REPO_OWNER}/{settings.GITEA_REPO_NAME}/pulls"
),
params={"state": "open", "limit": 20},
headers=headers,
)
if pulls.status_code != 200:
return {
"lookup_status": "failed",
"lookup_error": f"pulls_http_{pulls.status_code}",
}
for pr in pulls.json() or []:
if not _matches_drift_pr(report, pr):
continue
return await _build_pr_state(
client=client,
api_url=api_url,
owner=settings.GITEA_REPO_OWNER,
repo=settings.GITEA_REPO_NAME,
headers=headers,
pr=pr,
)
except Exception as exc:
logger.warning(
"drift_fingerprint_pr_lookup_failed",
report_id=report.report_id,
error=str(exc),
)
return {
"lookup_status": "failed",
"lookup_error": str(exc)[:160],
}
return None
def _default_pr_url(open_pr: dict[str, Any] | None) -> str | None:
if not open_pr:
return None
return open_pr.get("html_url") or open_pr.get("url")
def _repeat_state_fingerprint_aliases(repeat_state: dict[str, Any]) -> set[str]:
aliases = {str(repeat_state.get("strict_fingerprint") or "")}
for report in repeat_state.get("reports") or []:
if isinstance(report, dict):
aliases.add(str(report.get("strict_fingerprint") or ""))
return {value for value in aliases if value}
def _repeat_state_report_ids(repeat_state: dict[str, Any]) -> set[str]:
report_ids = set()
for report in repeat_state.get("reports") or []:
if isinstance(report, dict):
report_ids.add(str(report.get("report_id") or ""))
return {value for value in report_ids if value}
def _matches_drift_pr(report: DriftReport, pr: dict[str, Any]) -> bool:
text_blob = "\n".join(
str(value or "")
for value in (pr.get("title"), pr.get("body"), pr.get("html_url"))
)
if report.report_id and report.report_id in text_blob:
return True
if report.namespace not in text_blob:
return False
expected_parts = [
f"HIGH×{report.high_count}",
f"MEDIUM×{report.medium_count}",
f"INFO×{report.info_count}",
]
return any(part in text_blob for part in expected_parts)
def _nested(mapping: dict[str, Any], *keys: str) -> Any:
current: Any = mapping
for key in keys:
if not isinstance(current, dict):
return None
current = current.get(key)
return current
async def _response_list_count(
client: httpx.AsyncClient,
url: str,
*,
headers: dict[str, str],
) -> tuple[int | None, str | None]:
try:
response = await client.get(url, headers=headers)
if response.status_code != 200:
return None, f"http_{response.status_code}"
payload = response.json()
if isinstance(payload, list):
return len(payload), None
if isinstance(payload, dict):
for key in ("items", "files", "commits"):
if isinstance(payload.get(key), list):
return len(payload[key]), None
return 0, None
except Exception as exc:
return None, str(exc)[:160]
async def _build_pr_state(
*,
client: httpx.AsyncClient,
api_url: str,
owner: str,
repo: str,
headers: dict[str, str],
pr: dict[str, Any],
) -> dict[str, Any]:
number = pr.get("number") or pr.get("index")
base_url = f"{api_url}/api/v1/repos/{owner}/{repo}/pulls/{number}"
files_count, files_error = await _response_list_count(
client,
f"{base_url}/files",
headers=headers,
)
commits_count, commits_error = await _response_list_count(
client,
f"{base_url}/commits",
headers=headers,
)
head_sha = _nested(pr, "head", "sha") or _nested(pr, "head", "ref")
base_sha = _nested(pr, "base", "sha") or _nested(pr, "base", "ref")
is_zero_diff = (
(files_count == 0 and commits_count == 0)
or (bool(head_sha) and head_sha == base_sha)
)
return {
"lookup_status": "ok",
"number": number,
"title": pr.get("title"),
"state": pr.get("state"),
"merged": bool(pr.get("merged")),
"mergeable": pr.get("mergeable"),
"html_url": pr.get("html_url"),
"url": pr.get("url"),
"head_ref": _nested(pr, "head", "ref"),
"base_ref": _nested(pr, "base", "ref"),
"head_sha": head_sha,
"base_sha": base_sha,
"file_count": files_count,
"commit_count": commits_count,
"files_lookup_error": files_error,
"commits_lookup_error": commits_error,
"is_zero_diff": is_zero_diff,
"refs": _pr_refs(pr),
}
_drift_fingerprint_state_service: DriftFingerprintStateService | None = None
def get_drift_fingerprint_state_service() -> DriftFingerprintStateService:
global _drift_fingerprint_state_service
if _drift_fingerprint_state_service is None:
_drift_fingerprint_state_service = DriftFingerprintStateService()
return _drift_fingerprint_state_service