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.
Files changed (130) hide show
  1. engineering_platform/ENGINEERING_PLATFORM_CONFIG.json +32 -0
  2. engineering_platform/ENGINEERING_PLATFORM_VERSION.json +15 -0
  3. engineering_platform/__init__.py +1 -0
  4. engineering_platform/__main__.py +7 -0
  5. engineering_platform/agent_state.py +530 -0
  6. engineering_platform/agent_trust.py +174 -0
  7. engineering_platform/assets/dashboard.css +1317 -0
  8. engineering_platform/assets/dashboard.js +8534 -0
  9. engineering_platform/assets/dashboard_locales.mjs +4049 -0
  10. engineering_platform/assets/dashboard_status_store.mjs +41 -0
  11. engineering_platform/assets/operations-console/apple-touch-icon-dark.png +0 -0
  12. engineering_platform/assets/operations-console/apple-touch-icon-light.png +0 -0
  13. engineering_platform/assets/operations-console/icon-dark.png +0 -0
  14. engineering_platform/assets/operations-console/icon-light.png +0 -0
  15. engineering_platform/assets/operations-console/icon-transparent.png +0 -0
  16. engineering_platform/assets/operations-console/manifest.webmanifest +11 -0
  17. engineering_platform/capability_preflight.py +285 -0
  18. engineering_platform/capability_review.py +261 -0
  19. engineering_platform/central_data_transfer.py +195 -0
  20. engineering_platform/central_database.py +245 -0
  21. engineering_platform/central_store_migration.py +1672 -0
  22. engineering_platform/codex_capacity.py +81 -0
  23. engineering_platform/codex_chat.py +226 -0
  24. engineering_platform/codex_observability.py +153 -0
  25. engineering_platform/component_lock.py +40 -0
  26. engineering_platform/component_logging.py +420 -0
  27. engineering_platform/console_presentation.py +14 -0
  28. engineering_platform/console_route_ownership.py +83 -0
  29. engineering_platform/contracts/__init__.py +38 -0
  30. engineering_platform/contracts/ep_consumer.py +391 -0
  31. engineering_platform/contracts/models.py +105 -0
  32. engineering_platform/contracts/projection.py +401 -0
  33. engineering_platform/dashboard_browser_validation.py +206 -0
  34. engineering_platform/dashboard_state.py +630 -0
  35. engineering_platform/dashboard_supervisor.swift +105 -0
  36. engineering_platform/dashboard_translation.py +129 -0
  37. engineering_platform/dependabot_producer.py +349 -0
  38. engineering_platform/drift_diagnostics.py +144 -0
  39. engineering_platform/emergency_recovery.py +268 -0
  40. engineering_platform/engineering_memory.py +139 -0
  41. engineering_platform/ep_consumer_credentials.py +473 -0
  42. engineering_platform/evidence_projection.py +213 -0
  43. engineering_platform/execution_activity.py +218 -0
  44. engineering_platform/execution_context.py +132 -0
  45. engineering_platform/execution_errors.py +42 -0
  46. engineering_platform/execution_evidence.py +24 -0
  47. engineering_platform/execution_executor.py +730 -0
  48. engineering_platform/execution_finalization.py +44 -0
  49. engineering_platform/execution_host.py +3306 -0
  50. engineering_platform/execution_lease.py +365 -0
  51. engineering_platform/execution_lifecycle.py +447 -0
  52. engineering_platform/execution_models.py +43 -0
  53. engineering_platform/execution_readiness.py +166 -0
  54. engineering_platform/execution_reporting.py +1607 -0
  55. engineering_platform/execution_repository.py +253 -0
  56. engineering_platform/execution_timeout_policy.py +56 -0
  57. engineering_platform/execution_timing.py +440 -0
  58. engineering_platform/execution_transaction.py +28 -0
  59. engineering_platform/external_producer_binding.py +235 -0
  60. engineering_platform/file_inbox.py +249 -0
  61. engineering_platform/forensic_attribution.py +338 -0
  62. engineering_platform/forensic_attribution_v2.py +134 -0
  63. engineering_platform/forensic_delta.py +299 -0
  64. engineering_platform/golden_scenario.py +63 -0
  65. engineering_platform/historical_dashboard_configuration.py +171 -0
  66. engineering_platform/host_admin.py +199 -0
  67. engineering_platform/host_preflight.py +231 -0
  68. engineering_platform/installation_relocation.py +122 -0
  69. engineering_platform/investigation_ledger.py +89 -0
  70. engineering_platform/legacy_inbox_migration.py +79 -0
  71. engineering_platform/lifecycle_worker.py +223 -0
  72. engineering_platform/live_status.py +267 -0
  73. engineering_platform/local_api.py +209 -0
  74. engineering_platform/local_api_keychain.py +51 -0
  75. engineering_platform/local_repository_binding.py +138 -0
  76. engineering_platform/managed_autonomy.py +509 -0
  77. engineering_platform/managed_codex_runtime.py +105 -0
  78. engineering_platform/parity_context.py +203 -0
  79. engineering_platform/parity_lifecycle_dispatcher.py +488 -0
  80. engineering_platform/platform_admin.py +13 -0
  81. engineering_platform/platform_api.py +428 -0
  82. engineering_platform/platform_bootstrap.py +385 -0
  83. engineering_platform/platform_components.py +65 -0
  84. engineering_platform/platform_version.py +171 -0
  85. engineering_platform/pr_check_repair.py +276 -0
  86. engineering_platform/pr_evidence_backfill.py +278 -0
  87. engineering_platform/producer.py +209 -0
  88. engineering_platform/project_agent.py +366 -0
  89. engineering_platform/project_agent_service.py +244 -0
  90. engineering_platform/project_topology.py +126 -0
  91. engineering_platform/prompt_history.py +591 -0
  92. engineering_platform/provider_context.py +136 -0
  93. engineering_platform/provider_context_benchmark.py +41 -0
  94. engineering_platform/provider_context_scope.py +90 -0
  95. engineering_platform/provider_interruption.py +168 -0
  96. engineering_platform/provider_process_identity.py +80 -0
  97. engineering_platform/provider_readiness.py +138 -0
  98. engineering_platform/provider_recovery.py +647 -0
  99. engineering_platform/provider_usage.py +497 -0
  100. engineering_platform/providers.py +471 -0
  101. engineering_platform/qualification.py +220 -0
  102. engineering_platform/recommendation_handoff.py +238 -0
  103. engineering_platform/report_analysis.py +193 -0
  104. engineering_platform/repository_attachment.py +171 -0
  105. engineering_platform/repository_handoff.py +95 -0
  106. engineering_platform/resources.py +38 -0
  107. engineering_platform/reviewer_evidence.py +70 -0
  108. engineering_platform/schemas/repository-attachment.schema.json +61 -0
  109. engineering_platform/server.py +3679 -0
  110. engineering_platform/server_console_services.py +2024 -0
  111. engineering_platform/server_relay.py +172 -0
  112. engineering_platform/server_service.py +122 -0
  113. engineering_platform/status_model.py +135 -0
  114. engineering_platform/status_reconciliation.py +34 -0
  115. engineering_platform/storage.py +2440 -0
  116. engineering_platform/submission_cli.py +77 -0
  117. engineering_platform/submission_intake.py +45 -0
  118. engineering_platform/submission_service.py +317 -0
  119. engineering_platform/telemetry.py +951 -0
  120. engineering_platform/templates/workspace-config.json +25 -0
  121. engineering_platform/validation_identity.py +50 -0
  122. engineering_platform/validation_profile.py +211 -0
  123. engineering_platform/workspace_preflight.py +263 -0
  124. engineering_platform/worktree_provenance.py +147 -0
  125. engineering_platform/worktree_tooling.py +18 -0
  126. engineering_platform-2.2.0.dist-info/METADATA +18 -0
  127. engineering_platform-2.2.0.dist-info/RECORD +130 -0
  128. engineering_platform-2.2.0.dist-info/WHEEL +5 -0
  129. engineering_platform-2.2.0.dist-info/entry_points.txt +6 -0
  130. engineering_platform-2.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,440 @@
