agent-lifecycle-kit 1.50.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_lifecycle/__init__.py +5 -0
- agent_lifecycle/__main__.py +5 -0
- agent_lifecycle/_version.py +3 -0
- agent_lifecycle/adapter_sessions/__init__.py +41 -0
- agent_lifecycle/adapter_sessions/contracts.py +133 -0
- agent_lifecycle/adapter_sessions/env.py +44 -0
- agent_lifecycle/adapter_sessions/launcher.py +101 -0
- agent_lifecycle/adapter_sessions/process.py +39 -0
- agent_lifecycle/adapter_sessions/redaction.py +30 -0
- agent_lifecycle/adapter_sessions/session_store.py +64 -0
- agent_lifecycle/adapter_sessions/task_intake.py +450 -0
- agent_lifecycle/adapter_sessions/workflow_bridge.py +154 -0
- agent_lifecycle/audit/__init__.py +67 -0
- agent_lifecycle/audit/bug_forensics.py +94 -0
- agent_lifecycle/audit/implementation.py +516 -0
- agent_lifecycle/audit/ownership.py +119 -0
- agent_lifecycle/audit/proof_integrity.py +814 -0
- agent_lifecycle/audit/review_verdict.py +116 -0
- agent_lifecycle/changesets/__init__.py +5 -0
- agent_lifecycle/changesets/git.py +35 -0
- agent_lifecycle/cli/__init__.py +5 -0
- agent_lifecycle/cli/adapter.py +360 -0
- agent_lifecycle/cli/dispatch.py +1220 -0
- agent_lifecycle/cli/followup.py +89 -0
- agent_lifecycle/cli/main.py +41 -0
- agent_lifecycle/cli/metrics_parser.py +49 -0
- agent_lifecycle/cli/parsers.py +618 -0
- agent_lifecycle/cli/policy.py +101 -0
- agent_lifecycle/cli/progress_hooks.py +117 -0
- agent_lifecycle/cli/worktree.py +74 -0
- agent_lifecycle/compiler/__init__.py +17 -0
- agent_lifecycle/compiler/small_model_packets.py +322 -0
- agent_lifecycle/compiler/task_packets.py +229 -0
- agent_lifecycle/context/__init__.py +20 -0
- agent_lifecycle/context/episode_retrieval.py +30 -0
- agent_lifecycle/context/external_memory.py +71 -0
- agent_lifecycle/context/profiles.py +104 -0
- agent_lifecycle/context/rendering.py +226 -0
- agent_lifecycle/contracts/__init__.py +25 -0
- agent_lifecycle/contracts/adapter_contract_schemas.py +130 -0
- agent_lifecycle/contracts/adapter_event_schemas.py +118 -0
- agent_lifecycle/contracts/adapter_session_schemas.py +130 -0
- agent_lifecycle/contracts/adapter_task_schemas.py +90 -0
- agent_lifecycle/contracts/audit_schemas.py +133 -0
- agent_lifecycle/contracts/bug_forensics_schemas.py +400 -0
- agent_lifecycle/contracts/canonical.py +55 -0
- agent_lifecycle/contracts/compatibility.py +242 -0
- agent_lifecycle/contracts/context_model_schemas.py +265 -0
- agent_lifecycle/contracts/core_schemas.py +252 -0
- agent_lifecycle/contracts/cross_check_schemas.py +121 -0
- agent_lifecycle/contracts/errors.py +26 -0
- agent_lifecycle/contracts/evidence_import_schemas.py +152 -0
- agent_lifecycle/contracts/host_capability_schemas.py +64 -0
- agent_lifecycle/contracts/import_dialect_schemas.py +166 -0
- agent_lifecycle/contracts/metric_schemas.py +315 -0
- agent_lifecycle/contracts/paths.py +31 -0
- agent_lifecycle/contracts/plan_contract_schemas.py +148 -0
- agent_lifecycle/contracts/policy_schemas.py +350 -0
- agent_lifecycle/contracts/progress_bridge_schemas.py +114 -0
- agent_lifecycle/contracts/progress_hook_schemas.py +119 -0
- agent_lifecycle/contracts/proof_integrity_schemas.py +240 -0
- agent_lifecycle/contracts/release_contract_schemas.py +476 -0
- agent_lifecycle/contracts/review_mesh_recommendation_schemas.py +64 -0
- agent_lifecycle/contracts/review_mesh_schemas.py +222 -0
- agent_lifecycle/contracts/review_quality_schemas.py +109 -0
- agent_lifecycle/contracts/runner_schemas.py +91 -0
- agent_lifecycle/contracts/runner_worktree_schemas.py +389 -0
- agent_lifecycle/contracts/sandbox_schemas.py +159 -0
- agent_lifecycle/contracts/schema_builders.py +23 -0
- agent_lifecycle/contracts/schemas.py +93 -0
- agent_lifecycle/contracts/status_goal_schemas.py +469 -0
- agent_lifecycle/contracts/task_template_schemas.py +96 -0
- agent_lifecycle/contracts/usage_export_schemas.py +68 -0
- agent_lifecycle/diagnostics/__init__.py +17 -0
- agent_lifecycle/diagnostics/bundles.py +124 -0
- agent_lifecycle/diagnostics/readiness.py +733 -0
- agent_lifecycle/evidence_index/__init__.py +39 -0
- agent_lifecycle/evidence_index/core.py +272 -0
- agent_lifecycle/evidence_index/episode_index.py +329 -0
- agent_lifecycle/evidence_index/external_context.py +276 -0
- agent_lifecycle/followup/__init__.py +23 -0
- agent_lifecycle/followup/records.py +404 -0
- agent_lifecycle/freeze/__init__.py +5 -0
- agent_lifecycle/freeze/locks.py +23 -0
- agent_lifecycle/goal/__init__.py +15 -0
- agent_lifecycle/goal/records.py +244 -0
- agent_lifecycle/goal/view.py +109 -0
- agent_lifecycle/host_protocol/__init__.py +65 -0
- agent_lifecycle/host_protocol/acp_capability.py +195 -0
- agent_lifecycle/host_protocol/capabilities.py +268 -0
- agent_lifecycle/host_protocol/contracts.py +214 -0
- agent_lifecycle/host_protocol/event_capture.py +390 -0
- agent_lifecycle/host_protocol/events.py +196 -0
- agent_lifecycle/host_protocol/inspection.py +194 -0
- agent_lifecycle/host_protocol/inspection_common.py +310 -0
- agent_lifecycle/host_protocol/inspection_cursor.py +164 -0
- agent_lifecycle/host_protocol/inspection_gemini_cli.py +163 -0
- agent_lifecycle/host_protocol/inspection_hermes.py +161 -0
- agent_lifecycle/host_protocol/inspection_kimi_code.py +169 -0
- agent_lifecycle/host_protocol/inspection_opencode.py +147 -0
- agent_lifecycle/host_protocol/inspection_qwen_code.py +143 -0
- agent_lifecycle/host_protocol/receipts.py +58 -0
- agent_lifecycle/host_protocol/scaffold.py +329 -0
- agent_lifecycle/host_protocol/validation.py +229 -0
- agent_lifecycle/imports/__init__.py +76 -0
- agent_lifecycle/imports/agentskills_profile.py +54 -0
- agent_lifecycle/imports/bmad_profile.py +49 -0
- agent_lifecycle/imports/constitution_adr.py +91 -0
- agent_lifecycle/imports/dialect_profiles.py +121 -0
- agent_lifecycle/imports/external_agent.py +28 -0
- agent_lifecycle/imports/external_dialects.py +486 -0
- agent_lifecycle/imports/external_workflow.py +28 -0
- agent_lifecycle/imports/multi_markdown.py +111 -0
- agent_lifecycle/imports/openspec_profile.py +49 -0
- agent_lifecycle/imports/planning.py +536 -0
- agent_lifecycle/imports/spec_kit_profile.py +49 -0
- agent_lifecycle/imports/spec_kitty_profile.py +49 -0
- agent_lifecycle/metrics/__init__.py +60 -0
- agent_lifecycle/metrics/cost_collection.py +315 -0
- agent_lifecycle/metrics/costs.py +203 -0
- agent_lifecycle/metrics/outcome_index.py +330 -0
- agent_lifecycle/metrics/phase_resources.py +208 -0
- agent_lifecycle/metrics/recommendations.py +507 -0
- agent_lifecycle/metrics/regression_signals.py +64 -0
- agent_lifecycle/metrics/usage_export.py +390 -0
- agent_lifecycle/model_routing/__init__.py +19 -0
- agent_lifecycle/model_routing/profiles.py +323 -0
- agent_lifecycle/model_routing/receipts.py +140 -0
- agent_lifecycle/model_routing/resolver.py +566 -0
- agent_lifecycle/neutrality/__init__.py +12 -0
- agent_lifecycle/neutrality/__main__.py +6 -0
- agent_lifecycle/neutrality/authority.py +265 -0
- agent_lifecycle/neutrality/canonical.py +28 -0
- agent_lifecycle/neutrality/cli.py +294 -0
- agent_lifecycle/neutrality/ed25519.py +124 -0
- agent_lifecycle/neutrality/errors.py +89 -0
- agent_lifecycle/neutrality/gate.py +269 -0
- agent_lifecycle/neutrality/paths.py +92 -0
- agent_lifecycle/neutrality/policy.py +76 -0
- agent_lifecycle/neutrality/receipt.py +135 -0
- agent_lifecycle/neutrality/scanner.py +310 -0
- agent_lifecycle/neutrality/test_runner.py +116 -0
- agent_lifecycle/planning/__init__.py +47 -0
- agent_lifecycle/planning/acceptance_markdown.py +159 -0
- agent_lifecycle/planning/completeness.py +362 -0
- agent_lifecycle/planning/continuity.py +255 -0
- agent_lifecycle/planning/templates.py +294 -0
- agent_lifecycle/planning/tiering.py +87 -0
- agent_lifecycle/planning/validation.py +72 -0
- agent_lifecycle/policy/__init__.py +37 -0
- agent_lifecycle/policy/adaptive_lifecycle.py +416 -0
- agent_lifecycle/policy/apply.py +44 -0
- agent_lifecycle/policy/proposals.py +189 -0
- agent_lifecycle/policy/quality_floor.py +144 -0
- agent_lifecycle/policy/runtime_receipts.py +160 -0
- agent_lifecycle/py.typed +1 -0
- agent_lifecycle/quality/__init__.py +89 -0
- agent_lifecycle/quality/bug_forensics.py +727 -0
- agent_lifecycle/quality/bug_forensics_advisor.py +88 -0
- agent_lifecycle/quality/bug_forensics_recipes.py +239 -0
- agent_lifecycle/quality/cross_check.py +437 -0
- agent_lifecycle/quality/failure_classification.py +318 -0
- agent_lifecycle/quality/packs.py +265 -0
- agent_lifecycle/reporting/__init__.py +37 -0
- agent_lifecycle/reporting/change_summary.py +156 -0
- agent_lifecycle/reporting/event_feed.py +101 -0
- agent_lifecycle/reporting/progress_bridge.py +153 -0
- agent_lifecycle/reporting/progress_hooks.py +161 -0
- agent_lifecycle/reporting/progress_terminal.py +95 -0
- agent_lifecycle/reporting/progress_view.py +284 -0
- agent_lifecycle/reporting/status_view.py +125 -0
- agent_lifecycle/reporting/usage_export.py +59 -0
- agent_lifecycle/review/__init__.py +5 -0
- agent_lifecycle/review/validation.py +29 -0
- agent_lifecycle/review_mesh/__init__.py +83 -0
- agent_lifecycle/review_mesh/assignments.py +160 -0
- agent_lifecycle/review_mesh/contracts.py +685 -0
- agent_lifecycle/review_mesh/operator_templates.py +320 -0
- agent_lifecycle/review_mesh/quorum.py +38 -0
- agent_lifecycle/review_mesh/recommendation.py +487 -0
- agent_lifecycle/review_mesh/results.py +98 -0
- agent_lifecycle/review_mesh/synthesis.py +102 -0
- agent_lifecycle/runner/__init__.py +47 -0
- agent_lifecycle/runner/attempt_snapshots.py +244 -0
- agent_lifecycle/runner/core.py +653 -0
- agent_lifecycle/runner/sandbox_receipts.py +628 -0
- agent_lifecycle/specification/__init__.py +23 -0
- agent_lifecycle/specification/completion_check.py +135 -0
- agent_lifecycle/specification/completion_gate.py +501 -0
- agent_lifecycle/specification/completion_signal.py +75 -0
- agent_lifecycle/specification/validation.py +48 -0
- agent_lifecycle/workflow/__init__.py +79 -0
- agent_lifecycle/workflow/artifacts.py +74 -0
- agent_lifecycle/workflow/budget_decisions.py +370 -0
- agent_lifecycle/workflow/budget_policy.py +140 -0
- agent_lifecycle/workflow/budget_receipts.py +70 -0
- agent_lifecycle/workflow/bug_forensics_gates.py +284 -0
- agent_lifecycle/workflow/controller.py +45 -0
- agent_lifecycle/workflow/events.py +50 -0
- agent_lifecycle/workflow/final_proof_integrity.py +74 -0
- agent_lifecycle/workflow/finalization.py +422 -0
- agent_lifecycle/workflow/gates.py +185 -0
- agent_lifecycle/workflow/implementation_audit_gate.py +126 -0
- agent_lifecycle/workflow/leases.py +220 -0
- agent_lifecycle/workflow/lineage.py +108 -0
- agent_lifecycle/workflow/managed_runner.py +167 -0
- agent_lifecycle/workflow/model_usage.py +106 -0
- agent_lifecycle/workflow/next_action.py +86 -0
- agent_lifecycle/workflow/operation_kernel.py +98 -0
- agent_lifecycle/workflow/plan_adoption.py +439 -0
- agent_lifecycle/workflow/query.py +52 -0
- agent_lifecycle/workflow/review_mesh_gate.py +93 -0
- agent_lifecycle/workflow/reviews.py +168 -0
- agent_lifecycle/workflow/run_transitions.py +163 -0
- agent_lifecycle/workflow/sandbox_policy.py +214 -0
- agent_lifecycle/workflow/selectors.py +57 -0
- agent_lifecycle/workflow/state.py +130 -0
- agent_lifecycle/workflow/task_transitions.py +421 -0
- agent_lifecycle/worktree/__init__.py +17 -0
- agent_lifecycle/worktree/isolation.py +407 -0
- agent_lifecycle_kit-1.50.1.dist-info/METADATA +201 -0
- agent_lifecycle_kit-1.50.1.dist-info/RECORD +227 -0
- agent_lifecycle_kit-1.50.1.dist-info/WHEEL +5 -0
- agent_lifecycle_kit-1.50.1.dist-info/entry_points.txt +3 -0
- agent_lifecycle_kit-1.50.1.dist-info/licenses/LICENSE +201 -0
- agent_lifecycle_kit-1.50.1.dist-info/licenses/NOTICE +5 -0
- agent_lifecycle_kit-1.50.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Audit helpers for bug-forensics gate evidence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from agent_lifecycle.contracts import LifecycleError, canonical_digest
|
|
8
|
+
from agent_lifecycle.workflow.bug_forensics_gates import validate_bug_forensics_gate_receipt
|
|
9
|
+
|
|
10
|
+
BUG_FORENSICS_AUDIT_SCHEMA = "agent-bug-forensics-audit.v1"
|
|
11
|
+
BUG_FORENSICS_AUDIT_VALIDATION_SCHEMA = "agent-bug-forensics-audit-validation.v1"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def build_bug_forensics_audit(*, gate_receipt: dict[str, Any], task_id: str | None = None) -> dict[str, Any]:
|
|
15
|
+
"""Audit that an accepted bug-forensics gate covers the required chain."""
|
|
16
|
+
|
|
17
|
+
gate_validation = validate_bug_forensics_gate_receipt(gate_receipt)
|
|
18
|
+
blockers: list[dict[str, Any]] = []
|
|
19
|
+
gate_status = gate_validation.get("gateStatus")
|
|
20
|
+
if gate_validation.get("status") != "PASS":
|
|
21
|
+
blockers.append({"code": "bug-forensics-gate-invalid", "validation": gate_validation})
|
|
22
|
+
if gate_receipt.get("activated") is True and gate_status != "PASS":
|
|
23
|
+
blockers.append({"code": "bug-forensics-active-gate-not-pass", "gateStatus": gate_status})
|
|
24
|
+
evidence = gate_receipt.get("evidence") if isinstance(gate_receipt.get("evidence"), dict) else {}
|
|
25
|
+
coverage = {
|
|
26
|
+
"reproductionBeforeModification": evidence.get("reproduction") is not None,
|
|
27
|
+
"failureFingerprint": evidence.get("failureFingerprint") is not None,
|
|
28
|
+
"hypothesisLedger": evidence.get("hypothesisLedger") is not None,
|
|
29
|
+
"regressionProof": evidence.get("regressionProof") is not None,
|
|
30
|
+
"fixImpactReceiptReused": evidence.get("fixImpact") is not None,
|
|
31
|
+
"crossCheckReceiptReused": evidence.get("crossCheck") is not None,
|
|
32
|
+
"phase2Deferred": list(gate_receipt.get("phase2Deferred", [])),
|
|
33
|
+
}
|
|
34
|
+
status = "SKIPPED" if gate_status == "SKIPPED" else ("PASS" if not blockers else "FAIL")
|
|
35
|
+
body = {
|
|
36
|
+
"schemaVersion": BUG_FORENSICS_AUDIT_SCHEMA,
|
|
37
|
+
"status": status,
|
|
38
|
+
"taskId": task_id or gate_receipt.get("taskId"),
|
|
39
|
+
"gateReceiptDigest": gate_receipt.get("receiptDigest"),
|
|
40
|
+
"coverage": coverage,
|
|
41
|
+
"blockers": blockers,
|
|
42
|
+
"productionPromotionClaimed": False,
|
|
43
|
+
}
|
|
44
|
+
return {**body, "auditDigest": canonical_digest(body)}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def validate_bug_forensics_audit(audit: dict[str, Any]) -> dict[str, Any]:
|
|
48
|
+
blockers: list[dict[str, Any]] = []
|
|
49
|
+
if not isinstance(audit, dict):
|
|
50
|
+
raise LifecycleError("invalid-bug-forensics-audit", "bug-forensics audit must be an object")
|
|
51
|
+
if audit.get("schemaVersion") != BUG_FORENSICS_AUDIT_SCHEMA:
|
|
52
|
+
blockers.append({"code": "bug-forensics-audit-schema-invalid"})
|
|
53
|
+
audit_status = audit.get("status")
|
|
54
|
+
if audit_status not in {"PASS", "FAIL", "SKIPPED"}:
|
|
55
|
+
blockers.append({"code": "bug-forensics-audit-status-invalid", "status": audit_status})
|
|
56
|
+
if audit.get("gateReceiptDigest") is not None and not _is_digest(audit.get("gateReceiptDigest")):
|
|
57
|
+
blockers.append({"code": "bug-forensics-audit-gate-digest-invalid"})
|
|
58
|
+
if not isinstance(audit.get("coverage"), dict):
|
|
59
|
+
blockers.append({"code": "bug-forensics-audit-coverage-invalid"})
|
|
60
|
+
if not isinstance(audit.get("blockers"), list) or not all(isinstance(item, dict) for item in audit.get("blockers", [])):
|
|
61
|
+
blockers.append({"code": "bug-forensics-audit-blockers-invalid"})
|
|
62
|
+
if audit.get("productionPromotionClaimed") is not False:
|
|
63
|
+
blockers.append({"code": "bug-forensics-audit-production-claim"})
|
|
64
|
+
expected_digest = canonical_digest({key: value for key, value in audit.items() if key != "auditDigest"})
|
|
65
|
+
if audit.get("auditDigest") != expected_digest:
|
|
66
|
+
blockers.append({"code": "bug-forensics-audit-digest-mismatch"})
|
|
67
|
+
if audit_status == "FAIL" and not audit.get("blockers"):
|
|
68
|
+
blockers.append({"code": "bug-forensics-audit-fail-without-blockers"})
|
|
69
|
+
body = {
|
|
70
|
+
"schemaVersion": BUG_FORENSICS_AUDIT_VALIDATION_SCHEMA,
|
|
71
|
+
"status": "PASS" if not blockers else "FAIL",
|
|
72
|
+
"auditStatus": audit_status if isinstance(audit_status, str) else None,
|
|
73
|
+
"blockers": blockers,
|
|
74
|
+
"auditDigest": audit.get("auditDigest"),
|
|
75
|
+
}
|
|
76
|
+
return {**body, "validationDigest": canonical_digest(body)}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def require_bug_forensics_audit_pass(validation: dict[str, Any]) -> dict[str, Any]:
|
|
80
|
+
if validation.get("status") != "PASS" or validation.get("auditStatus") not in {"PASS", "SKIPPED"}:
|
|
81
|
+
raise LifecycleError("bug-forensics-audit-validation-failed", "bug-forensics audit did not pass", {"validation": validation})
|
|
82
|
+
return validation
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _is_digest(value: Any) -> bool:
|
|
86
|
+
return isinstance(value, str) and len(value) == 64 and all(char in "0123456789abcdef" for char in value)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
__all__ = [
|
|
90
|
+
"BUG_FORENSICS_AUDIT_SCHEMA",
|
|
91
|
+
"build_bug_forensics_audit",
|
|
92
|
+
"require_bug_forensics_audit_pass",
|
|
93
|
+
"validate_bug_forensics_audit",
|
|
94
|
+
]
|
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
"""Deterministic implementation audit facade."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from agent_lifecycle.audit.ownership import build_ownership_report, report_has_category
|
|
9
|
+
from agent_lifecycle.contracts import LifecycleError, canonical_digest, read_json_object
|
|
10
|
+
from agent_lifecycle.contracts.paths import normalize_repo_path
|
|
11
|
+
from agent_lifecycle.workflow.artifacts import artifact_identity, package_root
|
|
12
|
+
from agent_lifecycle.workflow.reviews import validate_task_result, validate_task_review
|
|
13
|
+
from agent_lifecycle.workflow.sandbox_policy import validate_task_sandbox_evidence
|
|
14
|
+
from agent_lifecycle.workflow.selectors import find_task
|
|
15
|
+
from agent_lifecycle.workflow.review_mesh_gate import validate_review_mesh_quorum_path
|
|
16
|
+
from agent_lifecycle.workflow.state import load_state
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
SEVERITY_RANK = {"BLOCKER": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3, "INFO": 4}
|
|
20
|
+
IMPLEMENTATION_AUDIT_SCHEMA = "agent-implementation-audit-report.v1"
|
|
21
|
+
FINAL_IMPLEMENTATION_AUDIT_SCHEMA = "agent-final-implementation-audit.v1"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def build_implementation_audit_report(
|
|
25
|
+
*,
|
|
26
|
+
manifest_path: Path,
|
|
27
|
+
state_path: Path,
|
|
28
|
+
task_id: str,
|
|
29
|
+
result_path: str,
|
|
30
|
+
review_path: str,
|
|
31
|
+
evidence_paths: list[str] | None = None,
|
|
32
|
+
sandbox_receipt_paths: list[str] | None = None,
|
|
33
|
+
review_mesh_quorum_paths: list[str] | None = None,
|
|
34
|
+
changed_paths: list[str] | None = None,
|
|
35
|
+
expected_revision: int | None = None,
|
|
36
|
+
base: str | None = None,
|
|
37
|
+
auditor_id: str = "implementation-auditor",
|
|
38
|
+
auditor_surface: str = "cli",
|
|
39
|
+
) -> dict[str, Any]:
|
|
40
|
+
manifest = read_json_object(manifest_path, label="plan manifest")
|
|
41
|
+
state = load_state(state_path)
|
|
42
|
+
root = package_root(state_path, state)
|
|
43
|
+
task = find_task(state, task_id)
|
|
44
|
+
result_rel = normalize_repo_path(result_path, label="task result")
|
|
45
|
+
review_rel = normalize_repo_path(review_path, label="task review")
|
|
46
|
+
result = read_json_object(root / result_rel, label="task result")
|
|
47
|
+
review = read_json_object(root / review_rel, label="task review")
|
|
48
|
+
result_identity = artifact_identity(root, result_rel, result)
|
|
49
|
+
review_identity = artifact_identity(root, review_rel, review)
|
|
50
|
+
|
|
51
|
+
findings: list[dict[str, Any]] = []
|
|
52
|
+
blockers: list[dict[str, Any]] = []
|
|
53
|
+
_capture("state", findings, blockers, lambda: _validate_state(state, manifest, expected_revision=expected_revision))
|
|
54
|
+
_capture("result", findings, blockers, lambda: validate_task_result(state, task, result, result_identity))
|
|
55
|
+
task_for_review = {**task, "result": result_identity}
|
|
56
|
+
_capture("review", findings, blockers, lambda: validate_task_review(state, task_for_review, review))
|
|
57
|
+
_check_self_certification(result, review, findings, blockers)
|
|
58
|
+
|
|
59
|
+
changed = changed_paths if changed_paths is not None else _result_changed_files(result)
|
|
60
|
+
ownership = build_ownership_report(manifest_path, changed, base=base)
|
|
61
|
+
if report_has_category(ownership, {"forbidden", "read-only", "unowned"}):
|
|
62
|
+
_add_finding(
|
|
63
|
+
findings,
|
|
64
|
+
blockers,
|
|
65
|
+
code="implementation-write-scope-violation",
|
|
66
|
+
severity="HIGH",
|
|
67
|
+
category="ownership",
|
|
68
|
+
message="changed files include forbidden, read-only or unowned paths",
|
|
69
|
+
context=ownership["summary"],
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
evidence = _evidence_summary(root, task, evidence_paths or [])
|
|
73
|
+
for missing in evidence["missingEvidenceIds"]:
|
|
74
|
+
_add_finding(
|
|
75
|
+
findings,
|
|
76
|
+
blockers,
|
|
77
|
+
code="implementation-evidence-missing",
|
|
78
|
+
severity="MEDIUM",
|
|
79
|
+
category="evidence",
|
|
80
|
+
message=f"required evidence is missing: {missing}",
|
|
81
|
+
context={"evidenceId": missing},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
sandbox = _sandbox_summary(root, state, task, sandbox_receipt_paths or [])
|
|
85
|
+
if sandbox["validation"]["status"] != "PASS":
|
|
86
|
+
for blocker in sandbox["validation"]["blockers"]:
|
|
87
|
+
_add_finding(
|
|
88
|
+
findings,
|
|
89
|
+
blockers,
|
|
90
|
+
code=str(blocker.get("code") or "sandbox-evidence-failed"),
|
|
91
|
+
severity="HIGH",
|
|
92
|
+
category="sandbox",
|
|
93
|
+
message="required sandbox evidence did not pass",
|
|
94
|
+
context=blocker,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
review_mesh = _review_mesh_summary(root, state, task, review_mesh_quorum_paths or [])
|
|
98
|
+
if review_mesh["validation"]["status"] != "PASS":
|
|
99
|
+
for blocker in review_mesh["validation"]["blockers"]:
|
|
100
|
+
_add_finding(
|
|
101
|
+
findings,
|
|
102
|
+
blockers,
|
|
103
|
+
code=str(blocker.get("code") or "review-mesh-quorum-failed"),
|
|
104
|
+
severity="HIGH",
|
|
105
|
+
category="review-mesh",
|
|
106
|
+
message="required Review Mesh quorum evidence did not pass",
|
|
107
|
+
context=blocker,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
coverage = _coverage_summary(task, result, review)
|
|
111
|
+
for missing in coverage["missingAcceptanceIds"]:
|
|
112
|
+
_add_finding(
|
|
113
|
+
findings,
|
|
114
|
+
blockers,
|
|
115
|
+
code="implementation-acceptance-missing",
|
|
116
|
+
severity="MEDIUM",
|
|
117
|
+
category="coverage",
|
|
118
|
+
message=f"acceptance criterion is not covered by PASS review: {missing}",
|
|
119
|
+
context={"acceptanceId": missing},
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
findings = _sorted_findings(findings)
|
|
123
|
+
verdict = _verdict(findings)
|
|
124
|
+
status = "PASS" if verdict == "ACCEPTED" else "FAIL"
|
|
125
|
+
body = {
|
|
126
|
+
"schemaVersion": IMPLEMENTATION_AUDIT_SCHEMA,
|
|
127
|
+
"status": status,
|
|
128
|
+
"verdict": verdict,
|
|
129
|
+
"runId": state.get("runId"),
|
|
130
|
+
"packageId": state.get("packageId"),
|
|
131
|
+
"taskId": task.get("id"),
|
|
132
|
+
"attempt": task.get("attempt"),
|
|
133
|
+
"planRevision": state.get("planRevision"),
|
|
134
|
+
"planDigest": state.get("planDigest"),
|
|
135
|
+
"sourceRevision": state.get("sourceRevision"),
|
|
136
|
+
"auditor": {"id": auditor_id, "surface": auditor_surface, "independent": True},
|
|
137
|
+
"plan": {
|
|
138
|
+
"path": manifest_path.as_posix(),
|
|
139
|
+
"status": manifest.get("status"),
|
|
140
|
+
"planDigest": canonical_digest(manifest),
|
|
141
|
+
},
|
|
142
|
+
"result": {**result_identity, "changedFiles": changed, "commands": result.get("commands", [])},
|
|
143
|
+
"review": {
|
|
144
|
+
**review_identity,
|
|
145
|
+
"reviewer": review.get("reviewer"),
|
|
146
|
+
"verdict": review.get("verdict"),
|
|
147
|
+
},
|
|
148
|
+
"ownership": {"status": "PASS" if not blockers_for_categories(ownership, {"forbidden", "read-only", "unowned"}) else "FAIL", "report": ownership},
|
|
149
|
+
"coverage": coverage,
|
|
150
|
+
"evidence": evidence,
|
|
151
|
+
"sandbox": sandbox,
|
|
152
|
+
"reviewMesh": review_mesh,
|
|
153
|
+
"findings": findings,
|
|
154
|
+
"blockers": blockers,
|
|
155
|
+
"productionPromotionClaimed": False,
|
|
156
|
+
}
|
|
157
|
+
return {**body, "reportDigest": canonical_digest(body)}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def validate_implementation_audit_report(
|
|
161
|
+
report: dict[str, Any],
|
|
162
|
+
*,
|
|
163
|
+
state: dict[str, Any] | None = None,
|
|
164
|
+
task: dict[str, Any] | None = None,
|
|
165
|
+
) -> dict[str, Any]:
|
|
166
|
+
blockers: list[dict[str, Any]] = []
|
|
167
|
+
if report.get("schemaVersion") != IMPLEMENTATION_AUDIT_SCHEMA:
|
|
168
|
+
blockers.append({"code": "implementation-audit-schema", "message": "unsupported implementation audit schemaVersion"})
|
|
169
|
+
body = {key: value for key, value in report.items() if key != "reportDigest"}
|
|
170
|
+
if report.get("reportDigest") != canonical_digest(body):
|
|
171
|
+
blockers.append({"code": "implementation-audit-digest", "message": "reportDigest does not match report body"})
|
|
172
|
+
if report.get("productionPromotionClaimed") is not False:
|
|
173
|
+
blockers.append({"code": "implementation-audit-production-claim", "message": "implementation audit must not claim production promotion"})
|
|
174
|
+
if report.get("status") not in {"PASS", "FAIL"}:
|
|
175
|
+
blockers.append({"code": "implementation-audit-status", "message": "status must be PASS or FAIL"})
|
|
176
|
+
verdict = report.get("verdict")
|
|
177
|
+
if verdict not in {"ACCEPTED", "REWORK", "CONTRACT_CHANGE", "BLOCKED"}:
|
|
178
|
+
blockers.append({"code": "implementation-audit-verdict", "message": "verdict is unsupported"})
|
|
179
|
+
if report.get("status") == "PASS" and verdict != "ACCEPTED":
|
|
180
|
+
blockers.append({"code": "implementation-audit-status-verdict", "message": "PASS status requires ACCEPTED verdict"})
|
|
181
|
+
if report.get("status") == "FAIL" and verdict == "ACCEPTED":
|
|
182
|
+
blockers.append({"code": "implementation-audit-status-verdict", "message": "ACCEPTED verdict requires PASS status"})
|
|
183
|
+
if report.get("status") == "PASS" and _has_blockers(report):
|
|
184
|
+
blockers.append({"code": "implementation-audit-open-blockers", "message": "PASS report must not contain blockers"})
|
|
185
|
+
if report.get("status") == "PASS" and _has_open_blocking_findings(report):
|
|
186
|
+
blockers.append({"code": "implementation-audit-open-findings", "message": "PASS report must not contain open Medium or higher findings"})
|
|
187
|
+
auditor = report.get("auditor")
|
|
188
|
+
if not isinstance(auditor, dict) or auditor.get("independent") is not True:
|
|
189
|
+
blockers.append({"code": "implementation-audit-auditor", "message": "auditor must be independent"})
|
|
190
|
+
if state is not None:
|
|
191
|
+
expected = {
|
|
192
|
+
"runId": state.get("runId"),
|
|
193
|
+
"packageId": state.get("packageId"),
|
|
194
|
+
"planRevision": state.get("planRevision"),
|
|
195
|
+
"planDigest": state.get("planDigest"),
|
|
196
|
+
"sourceRevision": state.get("sourceRevision"),
|
|
197
|
+
}
|
|
198
|
+
_check_expected(report, expected, blockers, prefix="implementation-audit")
|
|
199
|
+
if task is not None:
|
|
200
|
+
_check_expected(
|
|
201
|
+
report,
|
|
202
|
+
{"taskId": task.get("id"), "attempt": task.get("attempt")},
|
|
203
|
+
blockers,
|
|
204
|
+
prefix="implementation-audit",
|
|
205
|
+
)
|
|
206
|
+
body = {
|
|
207
|
+
"schemaVersion": "agent-implementation-audit-report-validation.v1",
|
|
208
|
+
"status": "PASS" if not blockers else "FAIL",
|
|
209
|
+
"verdict": verdict,
|
|
210
|
+
"blockers": blockers,
|
|
211
|
+
"reportDigest": report.get("reportDigest"),
|
|
212
|
+
}
|
|
213
|
+
return {**body, "validationDigest": canonical_digest(body)}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def require_implementation_audit_accepted(validation: dict[str, Any], report: dict[str, Any]) -> dict[str, Any]:
|
|
217
|
+
if validation.get("status") != "PASS" or report.get("verdict") != "ACCEPTED" or report.get("status") != "PASS":
|
|
218
|
+
raise LifecycleError(
|
|
219
|
+
"implementation-audit-not-accepted",
|
|
220
|
+
"implementation audit report is not accepted",
|
|
221
|
+
{"validation": validation, "verdict": report.get("verdict")},
|
|
222
|
+
)
|
|
223
|
+
return validation
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def build_final_implementation_audit(
|
|
227
|
+
*,
|
|
228
|
+
manifest_path: Path,
|
|
229
|
+
state_path: Path,
|
|
230
|
+
report_paths: list[str],
|
|
231
|
+
auditor_id: str = "final-implementation-auditor",
|
|
232
|
+
auditor_surface: str = "cli",
|
|
233
|
+
) -> dict[str, Any]:
|
|
234
|
+
manifest = read_json_object(manifest_path, label="plan manifest")
|
|
235
|
+
state = load_state(state_path)
|
|
236
|
+
root = package_root(state_path, state)
|
|
237
|
+
reports = []
|
|
238
|
+
findings: list[dict[str, Any]] = []
|
|
239
|
+
blockers: list[dict[str, Any]] = []
|
|
240
|
+
for raw_path in report_paths:
|
|
241
|
+
rel = normalize_repo_path(raw_path, label="implementation audit report")
|
|
242
|
+
report = read_json_object(root / rel, label="implementation audit report")
|
|
243
|
+
task = find_task(state, str(report.get("taskId"))) if isinstance(report.get("taskId"), str) else None
|
|
244
|
+
validation = validate_implementation_audit_report(report, state=state, task=task)
|
|
245
|
+
identity = artifact_identity(root, rel, report)
|
|
246
|
+
accepted = validation["status"] == "PASS" and report.get("status") == "PASS" and report.get("verdict") == "ACCEPTED"
|
|
247
|
+
if not accepted:
|
|
248
|
+
blockers.append({"code": "implementation-audit-report-not-accepted", "path": rel, "validation": validation})
|
|
249
|
+
reports.append({
|
|
250
|
+
**identity,
|
|
251
|
+
"taskId": report.get("taskId"),
|
|
252
|
+
"attempt": report.get("attempt"),
|
|
253
|
+
"verdict": report.get("verdict"),
|
|
254
|
+
"validation": validation,
|
|
255
|
+
})
|
|
256
|
+
findings.extend(report.get("findings", []) if isinstance(report.get("findings"), list) else [])
|
|
257
|
+
accepted_report_task_ids = {item.get("taskId") for item in reports if item.get("verdict") == "ACCEPTED"}
|
|
258
|
+
missing_task_ids = [
|
|
259
|
+
str(task.get("id"))
|
|
260
|
+
for task in state.get("tasks", [])
|
|
261
|
+
if task.get("required", True) and task.get("status") == "ACCEPTED" and task.get("id") not in accepted_report_task_ids
|
|
262
|
+
]
|
|
263
|
+
for task_id in missing_task_ids:
|
|
264
|
+
blockers.append({"code": "implementation-audit-report-missing", "taskId": task_id})
|
|
265
|
+
if manifest.get("status") != "FROZEN":
|
|
266
|
+
blockers.append({"code": "plan-not-frozen", "message": "final implementation audit requires a FROZEN plan"})
|
|
267
|
+
body = {
|
|
268
|
+
"schemaVersion": FINAL_IMPLEMENTATION_AUDIT_SCHEMA,
|
|
269
|
+
"status": "PASS" if not blockers else "FAIL",
|
|
270
|
+
"runId": state.get("runId"),
|
|
271
|
+
"packageId": state.get("packageId"),
|
|
272
|
+
"planRevision": state.get("planRevision"),
|
|
273
|
+
"planDigest": state.get("planDigest"),
|
|
274
|
+
"sourceRevision": state.get("sourceRevision"),
|
|
275
|
+
"auditor": {"id": auditor_id, "surface": auditor_surface, "independent": True},
|
|
276
|
+
"plan": {
|
|
277
|
+
"path": manifest_path.as_posix(),
|
|
278
|
+
"status": manifest.get("status"),
|
|
279
|
+
"planDigest": canonical_digest(manifest),
|
|
280
|
+
},
|
|
281
|
+
"reports": reports,
|
|
282
|
+
"missingTaskIds": missing_task_ids,
|
|
283
|
+
"findings": _sorted_findings(findings),
|
|
284
|
+
"blockers": blockers,
|
|
285
|
+
"productionPromotionClaimed": False,
|
|
286
|
+
}
|
|
287
|
+
return {**body, "auditDigest": canonical_digest(body)}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def validate_final_implementation_audit(audit: dict[str, Any], *, state: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
291
|
+
blockers: list[dict[str, Any]] = []
|
|
292
|
+
if audit.get("schemaVersion") != FINAL_IMPLEMENTATION_AUDIT_SCHEMA:
|
|
293
|
+
blockers.append({"code": "final-implementation-audit-schema", "message": "unsupported final implementation audit schemaVersion"})
|
|
294
|
+
body = {key: value for key, value in audit.items() if key != "auditDigest"}
|
|
295
|
+
if audit.get("auditDigest") != canonical_digest(body):
|
|
296
|
+
blockers.append({"code": "final-implementation-audit-digest", "message": "auditDigest does not match audit body"})
|
|
297
|
+
if audit.get("status") != "PASS":
|
|
298
|
+
blockers.append({"code": "final-implementation-audit-status", "message": "final implementation audit status must be PASS"})
|
|
299
|
+
if audit.get("status") == "PASS" and _has_blockers(audit):
|
|
300
|
+
blockers.append({"code": "final-implementation-audit-open-blockers", "message": "PASS final implementation audit must not contain blockers"})
|
|
301
|
+
if audit.get("status") == "PASS" and _has_open_blocking_findings(audit):
|
|
302
|
+
blockers.append({"code": "final-implementation-audit-open-findings", "message": "PASS final implementation audit must not contain open Medium or higher findings"})
|
|
303
|
+
if audit.get("productionPromotionClaimed") is not False:
|
|
304
|
+
blockers.append({"code": "final-implementation-audit-production-claim", "message": "audit must not claim production promotion"})
|
|
305
|
+
if state is not None:
|
|
306
|
+
_check_expected(
|
|
307
|
+
audit,
|
|
308
|
+
{
|
|
309
|
+
"runId": state.get("runId"),
|
|
310
|
+
"packageId": state.get("packageId"),
|
|
311
|
+
"planRevision": state.get("planRevision"),
|
|
312
|
+
"planDigest": state.get("planDigest"),
|
|
313
|
+
"sourceRevision": state.get("sourceRevision"),
|
|
314
|
+
},
|
|
315
|
+
blockers,
|
|
316
|
+
prefix="final-implementation-audit",
|
|
317
|
+
)
|
|
318
|
+
body = {
|
|
319
|
+
"schemaVersion": "agent-final-implementation-audit-validation.v1",
|
|
320
|
+
"status": "PASS" if not blockers else "FAIL",
|
|
321
|
+
"blockers": blockers,
|
|
322
|
+
"auditDigest": audit.get("auditDigest"),
|
|
323
|
+
}
|
|
324
|
+
return {**body, "validationDigest": canonical_digest(body)}
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _validate_state(state: dict[str, Any], manifest: dict[str, Any], *, expected_revision: int | None) -> None:
|
|
328
|
+
if expected_revision is not None and state.get("stateRevision") != expected_revision:
|
|
329
|
+
raise LifecycleError(
|
|
330
|
+
"state-revision-mismatch",
|
|
331
|
+
"workflow state revision mismatch",
|
|
332
|
+
{"expected": expected_revision, "actual": state.get("stateRevision")},
|
|
333
|
+
)
|
|
334
|
+
if manifest.get("status") != "FROZEN":
|
|
335
|
+
raise LifecycleError("plan-not-frozen", "implementation audit requires a FROZEN plan")
|
|
336
|
+
if canonical_digest(manifest) != state.get("planDigest"):
|
|
337
|
+
raise LifecycleError("plan-digest-mismatch", "workflow state planDigest does not match manifest")
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _capture(
|
|
341
|
+
category: str,
|
|
342
|
+
findings: list[dict[str, Any]],
|
|
343
|
+
blockers: list[dict[str, Any]],
|
|
344
|
+
callback: Any,
|
|
345
|
+
) -> None:
|
|
346
|
+
try:
|
|
347
|
+
callback()
|
|
348
|
+
except LifecycleError as exc:
|
|
349
|
+
severity = "BLOCKER" if exc.code in {"state-revision-mismatch", "plan-digest-mismatch"} else "HIGH"
|
|
350
|
+
_add_finding(
|
|
351
|
+
findings,
|
|
352
|
+
blockers,
|
|
353
|
+
code=exc.code,
|
|
354
|
+
severity=severity,
|
|
355
|
+
category=category,
|
|
356
|
+
message=exc.message,
|
|
357
|
+
context=exc.details,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _check_self_certification(
|
|
362
|
+
result: dict[str, Any],
|
|
363
|
+
review: dict[str, Any],
|
|
364
|
+
findings: list[dict[str, Any]],
|
|
365
|
+
blockers: list[dict[str, Any]],
|
|
366
|
+
) -> None:
|
|
367
|
+
reviewer = review.get("reviewer")
|
|
368
|
+
if not isinstance(reviewer, dict):
|
|
369
|
+
return
|
|
370
|
+
same_id = reviewer.get("id") == result.get("actor")
|
|
371
|
+
same_run = reviewer.get("runId") == result.get("actorRunId")
|
|
372
|
+
if same_id or same_run:
|
|
373
|
+
_add_finding(
|
|
374
|
+
findings,
|
|
375
|
+
blockers,
|
|
376
|
+
code="worker-self-certification",
|
|
377
|
+
severity="HIGH",
|
|
378
|
+
category="review",
|
|
379
|
+
message="worker and reviewer identity overlap",
|
|
380
|
+
context={"actor": result.get("actor"), "reviewer": reviewer.get("id")},
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _result_changed_files(result: dict[str, Any]) -> list[str]:
|
|
385
|
+
changed = result.get("changedFiles")
|
|
386
|
+
return [item for item in changed if isinstance(item, str)] if isinstance(changed, list) else []
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _evidence_summary(root: Path, task: dict[str, Any], evidence_paths: list[str]) -> dict[str, Any]:
|
|
390
|
+
identities = []
|
|
391
|
+
for raw_path in evidence_paths:
|
|
392
|
+
rel = normalize_repo_path(raw_path, label="evidence")
|
|
393
|
+
payload = read_json_object(root / rel, label="evidence")
|
|
394
|
+
identities.append(artifact_identity(root, rel, payload))
|
|
395
|
+
required_ids = [item for item in task.get("evidenceIds", []) if isinstance(item, str)]
|
|
396
|
+
# The facade is path-based; supplied evidence paths satisfy the task evidence set.
|
|
397
|
+
missing = required_ids if required_ids and not identities else []
|
|
398
|
+
return {
|
|
399
|
+
"requiredEvidenceIds": required_ids,
|
|
400
|
+
"suppliedEvidence": identities,
|
|
401
|
+
"missingEvidenceIds": missing,
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _sandbox_summary(root: Path, state: dict[str, Any], task: dict[str, Any], sandbox_receipt_paths: list[str]) -> dict[str, Any]:
|
|
406
|
+
receipt = None
|
|
407
|
+
receipt_identity = None
|
|
408
|
+
if sandbox_receipt_paths:
|
|
409
|
+
rel = normalize_repo_path(sandbox_receipt_paths[0], label="sandbox receipt")
|
|
410
|
+
receipt = read_json_object(root / rel, label="sandbox receipt")
|
|
411
|
+
receipt_identity = artifact_identity(root, rel, receipt)
|
|
412
|
+
lineage = {
|
|
413
|
+
"runId": state.get("runId"),
|
|
414
|
+
"packageId": state.get("packageId"),
|
|
415
|
+
"planRevision": state.get("planRevision"),
|
|
416
|
+
"planDigest": state.get("planDigest"),
|
|
417
|
+
"sourceRevision": state.get("sourceRevision"),
|
|
418
|
+
}
|
|
419
|
+
validation = validate_task_sandbox_evidence(task, receipt=receipt, expected_lineage=lineage, attempt=task.get("attempt"))
|
|
420
|
+
return {"receipt": receipt_identity, "validation": validation}
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _review_mesh_summary(root: Path, state: dict[str, Any], task: dict[str, Any], quorum_receipt_paths: list[str]) -> dict[str, Any]:
|
|
424
|
+
config = task.get("reviewMesh") if isinstance(task.get("reviewMesh"), dict) else state.get("reviewMesh")
|
|
425
|
+
receipt_path = quorum_receipt_paths[0] if quorum_receipt_paths else None
|
|
426
|
+
validation = validate_review_mesh_quorum_path(
|
|
427
|
+
root=root,
|
|
428
|
+
phase="implementation-audit",
|
|
429
|
+
config=config if isinstance(config, dict) else None,
|
|
430
|
+
receipt_path=receipt_path,
|
|
431
|
+
)
|
|
432
|
+
return {"receiptPath": receipt_path, "validation": validation}
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _coverage_summary(task: dict[str, Any], result: dict[str, Any], review: dict[str, Any]) -> dict[str, Any]:
|
|
436
|
+
item_outcomes = [item for item in result.get("itemOutcomes", []) if isinstance(item, dict)]
|
|
437
|
+
acceptance_checks = [item for item in review.get("acceptanceChecks", []) if isinstance(item, dict)]
|
|
438
|
+
required_acceptance = [item for item in task.get("acceptanceIds", []) if isinstance(item, str)]
|
|
439
|
+
passing_acceptance = {
|
|
440
|
+
item.get("acceptanceId")
|
|
441
|
+
for item in acceptance_checks
|
|
442
|
+
if item.get("status") == "PASS"
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
"plannedItemCount": len(item_outcomes),
|
|
446
|
+
"completedItemCount": sum(1 for item in item_outcomes if item.get("status") == "COMPLETE"),
|
|
447
|
+
"requiredAcceptanceIds": required_acceptance,
|
|
448
|
+
"passingAcceptanceIds": sorted(item for item in passing_acceptance if isinstance(item, str)),
|
|
449
|
+
"missingAcceptanceIds": sorted(set(required_acceptance).difference(passing_acceptance)),
|
|
450
|
+
"validationCommands": result.get("commands", []),
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def blockers_for_categories(report: dict[str, Any], categories: set[str]) -> list[dict[str, Any]]:
|
|
455
|
+
return [entry for entry in report.get("entries", []) if entry.get("category") in categories]
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _has_blockers(payload: dict[str, Any]) -> bool:
|
|
459
|
+
blockers = payload.get("blockers")
|
|
460
|
+
return isinstance(blockers, list) and len(blockers) > 0
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _has_open_blocking_findings(payload: dict[str, Any]) -> bool:
|
|
464
|
+
findings = payload.get("findings")
|
|
465
|
+
if not isinstance(findings, list):
|
|
466
|
+
return False
|
|
467
|
+
return any(
|
|
468
|
+
isinstance(item, dict)
|
|
469
|
+
and item.get("status") == "open"
|
|
470
|
+
and item.get("severity") in {"BLOCKER", "HIGH", "MEDIUM"}
|
|
471
|
+
for item in findings
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _add_finding(
|
|
476
|
+
findings: list[dict[str, Any]],
|
|
477
|
+
blockers: list[dict[str, Any]],
|
|
478
|
+
*,
|
|
479
|
+
code: str,
|
|
480
|
+
severity: str,
|
|
481
|
+
category: str,
|
|
482
|
+
message: str,
|
|
483
|
+
context: dict[str, Any] | None = None,
|
|
484
|
+
) -> None:
|
|
485
|
+
finding = {
|
|
486
|
+
"id": f"finding-{canonical_digest({'code': code, 'category': category, 'message': message})[:16]}",
|
|
487
|
+
"code": code,
|
|
488
|
+
"severity": severity,
|
|
489
|
+
"category": category,
|
|
490
|
+
"status": "open",
|
|
491
|
+
"message": message,
|
|
492
|
+
"context": context or {},
|
|
493
|
+
}
|
|
494
|
+
findings.append(finding)
|
|
495
|
+
if severity in {"BLOCKER", "HIGH", "MEDIUM"}:
|
|
496
|
+
blockers.append({"code": code, "severity": severity, "message": message, "context": context or {}})
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _sorted_findings(findings: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
500
|
+
return sorted(findings, key=lambda item: (SEVERITY_RANK.get(str(item.get("severity")), 99), str(item.get("code"))))
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def _verdict(findings: list[dict[str, Any]]) -> str:
|
|
504
|
+
if any(item.get("severity") == "BLOCKER" for item in findings):
|
|
505
|
+
return "BLOCKED"
|
|
506
|
+
if any(item.get("category") == "ownership" for item in findings):
|
|
507
|
+
return "CONTRACT_CHANGE"
|
|
508
|
+
if any(item.get("severity") in {"HIGH", "MEDIUM"} for item in findings):
|
|
509
|
+
return "REWORK"
|
|
510
|
+
return "ACCEPTED"
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _check_expected(payload: dict[str, Any], expected: dict[str, Any], blockers: list[dict[str, Any]], *, prefix: str) -> None:
|
|
514
|
+
for key, value in expected.items():
|
|
515
|
+
if payload.get(key) != value:
|
|
516
|
+
blockers.append({"code": f"{prefix}-lineage-mismatch", "field": key, "expected": value, "actual": payload.get(key)})
|