engineering-platform 2.2.0__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.
Potentially problematic release.
This version of engineering-platform might be problematic. Click here for more details.
- engineering_platform/ENGINEERING_PLATFORM_CONFIG.json +32 -0
- engineering_platform/ENGINEERING_PLATFORM_VERSION.json +15 -0
- engineering_platform/__init__.py +1 -0
- engineering_platform/__main__.py +7 -0
- engineering_platform/agent_state.py +530 -0
- engineering_platform/agent_trust.py +174 -0
- engineering_platform/assets/dashboard.css +1317 -0
- engineering_platform/assets/dashboard.js +8534 -0
- engineering_platform/assets/dashboard_locales.mjs +4049 -0
- engineering_platform/assets/dashboard_status_store.mjs +41 -0
- engineering_platform/assets/operations-console/apple-touch-icon-dark.png +0 -0
- engineering_platform/assets/operations-console/apple-touch-icon-light.png +0 -0
- engineering_platform/assets/operations-console/icon-dark.png +0 -0
- engineering_platform/assets/operations-console/icon-light.png +0 -0
- engineering_platform/assets/operations-console/icon-transparent.png +0 -0
- engineering_platform/assets/operations-console/manifest.webmanifest +11 -0
- engineering_platform/capability_preflight.py +285 -0
- engineering_platform/capability_review.py +261 -0
- engineering_platform/central_data_transfer.py +195 -0
- engineering_platform/central_database.py +245 -0
- engineering_platform/central_store_migration.py +1672 -0
- engineering_platform/codex_capacity.py +81 -0
- engineering_platform/codex_chat.py +226 -0
- engineering_platform/codex_observability.py +153 -0
- engineering_platform/component_lock.py +40 -0
- engineering_platform/component_logging.py +420 -0
- engineering_platform/console_presentation.py +14 -0
- engineering_platform/console_route_ownership.py +83 -0
- engineering_platform/contracts/__init__.py +38 -0
- engineering_platform/contracts/ep_consumer.py +391 -0
- engineering_platform/contracts/models.py +105 -0
- engineering_platform/contracts/projection.py +401 -0
- engineering_platform/dashboard_browser_validation.py +206 -0
- engineering_platform/dashboard_state.py +630 -0
- engineering_platform/dashboard_supervisor.swift +105 -0
- engineering_platform/dashboard_translation.py +129 -0
- engineering_platform/dependabot_producer.py +349 -0
- engineering_platform/drift_diagnostics.py +144 -0
- engineering_platform/emergency_recovery.py +268 -0
- engineering_platform/engineering_memory.py +139 -0
- engineering_platform/ep_consumer_credentials.py +473 -0
- engineering_platform/evidence_projection.py +213 -0
- engineering_platform/execution_activity.py +218 -0
- engineering_platform/execution_context.py +132 -0
- engineering_platform/execution_errors.py +42 -0
- engineering_platform/execution_evidence.py +24 -0
- engineering_platform/execution_executor.py +730 -0
- engineering_platform/execution_finalization.py +44 -0
- engineering_platform/execution_host.py +3306 -0
- engineering_platform/execution_lease.py +365 -0
- engineering_platform/execution_lifecycle.py +447 -0
- engineering_platform/execution_models.py +43 -0
- engineering_platform/execution_readiness.py +166 -0
- engineering_platform/execution_reporting.py +1607 -0
- engineering_platform/execution_repository.py +253 -0
- engineering_platform/execution_timeout_policy.py +56 -0
- engineering_platform/execution_timing.py +440 -0
- engineering_platform/execution_transaction.py +28 -0
- engineering_platform/external_producer_binding.py +235 -0
- engineering_platform/file_inbox.py +249 -0
- engineering_platform/forensic_attribution.py +338 -0
- engineering_platform/forensic_attribution_v2.py +134 -0
- engineering_platform/forensic_delta.py +299 -0
- engineering_platform/golden_scenario.py +63 -0
- engineering_platform/historical_dashboard_configuration.py +171 -0
- engineering_platform/host_admin.py +199 -0
- engineering_platform/host_preflight.py +231 -0
- engineering_platform/installation_relocation.py +122 -0
- engineering_platform/investigation_ledger.py +89 -0
- engineering_platform/legacy_inbox_migration.py +79 -0
- engineering_platform/lifecycle_worker.py +223 -0
- engineering_platform/live_status.py +267 -0
- engineering_platform/local_api.py +209 -0
- engineering_platform/local_api_keychain.py +51 -0
- engineering_platform/local_repository_binding.py +138 -0
- engineering_platform/managed_autonomy.py +509 -0
- engineering_platform/managed_codex_runtime.py +105 -0
- engineering_platform/parity_context.py +203 -0
- engineering_platform/parity_lifecycle_dispatcher.py +488 -0
- engineering_platform/platform_admin.py +13 -0
- engineering_platform/platform_api.py +428 -0
- engineering_platform/platform_bootstrap.py +385 -0
- engineering_platform/platform_components.py +65 -0
- engineering_platform/platform_version.py +171 -0
- engineering_platform/pr_check_repair.py +276 -0
- engineering_platform/pr_evidence_backfill.py +278 -0
- engineering_platform/producer.py +209 -0
- engineering_platform/project_agent.py +366 -0
- engineering_platform/project_agent_service.py +244 -0
- engineering_platform/project_topology.py +126 -0
- engineering_platform/prompt_history.py +591 -0
- engineering_platform/provider_context.py +136 -0
- engineering_platform/provider_context_benchmark.py +41 -0
- engineering_platform/provider_context_scope.py +90 -0
- engineering_platform/provider_interruption.py +168 -0
- engineering_platform/provider_process_identity.py +80 -0
- engineering_platform/provider_readiness.py +138 -0
- engineering_platform/provider_recovery.py +647 -0
- engineering_platform/provider_usage.py +497 -0
- engineering_platform/providers.py +471 -0
- engineering_platform/qualification.py +220 -0
- engineering_platform/recommendation_handoff.py +238 -0
- engineering_platform/report_analysis.py +193 -0
- engineering_platform/repository_attachment.py +171 -0
- engineering_platform/repository_handoff.py +95 -0
- engineering_platform/resources.py +38 -0
- engineering_platform/reviewer_evidence.py +70 -0
- engineering_platform/schemas/repository-attachment.schema.json +61 -0
- engineering_platform/server.py +3679 -0
- engineering_platform/server_console_services.py +2024 -0
- engineering_platform/server_relay.py +172 -0
- engineering_platform/server_service.py +122 -0
- engineering_platform/status_model.py +135 -0
- engineering_platform/status_reconciliation.py +34 -0
- engineering_platform/storage.py +2440 -0
- engineering_platform/submission_cli.py +77 -0
- engineering_platform/submission_intake.py +45 -0
- engineering_platform/submission_service.py +317 -0
- engineering_platform/telemetry.py +951 -0
- engineering_platform/templates/workspace-config.json +25 -0
- engineering_platform/validation_identity.py +50 -0
- engineering_platform/validation_profile.py +211 -0
- engineering_platform/workspace_preflight.py +263 -0
- engineering_platform/worktree_provenance.py +147 -0
- engineering_platform/worktree_tooling.py +18 -0
- engineering_platform-2.2.0.dist-info/METADATA +18 -0
- engineering_platform-2.2.0.dist-info/RECORD +130 -0
- engineering_platform-2.2.0.dist-info/WHEEL +5 -0
- engineering_platform-2.2.0.dist-info/entry_points.txt +6 -0
- engineering_platform-2.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
"""Read-only, run-scoped execution lifecycle projection for the console.
|
|
2
|
+
|
|
3
|
+
This module deliberately projects persisted Execution Host checkpoints only. It
|
|
4
|
+
does not coordinate transitions, repair, retry, resume, liveness, or telemetry.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .storage import EngineeringStorageError, load_run_qualification_snapshot, open_storage
|
|
12
|
+
from .agent_state import TransactionState
|
|
13
|
+
from .provider_recovery import load_recovery_state
|
|
14
|
+
from .status_reconciliation import is_stale_rolling_status_block
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
TERMINAL = frozenset({"COMPLETE", "BLOCKED", "FAILED"})
|
|
18
|
+
_MANAGED_PATH = (
|
|
19
|
+
"START", "INITIALIZE", "CAPABILITY_REVIEW", "EXECUTE_AGENT", "LOCAL_REPOSITORY_VALIDATION", "QUALITY_CONTROL_AGENT", "REPAIR_AGENT",
|
|
20
|
+
"WAIT_FOR_OPERATOR_MERGE", "FINALIZE_AGENT", "WAIT_FOR_FINALIZATION_MERGE",
|
|
21
|
+
"RECONCILE_AGENT", "REPOSITORY_CLEANUP", "TERMINAL",
|
|
22
|
+
)
|
|
23
|
+
_FINALIZATION_REPAIR_PATH = (
|
|
24
|
+
"START", "INITIALIZE", "CAPABILITY_REVIEW", "EXECUTE_AGENT", "QUALITY_CONTROL_AGENT", "REPAIR_AGENT",
|
|
25
|
+
"WAIT_FOR_OPERATOR_MERGE", "FINALIZE_AGENT", "FINALIZATION_REPAIR_AGENT", "WAIT_FOR_FINALIZATION_MERGE",
|
|
26
|
+
"RECONCILE_AGENT", "REPOSITORY_CLEANUP", "TERMINAL",
|
|
27
|
+
)
|
|
28
|
+
# Genesis has no pull-request merge boundary. This is presentation of the
|
|
29
|
+
# existing mode contract, not a new execution sequence.
|
|
30
|
+
_GENESIS_PATH = (
|
|
31
|
+
"START", "INITIALIZE", "CAPABILITY_REVIEW", "EXECUTE_AGENT", "QUALITY_CONTROL_AGENT", "REPAIR_AGENT",
|
|
32
|
+
"FINALIZE_AGENT", "REPOSITORY_CLEANUP", "TERMINAL",
|
|
33
|
+
)
|
|
34
|
+
# A reconciliation-only run remains part of the full delivery narrative. Its
|
|
35
|
+
# earlier delivery phases are explicitly skipped rather than silently omitted,
|
|
36
|
+
# so the dashboard never renders a misleading half-workflow.
|
|
37
|
+
_STATUS_RECONCILIATION_PATH = tuple(step for step in _MANAGED_PATH if step != "LOCAL_REPOSITORY_VALIDATION")
|
|
38
|
+
|
|
39
|
+
# This is a presentation-only association. The Execution Host stays the
|
|
40
|
+
# authority for phase timing; this projection only groups persisted evidence
|
|
41
|
+
# under the corresponding visible lifecycle step.
|
|
42
|
+
_STEP_PHASES = {
|
|
43
|
+
"START": frozenset({"QUEUE_WAIT", "SUBMISSION_CLAIM"}),
|
|
44
|
+
"INITIALIZE": frozenset({"INITIALIZATION", "HOST_PREFLIGHT", "WORKSPACE_PREFLIGHT", "CAPABILITY_PREFLIGHT"}),
|
|
45
|
+
"CAPABILITY_REVIEW": frozenset({"CAPABILITY_REVIEW"}),
|
|
46
|
+
"EXECUTE_AGENT": frozenset({"EXECUTION_PREPARATION", "PROVIDER_EXECUTION", "VALIDATION"}),
|
|
47
|
+
"LOCAL_REPOSITORY_VALIDATION": frozenset({"VALIDATION", "PROVIDER_EXECUTION"}),
|
|
48
|
+
"QUALITY_CONTROL_AGENT": frozenset({"QUALITY_CONTROL", "PROVIDER_EXECUTION", "VALIDATION"}),
|
|
49
|
+
"REPAIR_AGENT": frozenset({"REPAIR", "PROVIDER_EXECUTION", "VALIDATION"}),
|
|
50
|
+
"FINALIZATION_REPAIR_AGENT": frozenset({"REPAIR", "PROVIDER_EXECUTION", "VALIDATION"}),
|
|
51
|
+
"WAIT_FOR_OPERATOR_MERGE": frozenset({"PR_OR_MERGE", "EXTERNAL_CI_WAIT"}),
|
|
52
|
+
"FINALIZE_AGENT": frozenset({"REPOSITORY_FINALIZATION", "FINALIZATION"}),
|
|
53
|
+
"RECONCILE_AGENT": frozenset({"RECONCILIATION", "REPORT_GENERATION", "EVIDENCE_PERSISTENCE"}),
|
|
54
|
+
"REPOSITORY_CLEANUP": frozenset({"REPOSITORY_CLEANUP"}),
|
|
55
|
+
"TERMINAL": frozenset({"TOTAL_EXECUTION"}),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def intended_path(
|
|
60
|
+
execution_mode: object,
|
|
61
|
+
transaction_kind: object = "IMPLEMENTATION",
|
|
62
|
+
implementation_pull_request: object = None,
|
|
63
|
+
) -> tuple[str, ...]:
|
|
64
|
+
"""Return the canonical display path for one existing execution mode."""
|
|
65
|
+
if transaction_kind == "RECONCILIATION" or (
|
|
66
|
+
transaction_kind == "FINALIZATION" and implementation_pull_request is None
|
|
67
|
+
):
|
|
68
|
+
return _STATUS_RECONCILIATION_PATH
|
|
69
|
+
if transaction_kind == "FINALIZATION":
|
|
70
|
+
return _FINALIZATION_REPAIR_PATH
|
|
71
|
+
return _GENESIS_PATH if execution_mode == "GENESIS" else _MANAGED_PATH
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _checkpoint(value: object) -> dict[str, object]:
|
|
75
|
+
if not isinstance(value, str):
|
|
76
|
+
return {}
|
|
77
|
+
try:
|
|
78
|
+
parsed = json.loads(value)
|
|
79
|
+
except json.JSONDecodeError:
|
|
80
|
+
return {}
|
|
81
|
+
return parsed if isinstance(parsed, dict) else {}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _nonnegative_int(value: object) -> int:
|
|
85
|
+
return value if isinstance(value, int) and not isinstance(value, bool) and value >= 0 else 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _pull_request_recorded(value: object) -> bool:
|
|
89
|
+
return isinstance(value, int) and not isinstance(value, bool) and value > 0
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _display_phase(phase: str, checkpoint: dict[str, object]) -> str:
|
|
93
|
+
"""Map internal evidence polling onto its visible lifecycle boundary."""
|
|
94
|
+
# GitHub plus origin/main evidence has already proven this hand-off was
|
|
95
|
+
# merged. The watcher owns the subsequent resume, but the console must
|
|
96
|
+
# not keep showing an obsolete operator action in that small interval.
|
|
97
|
+
if phase == "WAIT_FOR_OPERATOR_MERGE" and checkpoint.get("next_action") == "resume_verified_merge":
|
|
98
|
+
return "FINALIZE_AGENT" if checkpoint.get("transaction_kind") == "IMPLEMENTATION" else "RECONCILE_AGENT"
|
|
99
|
+
# Finalization has its own PR hand-off. Do not project it back onto the
|
|
100
|
+
# implementation merge once that implementation PR is already merged.
|
|
101
|
+
if phase == "WAIT_FOR_OPERATOR_MERGE" and checkpoint.get("transaction_kind") == "FINALIZATION":
|
|
102
|
+
return (
|
|
103
|
+
"WAIT_FOR_RECONCILIATION_MERGE"
|
|
104
|
+
if checkpoint.get("implementation_pull_request") is None
|
|
105
|
+
else "WAIT_FOR_FINALIZATION_MERGE"
|
|
106
|
+
)
|
|
107
|
+
if phase == "WAIT_FOR_OPERATOR_MERGE" and checkpoint.get("transaction_kind") == "RECONCILIATION":
|
|
108
|
+
return "WAIT_FOR_RECONCILIATION_MERGE"
|
|
109
|
+
if phase == "REPAIR_AGENT" and checkpoint.get("transaction_kind") == "FINALIZATION":
|
|
110
|
+
return "FINALIZATION_REPAIR_AGENT"
|
|
111
|
+
if phase != "WAIT_FOR_TERMINAL_EVIDENCE":
|
|
112
|
+
return phase
|
|
113
|
+
# Required-check polling happens before the operator merge hand-off. It
|
|
114
|
+
# is not a completed merge, even though its PR timing evidence exists.
|
|
115
|
+
# A finalization transaction uses the same internal polling phase after
|
|
116
|
+
# the implementation merge, so it remains on its own visible step.
|
|
117
|
+
if checkpoint.get("transaction_kind") == "FINALIZATION":
|
|
118
|
+
return "RECONCILE_AGENT" if checkpoint.get("implementation_pull_request") is None else "FINALIZE_AGENT"
|
|
119
|
+
if checkpoint.get("transaction_kind") == "RECONCILIATION":
|
|
120
|
+
return "RECONCILE_AGENT"
|
|
121
|
+
return "WAIT_FOR_OPERATOR_MERGE"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def projection(root: Path, run_id: str | None) -> dict[str, object]:
|
|
125
|
+
"""Project persisted lifecycle evidence for exactly one ``run_id``.
|
|
126
|
+
|
|
127
|
+
Missing event history is explicitly represented as unavailable. The
|
|
128
|
+
terminal checkpoint may still be shown, but no intermediate progress is
|
|
129
|
+
invented from reports, commits, timing or prompt content.
|
|
130
|
+
"""
|
|
131
|
+
if not isinstance(run_id, str) or not run_id:
|
|
132
|
+
return {"run_id": run_id, "available": False, "steps": []}
|
|
133
|
+
try:
|
|
134
|
+
connection = open_storage(root, create=False)
|
|
135
|
+
try:
|
|
136
|
+
row = connection.execute(
|
|
137
|
+
"SELECT payload,phase FROM engineering_transactions WHERE run_id=?", (run_id,)
|
|
138
|
+
).fetchone()
|
|
139
|
+
events = connection.execute(
|
|
140
|
+
"SELECT phase,checkpoint,recorded_at FROM execution_lifecycle_events WHERE run_id=? ORDER BY id",
|
|
141
|
+
(run_id,),
|
|
142
|
+
).fetchall()
|
|
143
|
+
mode_row = connection.execute(
|
|
144
|
+
"SELECT execution_mode FROM execution_runs WHERE run_id=?", (run_id,)
|
|
145
|
+
).fetchone()
|
|
146
|
+
phase_spans = connection.execute(
|
|
147
|
+
"SELECT phase_id,parent_phase_id,phase_name,attempt,started_at,completed_at,duration_ms,outcome,metadata "
|
|
148
|
+
"FROM execution_phase_spans WHERE run_id=? ORDER BY ordinal",
|
|
149
|
+
(run_id,),
|
|
150
|
+
).fetchall()
|
|
151
|
+
finally:
|
|
152
|
+
connection.close()
|
|
153
|
+
except EngineeringStorageError:
|
|
154
|
+
return {"run_id": run_id, "available": False, "steps": []}
|
|
155
|
+
if row is None:
|
|
156
|
+
return {"run_id": run_id, "available": False, "steps": []}
|
|
157
|
+
checkpoint = _checkpoint(row[0])
|
|
158
|
+
phase = str(row[1])
|
|
159
|
+
mode = checkpoint.get("execution_mode") or (mode_row[0] if mode_row else None) or "MANAGED"
|
|
160
|
+
transaction_kind = checkpoint.get("transaction_kind") or "IMPLEMENTATION"
|
|
161
|
+
path = intended_path(mode, transaction_kind, checkpoint.get("implementation_pull_request"))
|
|
162
|
+
display_phase = _display_phase(phase, checkpoint)
|
|
163
|
+
observed: dict[str, dict[str, object]] = {}
|
|
164
|
+
repair_iterations = 0
|
|
165
|
+
implementation_merge_required = _pull_request_recorded(checkpoint.get("implementation_pull_request"))
|
|
166
|
+
finalization_merge_required = _pull_request_recorded(checkpoint.get("finalization_pull_request"))
|
|
167
|
+
reconciliation_merge_required = _pull_request_recorded(checkpoint.get("reconciliation_pull_request"))
|
|
168
|
+
recorded_pull_request = any(
|
|
169
|
+
_pull_request_recorded(checkpoint.get(field))
|
|
170
|
+
for field in ("pull_request", "implementation_pull_request", "finalization_pull_request", "reconciliation_pull_request")
|
|
171
|
+
)
|
|
172
|
+
if checkpoint.get("transaction_kind") == "FINALIZATION":
|
|
173
|
+
finalization_merge_required = finalization_merge_required or _pull_request_recorded(checkpoint.get("pull_request"))
|
|
174
|
+
if checkpoint.get("transaction_kind") == "RECONCILIATION":
|
|
175
|
+
reconciliation_merge_required = reconciliation_merge_required or _pull_request_recorded(checkpoint.get("pull_request"))
|
|
176
|
+
else:
|
|
177
|
+
implementation_merge_required = implementation_merge_required or _pull_request_recorded(checkpoint.get("pull_request"))
|
|
178
|
+
verified_merge_resume_kinds: set[str] = set()
|
|
179
|
+
if checkpoint.get("next_action") == "resume_verified_merge":
|
|
180
|
+
verified_merge_resume_kinds.add(str(transaction_kind))
|
|
181
|
+
for event_phase, event_checkpoint, recorded_at in events:
|
|
182
|
+
event = _checkpoint(event_checkpoint)
|
|
183
|
+
if event.get("next_action") == "resume_verified_merge":
|
|
184
|
+
verified_merge_resume_kinds.add(str(event.get("transaction_kind") or "IMPLEMENTATION"))
|
|
185
|
+
recorded_pull_request = recorded_pull_request or any(
|
|
186
|
+
_pull_request_recorded(event.get(field))
|
|
187
|
+
for field in ("pull_request", "implementation_pull_request", "finalization_pull_request", "reconciliation_pull_request")
|
|
188
|
+
)
|
|
189
|
+
event_step = _display_phase(str(event_phase), event)
|
|
190
|
+
if event_step in path and event_step not in {"START", "TERMINAL"}:
|
|
191
|
+
observed.setdefault(event_step, {"started_at": recorded_at})
|
|
192
|
+
if event_phase == "REPAIR_AGENT":
|
|
193
|
+
repair_iterations = max(repair_iterations, _nonnegative_int(event.get("repair_iterations")))
|
|
194
|
+
implementation_merge_required = (
|
|
195
|
+
implementation_merge_required or event_step == "WAIT_FOR_OPERATOR_MERGE"
|
|
196
|
+
)
|
|
197
|
+
finalization_merge_required = (
|
|
198
|
+
finalization_merge_required or event_step == "WAIT_FOR_FINALIZATION_MERGE"
|
|
199
|
+
)
|
|
200
|
+
reconciliation_merge_required = (
|
|
201
|
+
reconciliation_merge_required or event_step == "WAIT_FOR_RECONCILIATION_MERGE"
|
|
202
|
+
)
|
|
203
|
+
repair_iterations = max(repair_iterations, _nonnegative_int(checkpoint.get("repair_iterations")))
|
|
204
|
+
evidence_available = bool(events)
|
|
205
|
+
terminal_state = phase if phase in TERMINAL else None
|
|
206
|
+
try:
|
|
207
|
+
status_reconciliation_block = is_stale_rolling_status_block(
|
|
208
|
+
TransactionState.from_dict(checkpoint)
|
|
209
|
+
) and not recorded_pull_request
|
|
210
|
+
except ValueError:
|
|
211
|
+
status_reconciliation_block = False
|
|
212
|
+
# Reaching the pull-request hand-off is not evidence that the pull request
|
|
213
|
+
# was merged. A later finalization step (or a successful terminal state)
|
|
214
|
+
# is the first lifecycle evidence that can make the merge node complete.
|
|
215
|
+
implementation_merge_completed = (
|
|
216
|
+
terminal_state == "COMPLETE"
|
|
217
|
+
or "FINALIZE_AGENT" in observed
|
|
218
|
+
or "REPOSITORY_CLEANUP" in observed
|
|
219
|
+
or "IMPLEMENTATION" in verified_merge_resume_kinds
|
|
220
|
+
)
|
|
221
|
+
finalization_merge_completed = (
|
|
222
|
+
terminal_state == "COMPLETE"
|
|
223
|
+
or "REPOSITORY_CLEANUP" in observed
|
|
224
|
+
or "FINALIZATION" in verified_merge_resume_kinds
|
|
225
|
+
)
|
|
226
|
+
reconciliation_merge_completed = (
|
|
227
|
+
terminal_state == "COMPLETE"
|
|
228
|
+
or "REPOSITORY_CLEANUP" in observed
|
|
229
|
+
or "RECONCILIATION" in verified_merge_resume_kinds
|
|
230
|
+
)
|
|
231
|
+
# The managed contract permits a no-PR path and a single implementation-PR
|
|
232
|
+
# path. A terminal pre-flight block is not merge evidence. Omit boundaries
|
|
233
|
+
# that have no persisted PR evidence instead of inventing an operator wait.
|
|
234
|
+
if mode != "GENESIS":
|
|
235
|
+
path = tuple(
|
|
236
|
+
step_id for step_id in path
|
|
237
|
+
if not (
|
|
238
|
+
step_id == "WAIT_FOR_OPERATOR_MERGE"
|
|
239
|
+
and (not implementation_merge_required or status_reconciliation_block)
|
|
240
|
+
and ("FINALIZE_AGENT" in observed or "REPOSITORY_CLEANUP" in observed or terminal_state == "COMPLETE" or status_reconciliation_block)
|
|
241
|
+
)
|
|
242
|
+
and not (
|
|
243
|
+
step_id == "WAIT_FOR_FINALIZATION_MERGE"
|
|
244
|
+
and (not finalization_merge_required or status_reconciliation_block)
|
|
245
|
+
and ("REPOSITORY_CLEANUP" in observed or terminal_state == "COMPLETE" or status_reconciliation_block)
|
|
246
|
+
)
|
|
247
|
+
and not (
|
|
248
|
+
step_id == "WAIT_FOR_RECONCILIATION_MERGE"
|
|
249
|
+
and (not reconciliation_merge_required or status_reconciliation_block)
|
|
250
|
+
and ("REPOSITORY_CLEANUP" in observed or terminal_state == "COMPLETE" or status_reconciliation_block)
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
quality_phase_ids = {
|
|
254
|
+
str(phase_id) for phase_id, _, phase_name, *_ in phase_spans
|
|
255
|
+
if phase_name == "QUALITY_CONTROL"
|
|
256
|
+
}
|
|
257
|
+
repair_phase_ids = {
|
|
258
|
+
str(phase_id) for phase_id, _, phase_name, *_ in phase_spans
|
|
259
|
+
if phase_name == "REPAIR"
|
|
260
|
+
}
|
|
261
|
+
parent_by_phase_id = {
|
|
262
|
+
str(phase_id): str(parent_phase_id)
|
|
263
|
+
for phase_id, parent_phase_id, *_ in phase_spans
|
|
264
|
+
if parent_phase_id is not None
|
|
265
|
+
}
|
|
266
|
+
metadata_by_phase_id = {
|
|
267
|
+
str(phase_id): _checkpoint(metadata)
|
|
268
|
+
for phase_id, *_, metadata in phase_spans
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
def is_nested_below(parent_phase_id: object, owner_phase_ids: set[str]) -> bool:
|
|
272
|
+
"""Return whether a span is nested below a visible lifecycle owner."""
|
|
273
|
+
phase_id = str(parent_phase_id) if parent_phase_id is not None else None
|
|
274
|
+
seen: set[str] = set()
|
|
275
|
+
while phase_id and phase_id not in seen:
|
|
276
|
+
if phase_id in owner_phase_ids:
|
|
277
|
+
return True
|
|
278
|
+
seen.add(phase_id)
|
|
279
|
+
phase_id = parent_by_phase_id.get(phase_id)
|
|
280
|
+
return False
|
|
281
|
+
|
|
282
|
+
def is_local_validation_phase(phase_id: object, parent_phase_id: object) -> bool:
|
|
283
|
+
"""Return whether a span belongs to the local-validation invocation."""
|
|
284
|
+
current_id = str(phase_id)
|
|
285
|
+
current_parent = parent_phase_id
|
|
286
|
+
seen: set[str] = set()
|
|
287
|
+
while current_id and current_id not in seen:
|
|
288
|
+
if metadata_by_phase_id.get(current_id, {}).get("lifecycle_step") == "LOCAL_REPOSITORY_VALIDATION":
|
|
289
|
+
return True
|
|
290
|
+
seen.add(current_id)
|
|
291
|
+
current_id = str(current_parent) if current_parent is not None else ""
|
|
292
|
+
current_parent = parent_by_phase_id.get(current_id)
|
|
293
|
+
return False
|
|
294
|
+
|
|
295
|
+
def belongs_to_step(step_id: str, phase_id: object, parent_phase_id: object, phase_name: object) -> bool:
|
|
296
|
+
"""Keep nested provider timing with its visible lifecycle owner."""
|
|
297
|
+
name = str(phase_name)
|
|
298
|
+
nested_in_quality = is_nested_below(parent_phase_id, quality_phase_ids)
|
|
299
|
+
nested_in_repair = is_nested_below(parent_phase_id, repair_phase_ids)
|
|
300
|
+
local_validation = is_local_validation_phase(phase_id, parent_phase_id)
|
|
301
|
+
if step_id == "EXECUTE_AGENT":
|
|
302
|
+
return name in _STEP_PHASES[step_id] and not nested_in_quality and not nested_in_repair and not local_validation
|
|
303
|
+
if step_id == "LOCAL_REPOSITORY_VALIDATION":
|
|
304
|
+
return name in _STEP_PHASES[step_id] and local_validation
|
|
305
|
+
if step_id == "QUALITY_CONTROL_AGENT":
|
|
306
|
+
return name == "QUALITY_CONTROL" or nested_in_quality
|
|
307
|
+
if step_id in {"REPAIR_AGENT", "FINALIZATION_REPAIR_AGENT"}:
|
|
308
|
+
return name == "REPAIR" or nested_in_repair
|
|
309
|
+
return name in _STEP_PHASES.get(step_id, frozenset())
|
|
310
|
+
|
|
311
|
+
steps: list[dict[str, object]] = []
|
|
312
|
+
for order, step_id in enumerate(path):
|
|
313
|
+
state = "PENDING"
|
|
314
|
+
step: dict[str, object] = {
|
|
315
|
+
"id": step_id,
|
|
316
|
+
"order": order,
|
|
317
|
+
"presentation_key": (
|
|
318
|
+
"lifecycle.step.autonomous_quality_repair"
|
|
319
|
+
if mode == "GENESIS" and step_id == "REPAIR_AGENT"
|
|
320
|
+
else
|
|
321
|
+
"lifecycle.step.repair_agent"
|
|
322
|
+
if step_id == "FINALIZATION_REPAIR_AGENT"
|
|
323
|
+
else f"lifecycle.step.{step_id.lower()}"
|
|
324
|
+
),
|
|
325
|
+
"state": state,
|
|
326
|
+
}
|
|
327
|
+
if step_id == "START":
|
|
328
|
+
step["state"] = "START"
|
|
329
|
+
elif step_id == "TERMINAL":
|
|
330
|
+
step["state"] = terminal_state or "PENDING"
|
|
331
|
+
if terminal_state:
|
|
332
|
+
step["terminal_outcome"] = terminal_state
|
|
333
|
+
elif step_id in {"WAIT_FOR_OPERATOR_MERGE", "WAIT_FOR_FINALIZATION_MERGE", "WAIT_FOR_RECONCILIATION_MERGE"} and step_id in observed:
|
|
334
|
+
step.update(observed[step_id])
|
|
335
|
+
merge_completed = (
|
|
336
|
+
implementation_merge_completed
|
|
337
|
+
if step_id == "WAIT_FOR_OPERATOR_MERGE"
|
|
338
|
+
else finalization_merge_completed if step_id == "WAIT_FOR_FINALIZATION_MERGE" else reconciliation_merge_completed
|
|
339
|
+
)
|
|
340
|
+
if merge_completed:
|
|
341
|
+
step["state"] = "COMPLETED"
|
|
342
|
+
elif display_phase == step_id and terminal_state is None:
|
|
343
|
+
step["state"] = "ACTIVE"
|
|
344
|
+
else:
|
|
345
|
+
# The PR exists, but it has not been merged. In particular,
|
|
346
|
+
# bounded validation repair must leave the merge visibly
|
|
347
|
+
# blocked and must not render a completion checkmark.
|
|
348
|
+
step["state"] = "BLOCKED"
|
|
349
|
+
if step_id == "WAIT_FOR_OPERATOR_MERGE" and display_phase == "REPAIR_AGENT":
|
|
350
|
+
step["action_key"] = "state.repair_bounded_validation_failure"
|
|
351
|
+
elif step_id in observed:
|
|
352
|
+
step.update(observed[step_id])
|
|
353
|
+
step["state"] = "ACTIVE" if display_phase == step_id and terminal_state is None else "COMPLETED"
|
|
354
|
+
elif step_id == display_phase and terminal_state is None:
|
|
355
|
+
step["state"] = "ACTIVE"
|
|
356
|
+
if (
|
|
357
|
+
step_id == "WAIT_FOR_FINALIZATION_MERGE"
|
|
358
|
+
and display_phase == "FINALIZATION_REPAIR_AGENT"
|
|
359
|
+
and terminal_state is None
|
|
360
|
+
):
|
|
361
|
+
step["state"] = "BLOCKED"
|
|
362
|
+
step["action_key"] = "state.repair_bounded_validation_failure"
|
|
363
|
+
if (
|
|
364
|
+
transaction_kind == "RECONCILIATION"
|
|
365
|
+
and step_id in {"EXECUTE_AGENT", "LOCAL_REPOSITORY_VALIDATION", "QUALITY_CONTROL_AGENT", "REPAIR_AGENT", "WAIT_FOR_OPERATOR_MERGE", "FINALIZE_AGENT", "WAIT_FOR_FINALIZATION_MERGE"}
|
|
366
|
+
and step_id not in observed
|
|
367
|
+
):
|
|
368
|
+
step["state"] = "SKIPPED"
|
|
369
|
+
step["presentation_detail_key"] = "lifecycle.detail.not_part_of_reconciliation"
|
|
370
|
+
if step_id in {"REPAIR_AGENT", "FINALIZATION_REPAIR_AGENT"} and repair_iterations:
|
|
371
|
+
step["iteration_count"] = repair_iterations
|
|
372
|
+
if step_id in {"REPAIR_AGENT", "FINALIZATION_REPAIR_AGENT"} and step["state"] not in {"PENDING", "SKIPPED"}:
|
|
373
|
+
step["repair_evidence_key"] = (
|
|
374
|
+
"lifecycle.detail_autonomous_quality_repair_evidence"
|
|
375
|
+
if mode == "GENESIS" and step_id == "REPAIR_AGENT"
|
|
376
|
+
else "lifecycle.detail_repair_evidence"
|
|
377
|
+
)
|
|
378
|
+
audit = checkpoint.get("repair_audit")
|
|
379
|
+
if isinstance(audit, (list, tuple)) and audit:
|
|
380
|
+
step["repair_audit"] = list(audit)
|
|
381
|
+
if step_id == "QUALITY_CONTROL_AGENT" and step["state"] not in {"PENDING", "SKIPPED"}:
|
|
382
|
+
evidence = checkpoint.get("quality_evidence")
|
|
383
|
+
if isinstance(evidence, (list, tuple)) and evidence:
|
|
384
|
+
step["quality_evidence"] = list(evidence)
|
|
385
|
+
if step_id == "LOCAL_REPOSITORY_VALIDATION" and step["state"] not in {"PENDING", "SKIPPED"}:
|
|
386
|
+
audit = checkpoint.get("local_validation_audit")
|
|
387
|
+
if isinstance(audit, (list, tuple)) and audit:
|
|
388
|
+
step["repair_audit"] = list(audit)
|
|
389
|
+
step["iteration_count"] = checkpoint.get("local_validation_iterations", 0)
|
|
390
|
+
spans = [
|
|
391
|
+
{
|
|
392
|
+
"phase": phase_name,
|
|
393
|
+
"attempt": attempt,
|
|
394
|
+
"started_at": started_at,
|
|
395
|
+
"finished_at": completed_at,
|
|
396
|
+
"duration_ms": duration_ms,
|
|
397
|
+
"outcome": outcome,
|
|
398
|
+
}
|
|
399
|
+
for phase_id, parent_phase_id, phase_name, attempt, started_at, completed_at, duration_ms, outcome, _ in phase_spans
|
|
400
|
+
if belongs_to_step(step_id, phase_id, parent_phase_id, phase_name)
|
|
401
|
+
]
|
|
402
|
+
# Timing is supporting evidence, not lifecycle authority. In
|
|
403
|
+
# particular, admission can record a short reconciliation span before
|
|
404
|
+
# the end-reconciliation lifecycle step is ever reached. Do not let
|
|
405
|
+
# that span invent start/end timestamps or a completed phase for a
|
|
406
|
+
# pending (or explicitly skipped) step.
|
|
407
|
+
if spans and step["state"] not in {"PENDING", "SKIPPED"}:
|
|
408
|
+
step["timing"] = {
|
|
409
|
+
"started_at": min(str(span["started_at"]) for span in spans if span["started_at"]),
|
|
410
|
+
"finished_at": max((str(span["finished_at"]) for span in spans if span["finished_at"]), default=None),
|
|
411
|
+
"spans": spans,
|
|
412
|
+
}
|
|
413
|
+
steps.append(step)
|
|
414
|
+
recovery = None
|
|
415
|
+
if status_reconciliation_block:
|
|
416
|
+
recovery = {"kind": "status_reconciliation", "run_id": run_id}
|
|
417
|
+
else:
|
|
418
|
+
# Provider recovery is durable SQLite evidence. Do not consult the
|
|
419
|
+
# historical checkpoint ledger here: it can be stale after a host
|
|
420
|
+
# interruption, whereas this row is atomically advanced with the
|
|
421
|
+
# replacement provider receipt.
|
|
422
|
+
provider_recovery = load_recovery_state(root, run_id)
|
|
423
|
+
if provider_recovery is not None:
|
|
424
|
+
recovery = {
|
|
425
|
+
"kind": "provider_interruption",
|
|
426
|
+
"state": provider_recovery.get("state"),
|
|
427
|
+
"result": provider_recovery.get("result"),
|
|
428
|
+
"triggering_invocation_id": provider_recovery.get("triggering_invocation_id"),
|
|
429
|
+
"replacement_invocation_id": provider_recovery.get("replacement_invocation_id"),
|
|
430
|
+
"recovery_ordinal": provider_recovery.get("recovery_ordinal"),
|
|
431
|
+
"maximum_attempts": provider_recovery.get("maximum_attempts"),
|
|
432
|
+
"lifecycle_phase": provider_recovery.get("lifecycle_phase"),
|
|
433
|
+
}
|
|
434
|
+
qualification = load_run_qualification_snapshot(root, run_id) or {}
|
|
435
|
+
return {
|
|
436
|
+
"run_id": run_id,
|
|
437
|
+
"execution_mode": mode,
|
|
438
|
+
"available": evidence_available,
|
|
439
|
+
"terminal_state": terminal_state,
|
|
440
|
+
"current_step": display_phase if display_phase in path else None,
|
|
441
|
+
"steps": steps,
|
|
442
|
+
"recovery": recovery,
|
|
443
|
+
"qualification": {
|
|
444
|
+
"required_validation_state": qualification.get("required_validation_state", "UNAVAILABLE"),
|
|
445
|
+
"run_qualification": qualification.get("run_qualification", "UNAVAILABLE"),
|
|
446
|
+
},
|
|
447
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Provider-neutral execution domain models shared across host components."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class RepositoryEvidence:
|
|
9
|
+
repository: str
|
|
10
|
+
branch: str
|
|
11
|
+
head_sha: str
|
|
12
|
+
clean: bool
|
|
13
|
+
main_contains_head: bool = False
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class PullRequestEvidence:
|
|
18
|
+
number: int
|
|
19
|
+
state: str
|
|
20
|
+
checks_terminal: bool
|
|
21
|
+
checks_passed: bool
|
|
22
|
+
merge_commit: str | None = None
|
|
23
|
+
is_draft: bool = False
|
|
24
|
+
failed_checks: tuple[str, ...] = ()
|
|
25
|
+
head_branch: str | None = None
|
|
26
|
+
base_branch: str | None = None
|
|
27
|
+
merge_state_status: str | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class AgentResult:
|
|
32
|
+
terminal_state: str
|
|
33
|
+
branch: str | None = None
|
|
34
|
+
pull_request: int | None = None
|
|
35
|
+
terminal_condition: str = "repository_reconciled"
|
|
36
|
+
diagnostic: str | None = None
|
|
37
|
+
repository_path: str | None = None
|
|
38
|
+
commit_sha: str | None = None
|
|
39
|
+
validation_evidence: tuple[dict[str, str], ...] = ()
|
|
40
|
+
quality_evidence: tuple[dict[str, str], ...] = ()
|
|
41
|
+
# A failed validation never becomes a pass. This merely tells the host
|
|
42
|
+
# when a separate validation-environment recovery is warranted.
|
|
43
|
+
validation_disposition: str = "product_failure"
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Typed readiness profiles selected before an Execution Host lifecycle starts."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Callable
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Requirement(StrEnum):
|
|
12
|
+
REQUIRED = "REQUIRED"
|
|
13
|
+
OPTIONAL = "OPTIONAL"
|
|
14
|
+
NOT_APPLICABLE = "NOT_APPLICABLE"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class ReadinessProfile:
|
|
19
|
+
profile_id: str
|
|
20
|
+
version: int
|
|
21
|
+
execution_mode: str
|
|
22
|
+
repository: Requirement
|
|
23
|
+
remote: Requirement
|
|
24
|
+
upstream: Requirement
|
|
25
|
+
clean_worktree: Requirement
|
|
26
|
+
branch: Requirement
|
|
27
|
+
workspace_authorization: Requirement
|
|
28
|
+
host_qualification: Requirement
|
|
29
|
+
capability_qualification: Requirement
|
|
30
|
+
providers: Requirement
|
|
31
|
+
datastore: Requirement
|
|
32
|
+
active_run_lease: Requirement
|
|
33
|
+
producer_contract: Requirement
|
|
34
|
+
additional_constraints: tuple[str, ...] = ()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
PLATFORM_HOST = ReadinessProfile("platform_host", 1, "ANY", Requirement.NOT_APPLICABLE, Requirement.NOT_APPLICABLE, Requirement.NOT_APPLICABLE, Requirement.NOT_APPLICABLE, Requirement.NOT_APPLICABLE, Requirement.NOT_APPLICABLE, Requirement.REQUIRED, Requirement.NOT_APPLICABLE, Requirement.REQUIRED, Requirement.REQUIRED, Requirement.NOT_APPLICABLE, Requirement.NOT_APPLICABLE)
|
|
38
|
+
MANAGED_REPOSITORY = ReadinessProfile("managed_repository", 1, "MANAGED", Requirement.REQUIRED, Requirement.REQUIRED, Requirement.REQUIRED, Requirement.REQUIRED, Requirement.REQUIRED, Requirement.REQUIRED, Requirement.REQUIRED, Requirement.REQUIRED, Requirement.REQUIRED, Requirement.REQUIRED, Requirement.REQUIRED, Requirement.REQUIRED)
|
|
39
|
+
GENESIS_TARGET = ReadinessProfile("genesis_target", 1, "GENESIS", Requirement.REQUIRED, Requirement.NOT_APPLICABLE, Requirement.NOT_APPLICABLE, Requirement.REQUIRED, Requirement.REQUIRED, Requirement.REQUIRED, Requirement.REQUIRED, Requirement.REQUIRED, Requirement.REQUIRED, Requirement.REQUIRED, Requirement.REQUIRED, Requirement.REQUIRED)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class ReadinessResult:
|
|
44
|
+
profile: ReadinessProfile
|
|
45
|
+
ready: bool
|
|
46
|
+
diagnostic: str | None = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class ReadinessFacts:
|
|
51
|
+
host_ready: bool
|
|
52
|
+
repository_present: bool
|
|
53
|
+
repository_clean: bool | None
|
|
54
|
+
lease_available: bool
|
|
55
|
+
remote_present: bool | None = None
|
|
56
|
+
upstream_present: bool | None = None
|
|
57
|
+
branch_present: bool | None = None
|
|
58
|
+
workspace_authorized: bool | None = None
|
|
59
|
+
capabilities_available: bool | None = None
|
|
60
|
+
providers_available: bool | None = None
|
|
61
|
+
datastore_healthy: bool | None = None
|
|
62
|
+
producer_contract_valid: bool | None = None
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def from_preflight(cls, *, host: object, workspace: object, capability: object, lease_available: bool) -> "ReadinessFacts":
|
|
66
|
+
"""Adapt already-observed preflight outcomes without performing any probe."""
|
|
67
|
+
def field(subject: object, name: str, default: object = None) -> object:
|
|
68
|
+
return subject.get(name, default) if isinstance(subject, dict) else getattr(subject, name, default)
|
|
69
|
+
|
|
70
|
+
def outcome(subject: object, identifier: str) -> bool | None:
|
|
71
|
+
for item in field(subject, "checks", ()):
|
|
72
|
+
if field(item, "identifier") == identifier:
|
|
73
|
+
return field(item, "outcome") == "PASS"
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
host_ready = field(host, "outcome") == "PASS"
|
|
77
|
+
repository_present = outcome(workspace, "target_repository") is True
|
|
78
|
+
staged, unstaged, untracked = (outcome(workspace, identifier) for identifier in ("worktree_staged", "worktree_unstaged", "worktree_untracked"))
|
|
79
|
+
repository_clean = (
|
|
80
|
+
outcome(workspace, "clean_worktree")
|
|
81
|
+
if None in {staged, unstaged, untracked}
|
|
82
|
+
else all((staged, unstaged, untracked))
|
|
83
|
+
)
|
|
84
|
+
capabilities_ready = field(capability, "outcome") == "PASS"
|
|
85
|
+
return cls(
|
|
86
|
+
host_ready,
|
|
87
|
+
repository_present,
|
|
88
|
+
repository_clean,
|
|
89
|
+
lease_available,
|
|
90
|
+
remote_present=outcome(workspace, "managed_remote"),
|
|
91
|
+
upstream_present=outcome(workspace, "managed_synchronization"),
|
|
92
|
+
branch_present=outcome(workspace, "managed_expected_branch")
|
|
93
|
+
if outcome(workspace, "managed_expected_branch") is not None
|
|
94
|
+
else outcome(workspace, "genesis_local_repository"),
|
|
95
|
+
workspace_authorized=outcome(workspace, "WORKSPACE_TARGET_AUTHORIZED"),
|
|
96
|
+
capabilities_available=capabilities_ready,
|
|
97
|
+
providers_available=outcome(capability, "provider_support"),
|
|
98
|
+
datastore_healthy=outcome(host, "telemetry_storage"),
|
|
99
|
+
producer_contract_valid=outcome(capability, "producer_contract"),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass(frozen=True)
|
|
104
|
+
class ReadinessDecision:
|
|
105
|
+
passed: bool
|
|
106
|
+
profile_id: str
|
|
107
|
+
profile_version: int
|
|
108
|
+
execution_mode: str
|
|
109
|
+
failed_requirements: tuple[str, ...]
|
|
110
|
+
facts: ReadinessFacts
|
|
111
|
+
evaluated_at: str
|
|
112
|
+
diagnostic: str | None = None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def selected_profile(execution_mode: str) -> ReadinessProfile:
|
|
116
|
+
if execution_mode == "GENESIS":
|
|
117
|
+
return GENESIS_TARGET
|
|
118
|
+
if execution_mode == "MANAGED":
|
|
119
|
+
return MANAGED_REPOSITORY
|
|
120
|
+
raise ValueError(f"Unsupported execution readiness mode: {execution_mode}")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def evaluate(
|
|
124
|
+
profile: ReadinessProfile,
|
|
125
|
+
*,
|
|
126
|
+
host_root: Path,
|
|
127
|
+
target_root: Path | None,
|
|
128
|
+
managed_clean: Callable[[Path], bool],
|
|
129
|
+
genesis_preflight: Callable[[Path | None], str | None],
|
|
130
|
+
) -> ReadinessResult:
|
|
131
|
+
"""Evaluate only the selected profile; never mix Genesis and Managed checks."""
|
|
132
|
+
if profile.profile_id == PLATFORM_HOST.profile_id:
|
|
133
|
+
return ReadinessResult(profile, host_root.is_dir(), None if host_root.is_dir() else "Execution Host repository is unavailable.")
|
|
134
|
+
if profile.profile_id == GENESIS_TARGET.profile_id:
|
|
135
|
+
diagnostic = genesis_preflight(target_root)
|
|
136
|
+
return ReadinessResult(profile, diagnostic is None, diagnostic)
|
|
137
|
+
if managed_clean(host_root):
|
|
138
|
+
return ReadinessResult(profile, True)
|
|
139
|
+
return ReadinessResult(profile, False, "working tree is not clean; unrelated work will not be touched")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def decide(profile: ReadinessProfile, facts: ReadinessFacts) -> ReadinessDecision:
|
|
143
|
+
"""Fail-closed policy evaluation over facts; it never probes the environment."""
|
|
144
|
+
failed: list[str] = []
|
|
145
|
+
requirements = (
|
|
146
|
+
("host_qualification", profile.host_qualification, facts.host_ready),
|
|
147
|
+
("repository", profile.repository, facts.repository_present),
|
|
148
|
+
("remote", profile.remote, facts.remote_present),
|
|
149
|
+
("upstream", profile.upstream, facts.upstream_present),
|
|
150
|
+
("clean_worktree", profile.clean_worktree, facts.repository_clean),
|
|
151
|
+
("branch", profile.branch, facts.branch_present),
|
|
152
|
+
("workspace_authorization", profile.workspace_authorization, facts.workspace_authorized),
|
|
153
|
+
("capability_qualification", profile.capability_qualification, facts.capabilities_available),
|
|
154
|
+
("providers", profile.providers, facts.providers_available),
|
|
155
|
+
("datastore", profile.datastore, facts.datastore_healthy),
|
|
156
|
+
("active_run_lease", profile.active_run_lease, facts.lease_available),
|
|
157
|
+
("producer_contract", profile.producer_contract, facts.producer_contract_valid),
|
|
158
|
+
)
|
|
159
|
+
for identifier, requirement, observed in requirements:
|
|
160
|
+
if requirement is Requirement.REQUIRED and observed is not True:
|
|
161
|
+
failed.append(identifier)
|
|
162
|
+
return ReadinessDecision(
|
|
163
|
+
not failed, profile.profile_id, profile.version, profile.execution_mode,
|
|
164
|
+
tuple(failed), facts, datetime.now(timezone.utc).isoformat(),
|
|
165
|
+
None if not failed else "Readiness requirements are not satisfied: " + ", ".join(failed),
|
|
166
|
+
)
|