1
+ """Canonical Engineering Platform execution phase timing evidence.
2
+
3
+ This module deliberately owns measurement, persistence and deterministic
4
+ projection only. It has no Mission, Producer, retry or lifecycle authority.
5
+ Wall-clock UTC timestamps describe when a span occurred; a monotonic duration
6
+ is captured at its runtime boundary. Repeated and nested spans stay separate
7
+ in SQLite. Aggregates use only top-level spans, so nested work is never
8
+ double-counted.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass
13
+ from datetime import datetime, timezone
14
+ import json
15
+ from pathlib import Path
16
+ import sqlite3
17
+ from time import monotonic
18
+ from typing import Mapping
19
+ import uuid
20
+
21
+ from .storage import EngineeringStorageError, open_storage
22
+
23
+
24
+ PHASES = frozenset({
25
+ "QUEUE_WAIT", "SUBMISSION_CLAIM", "INITIALIZATION", "HOST_PREFLIGHT",
26
+ "WORKSPACE_PREFLIGHT", "CAPABILITY_PREFLIGHT", "DETERMINISTIC_ADMISSION", "CAPABILITY_REVIEW", "EXECUTION_PREPARATION",
27
+ "PROVIDER_EXECUTION", "VALIDATION", "QUALITY_CONTROL", "REPAIR", "REPOSITORY_FINALIZATION",
28
+ "PR_OR_MERGE", "FINALIZATION", "REPORT_GENERATION", "EVIDENCE_PERSISTENCE",
29
+ "REPOSITORY_CLEANUP", "RECONCILIATION", "EXTERNAL_CI_WAIT", "TOTAL_EXECUTION",
30
+ })
31
+ TERMINAL_OUTCOMES = frozenset({"COMPLETE", "FAILED", "INTERRUPTED", "STALE"})
32
+ _MAX_METADATA_BYTES = 2048
33
+
34
+
35
+ def _connection(root: Path, central_database: Path | None = None) -> sqlite3.Connection:
36
+ """Open timing storage from an explicit CENTRAL binding when supplied."""
37
+ if central_database is None:
38
+ return open_storage(root)
39
+ database = central_database.resolve()
40
+ if not database.is_file():
41
+ raise EngineeringStorageError("CENTRAL timing database is unavailable.")
42
+ connection = sqlite3.connect(database, isolation_level=None)
43
+ connection.execute("PRAGMA foreign_keys=ON")
44
+ connection.execute("PRAGMA busy_timeout=10000")
45
+ return connection
46
+
47
+
48
+ def _utc(value: datetime | None = None) -> str:
49
+ value = value or datetime.now(timezone.utc)
50
+ return (value.astimezone(timezone.utc) if value.tzinfo else value.replace(tzinfo=timezone.utc)).isoformat()
51
+
52
+
53
+ def _metadata(value: Mapping[str, object] | None) -> str:
54
+ if value is None:
55
+ return "{}"
56
+ if not isinstance(value, Mapping):
57
+ raise EngineeringStorageError("Execution phase metadata must be a mapping.")
58
+ encoded = json.dumps(dict(value), sort_keys=True, separators=(",", ":"))
59
+ if len(encoded.encode()) > _MAX_METADATA_BYTES:
60
+ raise EngineeringStorageError("Execution phase metadata exceeds its bounded limit.")
61
+ return encoded
62
+
63
+
64
+ @dataclass(frozen=True)
65
+ class ActivePhase:
66
+ run_id: str
67
+ phase_id: str
68
+ started_monotonic: float | None
69
+ central_database: Path | None = None
70
+
71
+
72
+ def start_phase(root: Path, run_id: str, phase_name: str, *, category: str | None = None,
73
+ parent_phase_id: str | None = None, attempt: int = 1,
74
+ metadata: Mapping[str, object] | None = None, started_at: datetime | None = None,
75
+ monotonic_clock: float | None = None, central_database: Path | None = None) -> ActivePhase:
76
+ """Persist a real active phase boundary and return its monotonic handle."""
77
+ if phase_name not in PHASES or not run_id or attempt < 1:
78
+ raise EngineeringStorageError("Execution phase identity is invalid.")
79
+ phase_id = f"phase-{uuid.uuid4()}"
80
+ connection = _connection(root, central_database)
81
+ try:
82
+ ordinal = connection.execute(
83
+ "SELECT COALESCE(MAX(ordinal), 0) + 1 FROM execution_phase_spans WHERE run_id=?", (run_id,)
84
+ ).fetchone()[0]
85
+ connection.execute(
86
+ "INSERT INTO execution_phase_spans(phase_id,run_id,phase_name,phase_category,parent_phase_id,attempt,ordinal,started_at,outcome,metadata) VALUES(?,?,?,?,?,?,?,?,?,?)",
87
+ (phase_id, run_id, phase_name, category or phase_name, parent_phase_id, attempt, ordinal,
88
+ _utc(started_at), "ACTIVE", _metadata(metadata)),
89
+ )
90
+ finally:
91
+ connection.close()
92
+ return ActivePhase(run_id, phase_id, monotonic() if monotonic_clock is None else monotonic_clock, central_database)
93
+
94
+
95
+ def complete_phase(root: Path, active: ActivePhase, *, outcome: str = "COMPLETE",
96
+ completed_at: datetime | None = None, monotonic_clock: float | None = None) -> None:
97
+ """Close an active span with its directly measured monotonic duration."""
98
+ if outcome not in TERMINAL_OUTCOMES:
99
+ raise EngineeringStorageError("Execution phase outcome is invalid.")
100
+ connection = _connection(root, active.central_database)
101
+ try:
102
+ if active.started_monotonic is None:
103
+ # A phase can outlive the runner process. Its terminal boundary
104
+ # is still observable, but monotonic state is intentionally not
105
+ # fabricated across that restart.
106
+ row = connection.execute(
107
+ "SELECT started_at FROM execution_phase_spans WHERE phase_id=? AND run_id=? AND outcome='ACTIVE'",
108
+ (active.phase_id, active.run_id),
109
+ ).fetchone()
110
+ if row is None:
111
+ raise EngineeringStorageError("Execution phase is not active.")
112
+ started = datetime.fromisoformat(str(row[0]).replace("Z", "+00:00"))
113
+ ended = completed_at or datetime.now(timezone.utc)
114
+ duration_ms = max(0, round((ended - started).total_seconds() * 1000))
115
+ else:
116
+ elapsed = (monotonic() if monotonic_clock is None else monotonic_clock) - active.started_monotonic
117
+ duration_ms = max(0, round(elapsed * 1000))
118
+ changed = connection.execute(
119
+ "UPDATE execution_phase_spans SET completed_at=?,duration_ms=?,outcome=? WHERE phase_id=? AND run_id=? AND outcome='ACTIVE'",
120
+ (_utc(completed_at), duration_ms, outcome, active.phase_id, active.run_id),
121
+ ).rowcount
122
+ if changed != 1:
123
+ raise EngineeringStorageError("Execution phase is not active.")
124
+ finally:
125
+ connection.close()
126
+
127
+
128
+ def start_or_resume_phase(root: Path, run_id: str, phase_name: str, **kwargs: object) -> ActivePhase:
129
+ """Start one envelope or resume its observed active database boundary.
130
+
131
+ Only lifecycle envelopes use this. The resumed handle deliberately has no
132
+ monotonic value, so its completion is explicitly bounded by persisted UTC
133
+ timestamps rather than pretending a monotonic clock survived restart.
134
+ """
135
+ central_database = kwargs.get("central_database")
136
+ if central_database is not None and not isinstance(central_database, Path):
137
+ raise EngineeringStorageError("CENTRAL timing database is invalid.")
138
+ connection = _connection(root, central_database)
139
+ try:
140
+ row = connection.execute(
141
+ "SELECT phase_id FROM execution_phase_spans WHERE run_id=? AND phase_name=? AND outcome='ACTIVE' ORDER BY ordinal LIMIT 1",
142
+ (run_id, phase_name),
143
+ ).fetchone()
144
+ finally:
145
+ connection.close()
146
+ if row:
147
+ return ActivePhase(run_id, str(row[0]), None, central_database)
148
+ return start_phase(root, run_id, phase_name, **kwargs)
149
+
150
+
151
+ def record_phase(root: Path, run_id: str, phase_name: str, *, started_at: datetime,
152
+ completed_at: datetime, category: str | None = None,
153
+ parent_phase_id: str | None = None, attempt: int = 1,
154
+ metadata: Mapping[str, object] | None = None, outcome: str = "COMPLETE") -> str:
155
+ """Record a completed observed boundary when no monotonic handle survives.
156
+
157
+ This is reserved for cross-process boundaries such as queue submission to
158
+ claim. In-process work must use :func:`start_phase` and
159
+ :func:`complete_phase` instead.
160
+ """
161
+ active = start_phase(root, run_id, phase_name, category=category, parent_phase_id=parent_phase_id,
162
+ attempt=attempt, metadata=metadata, started_at=started_at, monotonic_clock=0)
163
+ elapsed = max(0, (completed_at - started_at).total_seconds())
164
+ complete_phase(root, active, outcome=outcome, completed_at=completed_at, monotonic_clock=elapsed)
165
+ return active.phase_id
166
+
167
+
168
+ def record_queue_wait_from_submission(root: Path, run_id: str, *, claimed_at: datetime | None = None) -> bool:
169
+ """Record the persisted submission-to-claim delay when it is observable."""
170
+ connection = open_storage(root)
171
+ try:
172
+ row = connection.execute(
173
+ "SELECT submission.received_at FROM execution_submissions AS submission JOIN execution_submission_links AS link ON link.submission_id=submission.submission_id WHERE link.run_id=?", (run_id,)
174
+ ).fetchone()
175
+ finally:
176
+ connection.close()
177
+ if not row or not isinstance(row[0], str):
178
+ return False
179
+ try:
180
+ submitted = datetime.fromisoformat(row[0].replace("Z", "+00:00"))
181
+ except ValueError:
182
+ return False
183
+ record_phase(root, run_id, "QUEUE_WAIT", started_at=submitted, completed_at=claimed_at or datetime.now(timezone.utc), category="QUEUE")
184
+ return True
185
+
186
+
187
+ def reconcile_interrupted_phases(root: Path, run_id: str, *, outcome: str = "STALE",
188
+ completed_at: datetime | None = None,
189
+ central_database: Path | None = None) -> int:
190
+ """Close observable abandoned work at reconciliation, never backdating it."""
191
+ if outcome not in {"STALE", "INTERRUPTED"}:
192
+ raise EngineeringStorageError("Interrupted phase outcome is invalid.")
193
+ connection = _connection(root, central_database)
194
+ try:
195
+ now = _utc(completed_at)
196
+ # Monotonic state cannot survive a process restart, so a reconciled
197
+ # duration is intentionally wall-clock bounded and explicitly STALE.
198
+ return connection.execute(
199
+ "UPDATE execution_phase_spans SET completed_at=?,duration_ms=MAX(0,CAST((julianday(?) - julianday(started_at))*86400000 AS INTEGER)),outcome=? WHERE run_id=? AND outcome='ACTIVE'",
200
+ (now, now, outcome, run_id),
201
+ ).rowcount
202
+ finally:
203
+ connection.close()
204
+
205
+
206
+ def complete_active_phase(
207
+ root: Path, run_id: str, phase_name: str, *, outcome: str = "COMPLETE",
208
+ central_database: Path | None = None,
209
+ ) -> bool:
210
+ """Close the one active lifecycle envelope when its owner observes completion.
211
+
212
+ Queue admission, the runner and the watcher are separate processes. The
213
+ watcher therefore owns the final total envelope boundary, after report and
214
+ evidence persistence have completed. This intentionally uses the stored
215
+ UTC boundary rather than claiming a monotonic clock crosses processes.
216
+ """
217
+ connection = _connection(root, central_database)
218
+ try:
219
+ row = connection.execute(
220
+ "SELECT phase_id FROM execution_phase_spans WHERE run_id=? AND phase_name=? AND outcome='ACTIVE' ORDER BY ordinal LIMIT 1",
221
+ (run_id, phase_name),
222
+ ).fetchone()
223
+ finally:
224
+ connection.close()
225
+ if row is None:
226
+ return False
227
+ complete_phase(root, ActivePhase(run_id, str(row[0]), None, central_database), outcome=outcome)
228
+ return True
229
+
230
+
231
+ def phase_spans(root: Path, run_id: str, *, central_database: Path | None = None) -> list[dict[str, object]]:
232
+ connection = _connection(root, central_database)
233
+ try:
234
+ rows = connection.execute(
235
+ "SELECT phase_id,phase_name,phase_category,parent_phase_id,attempt,ordinal,started_at,completed_at,duration_ms,outcome,metadata FROM execution_phase_spans WHERE run_id=? ORDER BY ordinal", (run_id,)
236
+ ).fetchall()
237
+ finally:
238
+ connection.close()
239
+ keys = ("phase_id", "phase_name", "phase_category", "parent_phase_id", "attempt", "ordinal", "started_at", "completed_at", "duration_ms", "outcome", "metadata")
240
+ result = []
241
+ for row in rows:
242
+ item = dict(zip(keys, row, strict=True))
243
+ item["metadata"] = json.loads(item["metadata"])
244
+ result.append(item)
245
+ return result
246
+
247
+
248
+ def timing_summary(root: Path, run_id: str, *, central_database: Path | None = None) -> dict[str, object]:
249
+ """Return the one canonical timing read model for a completed run.
250
+
251
+ ``phase_aggregates`` and ``longest_individual_spans`` intentionally answer
252
+ different questions. Aggregates suppress only a same-category ancestor,
253
+ so a category is represented once without inventing a critical path.
254
+ Individual spans retain every observed occurrence (apart from the total
255
+ envelope) and are ranked independently. Consumers must use these fields
256
+ rather than deriving their own bottleneck order.
257
+ """
258
+ spans = phase_spans(root, run_id, central_database=central_database)
259
+ historical_total: int | None = None
260
+ if not spans:
261
+ # Earlier runs already have a coarse immutable execution receipt. It
262
+ # remains useful total-duration evidence, but never becomes invented
263
+ # phase detail.
264
+ connection = _connection(root, central_database)
265
+ try:
266
+ row = connection.execute(
267
+ "SELECT total_execution_seconds FROM execution_runs WHERE run_id=?", (run_id,)
268
+ ).fetchone()
269
+ finally:
270
+ connection.close()
271
+ if row and isinstance(row[0], (int, float)) and not isinstance(row[0], bool) and row[0] >= 0:
272
+ historical_total = round(float(row[0]) * 1000)
273
+ completed = [span for span in spans if span["outcome"] != "ACTIVE" and isinstance(span["duration_ms"], int)]
274
+ top_level = [span for span in completed if span["parent_phase_id"] is None]
275
+ by_id = {str(span["phase_id"]): span for span in completed}
276
+
277
+ def has_same_phase_ancestor(span: dict[str, object]) -> bool:
278
+ parent = span["parent_phase_id"]
279
+ while isinstance(parent, str) and parent in by_id:
280
+ ancestor = by_id[parent]
281
+ if ancestor["phase_name"] == span["phase_name"]:
282
+ return True
283
+ parent = ancestor["parent_phase_id"]
284
+ return False
285
+
286
+ semantic = [span for span in completed if not has_same_phase_ancestor(span)]
287
+ by_phase: dict[str, int] = {}
288
+ for span in top_level:
289
+ by_phase[span["phase_name"]] = by_phase.get(span["phase_name"], 0) + int(span["duration_ms"])
290
+ total = by_phase.get("TOTAL_EXECUTION")
291
+ if total is None:
292
+ total = historical_total if historical_total is not None else sum(
293
+ duration for name, duration in by_phase.items() if name != "QUEUE_WAIT"
294
+ )
295
+
296
+ def timestamp(span: dict[str, object], key: str) -> datetime | None:
297
+ value = span.get(key)
298
+ if not isinstance(value, str):
299
+ return None
300
+ try:
301
+ return datetime.fromisoformat(value.replace("Z", "+00:00"))
302
+ except ValueError:
303
+ return None
304
+
305
+ total_envelopes = [
306
+ (timestamp(span, "started_at"), timestamp(span, "completed_at"))
307
+ for span in top_level
308
+ if span["phase_name"] == "TOTAL_EXECUTION"
309
+ ]
310
+ total_envelopes = [
311
+ (started, completed) for started, completed in total_envelopes
312
+ if started is not None and completed is not None and completed >= started
313
+ ]
314
+ if total_envelopes:
315
+ observed_total = round(sum((completed - started).total_seconds() * 1000 for started, completed in total_envelopes))
316
+ # Unit and recovery paths can preserve monotonic durations while their
317
+ # wall-clock timestamps are only boundary markers. Do not turn such
318
+ # non-comparable timestamps into invented overlap evidence.
319
+ if abs(observed_total - total) > 5_000:
320
+ total_envelopes = []
321
+
322
+ def envelope_overlap(span: dict[str, object]) -> int:
323
+ """Return the measurable overlap with TOTAL_EXECUTION, never raw stale tail time."""
324
+ duration = int(span["duration_ms"])
325
+ if not total_envelopes:
326
+ return min(duration, total)
327
+ started, completed = timestamp(span, "started_at"), timestamp(span, "completed_at")
328
+ if started is None or completed is None or completed < started:
329
+ return min(duration, total)
330
+ overlap = sum(
331
+ max(0.0, (min(completed, envelope_end) - max(started, envelope_start)).total_seconds())
332
+ for envelope_start, envelope_end in total_envelopes
333
+ )
334
+ return min(duration, total, round(overlap * 1000))
335
+
336
+ def measured(name: str) -> int:
337
+ return sum(int(span["duration_ms"]) for span in semantic if span["phase_name"] == name)
338
+
339
+ provider = measured("PROVIDER_EXECUTION")
340
+ validation = measured("VALIDATION")
341
+ external = measured("EXTERNAL_CI_WAIT")
342
+ queue = by_phase.get("QUEUE_WAIT", 0)
343
+ report_generation = measured("REPORT_GENERATION")
344
+ evidence_persistence = measured("EVIDENCE_PERSISTENCE")
345
+ repository_finalization = measured("REPOSITORY_FINALIZATION")
346
+ active = max(0, total - external)
347
+
348
+ def has_processing_ancestor(span: dict[str, object]) -> bool:
349
+ """Whether this work is already covered by an enclosing work span.
350
+
351
+ Provider command-boundary validation is deliberately nested under the
352
+ provider process. It remains independently measurable, but cannot
353
+ also consume a second portion of overhead. This ancestry rule keeps
354
+ the accounting partition deterministic even where the persisted UTC
355
+ timestamps have the normal small clock-resolution differences from
356
+ monotonic duration measurement.
357
+ """
358
+ parent = span["parent_phase_id"]
359
+ while isinstance(parent, str) and parent in by_id:
360
+ if by_id[parent]["phase_name"] in {"PROVIDER_EXECUTION", "VALIDATION"}:
361
+ return True
362
+ parent = by_id[parent]["parent_phase_id"]
363
+ return False
364
+
365
+ processing_coverage = sum(
366
+ envelope_overlap(span)
367
+ for span in semantic
368
+ if span["phase_name"] in {"PROVIDER_EXECUTION", "VALIDATION"}
369
+ and not has_processing_ancestor(span)
370
+ )
371
+ overhead = max(0, active - processing_coverage)
372
+ # Category aggregates use the same semantic selection as the named
373
+ # metrics. The deterministic category-name tie break keeps reports, API
374
+ # projections and dashboard detail identical.
375
+ aggregate_by_phase: dict[str, int] = {}
376
+ share_by_phase: dict[str, int] = {}
377
+ for span in semantic:
378
+ name = str(span["phase_name"])
379
+ if name != "TOTAL_EXECUTION":
380
+ aggregate_by_phase[name] = aggregate_by_phase.get(name, 0) + int(span["duration_ms"])
381
+ share_by_phase[name] = min(total, share_by_phase.get(name, 0) + envelope_overlap(span))
382
+ phase_aggregates = [
383
+ {"phase": phase, "duration_ms": duration}
384
+ for phase, duration in sorted(aggregate_by_phase.items(), key=lambda item: (-item[1], item[0]))
385
+ ]
386
+
387
+ def span_label(span: dict[str, object]) -> str:
388
+ """Give repeated spans bounded, typed context without prompt content."""
389
+ name = str(span["phase_name"])
390
+ metadata = span.get("metadata")
391
+ context: str | None = None
392
+ if isinstance(metadata, dict):
393
+ for key in ("validation_kind", "operation", "reason"):
394
+ value = metadata.get(key)
395
+ if isinstance(value, str) and value:
396
+ context = value.replace("_", " ")[:80]
397
+ break
398
+ if context is None and isinstance(metadata.get("iteration"), int):
399
+ context = f"iteration {metadata['iteration']}"
400
+ attempt = span.get("attempt")
401
+ if context:
402
+ return f"{name} — {context}"
403
+ if isinstance(attempt, int) and attempt > 1:
404
+ return f"{name} — attempt {attempt}"
405
+ return name
406
+
407
+ longest_individual_spans = [
408
+ {
409
+ "phase_id": item["phase_id"], "phase": item["phase_name"], "label": span_label(item),
410
+ "duration_ms": item["duration_ms"], "attempt": item["attempt"],
411
+ "ordinal": item["ordinal"], "outcome": item["outcome"],
412
+ }
413
+ for item in sorted(
414
+ (span for span in completed if span["phase_name"] != "TOTAL_EXECUTION"),
415
+ key=lambda item: (-int(item["duration_ms"]), str(item["phase_name"]), int(item["ordinal"])),
416
+ )
417
+ ]
418
+ def share(value: int) -> float:
419
+ return round(value * 100 / total, 3) if total else 0.0
420
+ return {"phase_durations_ms": by_phase, "total_wall_time_ms": total,
421
+ "occurred_phases": tuple(sorted({str(span["phase_name"]) for span in completed})),
422
+ "active_ep_processing_time_ms": active, "provider_execution_time_ms": provider,
423
+ "validation_time_ms": validation, "external_wait_time_ms": external,
424
+ "queue_wait_time_ms": queue, "report_generation_time_ms": report_generation,
425
+ "evidence_persistence_time_ms": evidence_persistence,
426
+ "repository_finalization_time_ms": repository_finalization, "overhead_time_ms": overhead,
427
+ "provider_share_percent": share(share_by_phase.get("PROVIDER_EXECUTION", 0)), "validation_share_percent": share(share_by_phase.get("VALIDATION", 0)),
428
+ "external_wait_share_percent": share(share_by_phase.get("EXTERNAL_CI_WAIT", 0)), "queue_share_percent": share(share_by_phase.get("QUEUE_WAIT", 0)),
429
+ "overhead_share_percent": share(overhead),
430
+ "longest_phase": phase_aggregates[0]["phase"] if phase_aggregates else None,
431
+ "longest_phase_duration_ms": phase_aggregates[0]["duration_ms"] if phase_aggregates else None,
432
+ "phase_aggregates": phase_aggregates,
433
+ "phase_share_durations_ms": share_by_phase,
434
+ "top_phase_categories": phase_aggregates[:3],
435
+ "longest_individual_spans": longest_individual_spans[:3],
436
+ # Compatibility alias for pre-reconciliation API clients. It is
437
+ # intentionally category-only and no longer mixes individual spans.
438
+ "top_time_consumers": phase_aggregates[:3],
439
+ "phase_telemetry_available": bool(spans),
440
+ "historical_total_available": historical_total is not None}
@@ -0,0 +1,28 @@
1
+ """Transaction-scoped Execution Host state; persistence remains in StateStore."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass, replace
5
+ from pathlib import Path
6
+
7
+ from .agent_state import TransactionState
8
+ from .execution_lease import Lease
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class ExecutionTransaction:
13
+ state: TransactionState
14
+ target_repository: Path
15
+ lease: Lease | None = None
16
+
17
+ @property
18
+ def run_id(self) -> str:
19
+ return self.state.run_id
20
+
21
+ @property
22
+ def execution_mode(self) -> str:
23
+ return self.state.execution_mode
24
+
25
+ def with_lease(self, lease: Lease) -> "ExecutionTransaction":
26
+ if lease.run_id != self.run_id:
27
+ raise ValueError("lease run identity conflicts with execution transaction")
28
+ return replace(self, lease=lease)