feat(awooop): surface execution backend evidence
All checks were successful
CD Pipeline / tests (push) Successful in 5m49s
Code Review / ai-code-review (push) Successful in 11s
CD Pipeline / build-and-deploy (push) Successful in 4m5s
CD Pipeline / post-deploy-checks (push) Successful in 1m46s

This commit is contained in:
Your Name
2026-05-24 14:35:42 +08:00
parent 17b62da59a
commit dc09dac4d4
5 changed files with 212 additions and 3 deletions

View File

@@ -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 = [

View File

@@ -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"

View File

@@ -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",

View File

@@ -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": "品質摘要未列出主要缺口",

View File

@@ -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',