feat(awooop): surface execution backend evidence
This commit is contained in:
@@ -620,6 +620,91 @@ def _automation_quality_score_bucket(score: int) -> str:
|
||||
return "red"
|
||||
|
||||
|
||||
def _int_value(value: Any, fallback: int = 0) -> int:
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return fallback
|
||||
|
||||
|
||||
def _execution_backend_summary(records: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
summary = {
|
||||
"operation_records_total": 0,
|
||||
"effective_execution_records_total": 0,
|
||||
"noop_operation_records_total": 0,
|
||||
"audit_only_operation_records_total": 0,
|
||||
"auto_repair_execution_records_total": 0,
|
||||
"ansible_considered_total": 0,
|
||||
"ansible_audit_record_total": 0,
|
||||
"ansible_candidate_total": 0,
|
||||
"ansible_check_mode_total": 0,
|
||||
"ansible_apply_total": 0,
|
||||
"ansible_rollback_total": 0,
|
||||
"ansible_pending_check_mode_total": 0,
|
||||
}
|
||||
|
||||
for record in records:
|
||||
quality = record.get("automation_quality") if isinstance(record.get("automation_quality"), dict) else {}
|
||||
facts = quality.get("facts") if isinstance(quality.get("facts"), dict) else {}
|
||||
execution = record.get("execution") if isinstance(record.get("execution"), dict) else {}
|
||||
ops = (
|
||||
execution.get("automation_operation_log")
|
||||
if isinstance(execution.get("automation_operation_log"), list)
|
||||
else []
|
||||
)
|
||||
auto_repair_executions = (
|
||||
execution.get("auto_repair_executions")
|
||||
if isinstance(execution.get("auto_repair_executions"), list)
|
||||
else []
|
||||
)
|
||||
ansible = execution.get("ansible") if isinstance(execution.get("ansible"), dict) else {}
|
||||
ansible_records = ansible.get("records") if isinstance(ansible.get("records"), list) else []
|
||||
catalog = (
|
||||
ansible.get("candidate_catalog")
|
||||
if isinstance(ansible.get("candidate_catalog"), dict)
|
||||
else {}
|
||||
)
|
||||
candidates = catalog.get("candidates") if isinstance(catalog.get("candidates"), list) else []
|
||||
|
||||
summary["operation_records_total"] += _int_value(facts.get("automation_operation_records"), len(ops))
|
||||
summary["effective_execution_records_total"] += _int_value(
|
||||
facts.get("effective_execution_records"),
|
||||
len(_effective_execution_ops([row for row in ops if isinstance(row, dict)])),
|
||||
)
|
||||
summary["noop_operation_records_total"] += _int_value(
|
||||
facts.get("noop_operation_records"),
|
||||
sum(1 for row in ops if isinstance(row, dict) and _is_no_action_operation(row)),
|
||||
)
|
||||
summary["audit_only_operation_records_total"] += _int_value(
|
||||
facts.get("audit_only_operation_records"),
|
||||
sum(1 for row in ops if isinstance(row, dict) and _is_audit_only_operation(row)),
|
||||
)
|
||||
summary["auto_repair_execution_records_total"] += _int_value(
|
||||
facts.get("auto_repair_execution_records"),
|
||||
len(auto_repair_executions),
|
||||
)
|
||||
|
||||
if ansible.get("considered") is True:
|
||||
summary["ansible_considered_total"] += 1
|
||||
summary["ansible_audit_record_total"] += len(ansible_records)
|
||||
summary["ansible_candidate_total"] += len(candidates)
|
||||
|
||||
for row in ansible_records:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
operation_type = str(row.get("operation_type") or "")
|
||||
if operation_type == "ansible_check_mode_executed":
|
||||
summary["ansible_check_mode_total"] += 1
|
||||
elif operation_type == "ansible_apply_executed":
|
||||
summary["ansible_apply_total"] += 1
|
||||
elif operation_type == "ansible_rollback_executed":
|
||||
summary["ansible_rollback_total"] += 1
|
||||
elif operation_type == "ansible_candidate_matched":
|
||||
summary["ansible_pending_check_mode_total"] += 1
|
||||
|
||||
return summary
|
||||
|
||||
|
||||
def summarize_automation_quality_records(
|
||||
*,
|
||||
project_id: str,
|
||||
@@ -724,6 +809,7 @@ def summarize_automation_quality_records(
|
||||
"score_buckets": score_buckets,
|
||||
"by_verdict": by_verdict,
|
||||
"gate_failures": failing_gates,
|
||||
"execution_backend_summary": _execution_backend_summary(records),
|
||||
"examples": examples[:25],
|
||||
"production_claim": {
|
||||
"can_claim_full_auto_repair": evaluated_total > 0 and verified_total == evaluated_total,
|
||||
@@ -1491,6 +1577,7 @@ async def fetch_automation_quality_summary(
|
||||
"incident": truth_chain.get("incident") or incident,
|
||||
"truth_status": truth_chain.get("truth_status") or {},
|
||||
"automation_quality": truth_chain.get("automation_quality") or {},
|
||||
"execution": truth_chain.get("execution") or {},
|
||||
}
|
||||
|
||||
records = [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from types import SimpleNamespace
|
||||
|
||||
from src.services.awooop_ansible_audit_service import (
|
||||
@@ -258,7 +258,7 @@ def test_drift_fingerprint_is_stable_across_item_order() -> None:
|
||||
|
||||
|
||||
def test_drift_repeat_state_counts_matching_fingerprint_only() -> None:
|
||||
now = datetime(2026, 5, 13, 1, 0, tzinfo=timezone.utc)
|
||||
now = datetime(2026, 5, 13, 1, 0, tzinfo=UTC)
|
||||
report = {
|
||||
"report_id": "drift-now",
|
||||
"namespace": "awoooi-prod",
|
||||
@@ -303,7 +303,7 @@ def test_drift_repeat_state_counts_matching_fingerprint_only() -> None:
|
||||
|
||||
|
||||
def test_drift_repeat_state_can_group_semantic_shape_without_values() -> None:
|
||||
now = datetime(2026, 5, 19, 1, 0, tzinfo=timezone.utc)
|
||||
now = datetime(2026, 5, 19, 1, 0, tzinfo=UTC)
|
||||
report = {
|
||||
"report_id": "drift-now",
|
||||
"namespace": "awoooi-prod",
|
||||
@@ -565,11 +565,32 @@ def test_automation_quality_summary_denies_full_claim_when_unverified() -> None:
|
||||
"applicable": True,
|
||||
"verdict": "auto_repaired_verified",
|
||||
"score": 100,
|
||||
"facts": {
|
||||
"automation_operation_records": 1,
|
||||
"effective_execution_records": 1,
|
||||
"noop_operation_records": 0,
|
||||
"audit_only_operation_records": 0,
|
||||
"auto_repair_execution_records": 1,
|
||||
},
|
||||
"gates": [
|
||||
{"name": "verification_recorded", "status": "passed"},
|
||||
],
|
||||
"blockers": [],
|
||||
},
|
||||
"execution": {
|
||||
"automation_operation_log": [
|
||||
{
|
||||
"operation_type": "playbook_executed",
|
||||
"status": "success",
|
||||
}
|
||||
],
|
||||
"auto_repair_executions": [{"success": True}],
|
||||
"ansible": {
|
||||
"considered": False,
|
||||
"records": [],
|
||||
"candidate_catalog": {"candidates": []},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"incident": {
|
||||
@@ -588,12 +609,51 @@ def test_automation_quality_summary_denies_full_claim_when_unverified() -> None:
|
||||
"applicable": True,
|
||||
"verdict": "execution_unverified",
|
||||
"score": 65,
|
||||
"facts": {
|
||||
"automation_operation_records": 2,
|
||||
"effective_execution_records": 0,
|
||||
"noop_operation_records": 0,
|
||||
"audit_only_operation_records": 2,
|
||||
"auto_repair_execution_records": 0,
|
||||
},
|
||||
"gates": [
|
||||
{"name": "verification_recorded", "status": "missing"},
|
||||
{"name": "learning_recorded", "status": "missing"},
|
||||
],
|
||||
"blockers": ["verification_recorded", "learning_recorded"],
|
||||
},
|
||||
"execution": {
|
||||
"automation_operation_log": [
|
||||
{
|
||||
"operation_type": "ansible_candidate_matched",
|
||||
"status": "dry_run",
|
||||
},
|
||||
{
|
||||
"operation_type": "ansible_check_mode_executed",
|
||||
"status": "dry_run",
|
||||
},
|
||||
],
|
||||
"auto_repair_executions": [],
|
||||
"ansible": {
|
||||
"considered": True,
|
||||
"records": [
|
||||
{
|
||||
"operation_type": "ansible_candidate_matched",
|
||||
"status": "dry_run",
|
||||
},
|
||||
{
|
||||
"operation_type": "ansible_check_mode_executed",
|
||||
"status": "dry_run",
|
||||
},
|
||||
],
|
||||
"candidate_catalog": {
|
||||
"candidates": [
|
||||
{"catalog_id": "ansible:188-ai-web"},
|
||||
{"catalog_id": "ansible:nginx-sync"},
|
||||
]
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
)
|
||||
@@ -613,6 +673,20 @@ def test_automation_quality_summary_denies_full_claim_when_unverified() -> None:
|
||||
"learning_recorded": 1,
|
||||
"verification_recorded": 1,
|
||||
}
|
||||
assert summary["execution_backend_summary"] == {
|
||||
"operation_records_total": 3,
|
||||
"effective_execution_records_total": 1,
|
||||
"noop_operation_records_total": 0,
|
||||
"audit_only_operation_records_total": 2,
|
||||
"auto_repair_execution_records_total": 1,
|
||||
"ansible_considered_total": 1,
|
||||
"ansible_audit_record_total": 2,
|
||||
"ansible_candidate_total": 2,
|
||||
"ansible_check_mode_total": 1,
|
||||
"ansible_apply_total": 0,
|
||||
"ansible_rollback_total": 0,
|
||||
"ansible_pending_check_mode_total": 1,
|
||||
}
|
||||
assert summary["examples"][1]["incident_id"] == "INC-GAP"
|
||||
assert summary["examples"][1]["score_bucket"] == "yellow"
|
||||
|
||||
|
||||
@@ -252,6 +252,7 @@
|
||||
"autoRepair": "Auto repair",
|
||||
"qualityDetail": "Average {score}, red {red}",
|
||||
"qualityPending": "Quality summary is still calculating; other evidence is already shown",
|
||||
"executionBackendDetail": "Execution evidence: operations {operations} (effective {effective} / audit {auditOnly}), auto-repair {autoRepair}; Ansible audit {ansibleRecords}, candidates {ansibleCandidates}, check-mode {checkMode}, apply {apply}, pending wiring {pending}",
|
||||
"humanGap": "Human gap",
|
||||
"humanGapDetail": "{gate} missing {count}",
|
||||
"humanGapClear": "Quality summary has no top gap",
|
||||
|
||||
@@ -253,6 +253,7 @@
|
||||
"autoRepair": "自動修復",
|
||||
"qualityDetail": "平均 {score},紅燈 {red}",
|
||||
"qualityPending": "品質摘要計算中,其他證據已先顯示",
|
||||
"executionBackendDetail": "執行證據:操作 {operations}(有效 {effective} / 稽核 {auditOnly}),自動修復 {autoRepair};Ansible 稽核 {ansibleRecords},候選 {ansibleCandidates},check-mode {checkMode},apply {apply},待接線 {pending}",
|
||||
"humanGap": "人工缺口",
|
||||
"humanGapDetail": "{gate} 缺 {count} 筆",
|
||||
"humanGapClear": "品質摘要未列出主要缺口",
|
||||
|
||||
@@ -39,6 +39,18 @@ interface AutomationQualitySummary {
|
||||
gate: string
|
||||
total: number
|
||||
}>
|
||||
execution_backend_summary?: {
|
||||
operation_records_total?: number
|
||||
effective_execution_records_total?: number
|
||||
audit_only_operation_records_total?: number
|
||||
auto_repair_execution_records_total?: number
|
||||
ansible_considered_total?: number
|
||||
ansible_audit_record_total?: number
|
||||
ansible_candidate_total?: number
|
||||
ansible_check_mode_total?: number
|
||||
ansible_apply_total?: number
|
||||
ansible_pending_check_mode_total?: number
|
||||
}
|
||||
}
|
||||
|
||||
interface DossierCoverageResponse {
|
||||
@@ -325,6 +337,7 @@ export function AutomationEvidenceCard() {
|
||||
)
|
||||
|
||||
const topGate = quality?.gate_failures?.[0]
|
||||
const executionBackend = quality?.execution_backend_summary ?? null
|
||||
const qualityLoaded = Boolean(quality)
|
||||
const claimReady = Boolean(quality?.production_claim?.can_claim_full_auto_repair)
|
||||
const route = snapshot?.route ?? null
|
||||
@@ -360,6 +373,7 @@ export function AutomationEvidenceCard() {
|
||||
fallback,
|
||||
routeDetail,
|
||||
routeTone: routeTone(route),
|
||||
executionBackend,
|
||||
}
|
||||
}, [snapshot, t])
|
||||
|
||||
@@ -484,6 +498,38 @@ export function AutomationEvidenceCard() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{derived.executionBackend && (
|
||||
<div
|
||||
data-testid="automation-execution-backend-evidence"
|
||||
style={{
|
||||
borderTop: '0.5px solid #e0ddd4',
|
||||
background: '#fbfaf6',
|
||||
padding: '8px 14px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
fontSize: 11,
|
||||
color: '#555550',
|
||||
lineHeight: 1.45,
|
||||
}}
|
||||
>
|
||||
<Activity className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
|
||||
<span style={{ minWidth: 0 }}>
|
||||
{t('executionBackendDetail', {
|
||||
operations: derived.executionBackend.operation_records_total ?? 0,
|
||||
effective: derived.executionBackend.effective_execution_records_total ?? 0,
|
||||
auditOnly: derived.executionBackend.audit_only_operation_records_total ?? 0,
|
||||
autoRepair: derived.executionBackend.auto_repair_execution_records_total ?? 0,
|
||||
ansibleRecords: derived.executionBackend.ansible_audit_record_total ?? 0,
|
||||
ansibleCandidates: derived.executionBackend.ansible_candidate_total ?? 0,
|
||||
checkMode: derived.executionBackend.ansible_check_mode_total ?? 0,
|
||||
apply: derived.executionBackend.ansible_apply_total ?? 0,
|
||||
pending: derived.executionBackend.ansible_pending_check_mode_total ?? 0,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{derived.topGate && (
|
||||
<div style={{
|
||||
borderTop: '0.5px solid #e0ddd4',
|
||||
|
||||
Reference in New Issue
Block a user