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,1672 @@
1
+ """Read-only Engineering Platform central-store migration preflight.
2
+
3
+ This module deliberately never opens the legacy store through ``open_storage``:
4
+ that API owns normal write/migration behaviour. Increment 2 may inspect and
5
+ compare stores, but it cannot copy, create, checkpoint, freeze, stop, or hand
6
+ off an authority.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ from dataclasses import asdict, dataclass
13
+ from datetime import datetime, timedelta, timezone
14
+ import fcntl
15
+ import hashlib
16
+ import json
17
+ import os
18
+ from pathlib import Path
19
+ import shutil
20
+ import sqlite3
21
+ import subprocess
22
+ import sys
23
+ import unicodedata
24
+ import uuid
25
+
26
+ from .storage import DATABASE_FILENAME, ENGINEERING_STORAGE_SCHEMA_VERSION, EngineeringStorageError, database_path, legacy_database_path
27
+ from .providers import LaunchdProvider
28
+ from .forensic_attribution import ForensicAttributionError, canonical_attribution_json, load_and_attribute
29
+ from .forensic_attribution_v2 import ForensicAttributionV2Error, canonical_attribution_v2_json, load_and_enrich_v2
30
+ from .forensic_delta import ForensicDeltaError, canonical_report_json, export_forensic_delta
31
+
32
+
33
+ TOOL_VERSION = "2.0.0-phase2-increment3"
34
+ EXPECTED_SCHEMA = ENGINEERING_STORAGE_SCHEMA_VERSION
35
+ HISTORICAL_ATTESTATION_VERSION = 1
36
+ HISTORICAL_ATTESTATION_CLASSIFIER_VERSION = 1
37
+ _BASE_REQUIRED_TABLES = frozenset({
38
+ "engineering_schema_migrations", "engineering_metadata", "engineering_transactions",
39
+ "execution_run_leases", "provider_recovery_attempts", "execution_runs", "execution_submissions",
40
+ "provider_invocation_receipts", "prompt_execution_history",
41
+ "execution_run_qualification_snapshots",
42
+ })
43
+ _CURRENT_CONSUMER_TABLES = ("ep_consumer_credentials", "ep_consumer_registrations")
44
+ _LEGACY_CONSUMER_TABLES = ("local_api_credentials", "local_api_consumer_registrations")
45
+ REQUIRED_TABLES = _BASE_REQUIRED_TABLES | frozenset(_CURRENT_CONSUMER_TABLES)
46
+ TERMINAL_PHASES = ("COMPLETE", "BLOCKED", "FAILED")
47
+ ACTIVE_RECOVERY_STATES = ("RECOVERY_AVAILABLE", "RECOVERY_STARTING", "RECOVERY_IN_PROGRESS")
48
+ FAILURE_CODES = frozenset({
49
+ "LEGACY_STORE_NOT_FOUND", "LEGACY_STORE_AMBIGUOUS", "TARGET_STORE_CONFLICT",
50
+ "ACTIVE_EXECUTION", "ACTIVE_LEASE", "SOURCE_SCHEMA_MISMATCH",
51
+ "SOURCE_INTEGRITY_FAILED", "BACKUP_NOT_READY", "TARGET_UNREADABLE",
52
+ "PROJECT_SCOPE_UNRESOLVED", "AUTHORITY_HANDOFF_NOT_SAFE", "ABORT_PRE_HANDOFF_FAILED",
53
+ "LEGITIMATE_CENTRAL_WRITE_PRESENT", "CONTAMINATION_PROVENANCE_UNRESOLVED",
54
+ "FORENSIC_CENTRAL_UNREADABLE", "LEGACY_BASELINE_MISMATCH",
55
+ "RECOVERY_SERVICE_QUIESCENCE_FAILED", "RECOVERY_AUTHORITY_SWITCH_FAILED",
56
+ "RECOVERY_SERVICE_RESTART_FAILED", "RECOVERY_MIXED_BINDING", "RECOVERY_POSTCHECK_FAILED",
57
+ })
58
+ CONTROL_KEY = "admission_freeze.v1"
59
+ STATE_KEY = "central_store_cutover.v1"
60
+ POINTER_VERSION = 1
61
+ STATES = (
62
+ "PRECHECK", "ADMISSION_FROZEN", "QUIESCENT_SOURCE_BASELINE", "BACKUP_VERIFIED",
63
+ "CENTRAL_STORE_CREATED", "TARGET_VERIFIED", "AUTHORITY_SWITCHED",
64
+ "SERVICES_RESTARTED", "POST_CUTOVER_VERIFIED",
65
+ "LEGACY_ROLLBACK_COMPATIBLE", "CENTRAL_STORE_ACTIVE_POST_WRITE", "ABORTED_PRE_HANDOFF",
66
+ "CONTAMINATED_RECOVERY_PRECHECK", "RECOVERY_SERVICES_QUIESCED",
67
+ "RECOVERY_LEGACY_VERIFIED", "ROLLBACK_IN_PROGRESS", "ROLLBACK_COMPLETED",
68
+ )
69
+ ABORTABLE_STATES = frozenset({"PRECHECK", "ADMISSION_FROZEN", "QUIESCENT_SOURCE_BASELINE"})
70
+ ABORT_REASONS = frozenset({"CONTROLLER_VERSION_INCOMPATIBLE", "PRE_HANDOFF_CONTROLLER_DEFECT"})
71
+ # CENTRAL cutover may control only the current relay. The other two labels are
72
+ # retained below solely to recognize a stale pre-P-NEUTRAL lock; they are never
73
+ # installed, restarted or selected as lifecycle authority by this module.
74
+ SERVICE_STOP_ORDER = ("com.engineeringplatform.dashboard-relay",)
75
+ SERVICE_START_ORDER = ("com.engineeringplatform.dashboard-relay",)
76
+ HISTORICAL_LOCK_IDENTITIES = {
77
+ "dashboard.lock": ("dashboard", "engineering_platform.dashboard", "com.djconnect.engineering-dashboard"),
78
+ "inbox-watcher.lock": ("inbox-watcher", "engineering_platform.inbox_watcher", "com.djconnect.engineering-inbox"),
79
+ }
80
+ EXPECTED_RUNNING_LOCKS = {
81
+ **HISTORICAL_LOCK_IDENTITIES,
82
+ }
83
+
84
+
85
+ class CutoverError(RuntimeError):
86
+ """Stable fail-closed cutover error."""
87
+
88
+ def __init__(self, code: str, detail: str = "") -> None:
89
+ super().__init__(code if not detail else f"{code}: {detail}")
90
+ self.code = code
91
+
92
+
93
+ class LaunchAgentServiceControl:
94
+ """Canonical macOS LaunchAgent adapter; invoked only by explicit cutover CLI."""
95
+
96
+ def __init__(self, uid: int | None = None) -> None:
97
+ self._domain = f"gui/{uid if uid is not None else os.getuid()}"
98
+ self._launchd = LaunchdProvider()
99
+
100
+ @staticmethod
101
+ def _plist(label: str) -> Path:
102
+ return Path.home() / "Library" / "LaunchAgents" / f"{label}.plist"
103
+
104
+ def stop(self, label: str) -> None:
105
+ try:
106
+ self._launchd.quiesce(label, self._plist(label))
107
+ except OSError as error:
108
+ raise CutoverError("SERVICE_STOP_FAILED", label) from error
109
+ if not self.stopped(label):
110
+ raise CutoverError("SERVICE_STOP_FAILED", label)
111
+
112
+ def start(self, label: str) -> None:
113
+ try:
114
+ self._launchd.resume(label, self._plist(label))
115
+ except OSError as error:
116
+ raise CutoverError("SERVICE_RESTART_FAILED", label) from error
117
+ if not self.running(label):
118
+ raise CutoverError("SERVICE_RESTART_FAILED", label)
119
+
120
+ def stopped(self, label: str) -> bool:
121
+ return not self._launchd.inspect(label)
122
+
123
+ def running(self, label: str) -> bool:
124
+ result = subprocess.run(["launchctl", "print", f"{self._domain}/{label}"], capture_output=True, text=True, check=False)
125
+ return result.returncode == 0 and "state = running" in result.stdout
126
+
127
+
128
+ @dataclass(frozen=True)
129
+ class StoreCandidate:
130
+ path: str
131
+ resolved_path: str
132
+ provenance: tuple[str, ...]
133
+
134
+
135
+ @dataclass(frozen=True)
136
+ class StoreIdentity:
137
+ path: str
138
+ resolved_path: str
139
+ size_bytes: int
140
+ modified_ns: int
141
+ fingerprint_sha256: str
142
+ schema_version: int | None
143
+ provenance: tuple[str, ...]
144
+
145
+
146
+ def user_data_dir(app_name: str = "Engineering Platform") -> Path:
147
+ """Return the portable per-user application-data directory.
148
+
149
+ This is the repository's dependency-free equivalent of
150
+ ``platformdirs.user_data_dir``. It follows platform application-data
151
+ conventions without embedding a user or checkout path in the contract.
152
+ """
153
+ if sys.platform == "darwin":
154
+ return Path.home() / "Library" / "Application Support" / app_name
155
+ if os.name == "nt":
156
+ base = os.environ.get("LOCALAPPDATA") or os.environ.get("APPDATA")
157
+ return Path(base) / app_name if base else Path.home() / "AppData" / "Local" / app_name
158
+ base = os.environ.get("XDG_DATA_HOME")
159
+ return Path(base) / app_name if base else Path.home() / ".local" / "share" / app_name
160
+
161
+
162
+ def installation_data_root() -> Path:
163
+ """Resolve the one canonical, installation-owned EP data root."""
164
+ return user_data_dir("Engineering Platform")
165
+
166
+
167
+ def central_store_path() -> Path:
168
+ """Return the future central store path without creating it."""
169
+ return installation_data_root() / DATABASE_FILENAME
170
+
171
+
172
+ def _readonly(path: Path) -> sqlite3.Connection:
173
+ return sqlite3.connect(f"file:{path.resolve().as_posix()}?mode=ro", uri=True)
174
+
175
+
176
+ def _tables(connection: sqlite3.Connection) -> set[str]:
177
+ return {str(row[0]) for row in connection.execute("SELECT name FROM sqlite_master WHERE type='table'")}
178
+
179
+
180
+ def _schema(connection: sqlite3.Connection, tables: set[str] | None = None) -> int | None:
181
+ tables = tables if tables is not None else _tables(connection)
182
+ if "engineering_schema_migrations" not in tables:
183
+ return None
184
+ row = connection.execute("SELECT MAX(version) FROM engineering_schema_migrations").fetchone()
185
+ return int(row[0]) if row and row[0] is not None else 0
186
+
187
+
188
+ def _fingerprint(path: Path) -> str:
189
+ digest = hashlib.sha256()
190
+ with path.open("rb") as handle:
191
+ while chunk := handle.read(1_048_576):
192
+ digest.update(chunk)
193
+ return digest.hexdigest()
194
+
195
+
196
+ def discover_legacy_stores(repo: Path, *, extra_runtime_roots: tuple[Path, ...] = ()) -> tuple[StoreCandidate, ...]:
197
+ """Discover only canonical runtime-resolver candidates; never scan disks."""
198
+ roots = ((repo.resolve(), "storage.database_path(repo)"),) + tuple(
199
+ (root.resolve(), "explicit_runtime_evidence") for root in extra_runtime_roots
200
+ )
201
+ grouped: dict[Path, set[str]] = {}
202
+ for root, provenance in roots:
203
+ candidate = legacy_database_path(root).resolve()
204
+ if candidate.is_file():
205
+ grouped.setdefault(candidate, set()).add(provenance)
206
+ return tuple(
207
+ StoreCandidate(str(path), str(path), tuple(sorted(provenance)))
208
+ for path, provenance in sorted(grouped.items(), key=lambda item: str(item[0]))
209
+ )
210
+
211
+
212
+ def source_identity(candidate: StoreCandidate) -> StoreIdentity:
213
+ path = Path(candidate.resolved_path)
214
+ stat = path.stat()
215
+ schema: int | None = None
216
+ try:
217
+ with _readonly(path) as connection:
218
+ schema = _schema(connection)
219
+ except (OSError, sqlite3.DatabaseError):
220
+ pass
221
+ return StoreIdentity(candidate.path, candidate.resolved_path, stat.st_size, stat.st_mtime_ns, _fingerprint(path), schema, candidate.provenance)
222
+
223
+
224
+ def _same_source_content(left: StoreIdentity, right: StoreIdentity | dict[str, object]) -> bool:
225
+ """Compare source authority content; filesystem timestamps are not evidence writes."""
226
+ values = asdict(left)
227
+ other = asdict(right) if isinstance(right, StoreIdentity) else right
228
+ return all(values.get(key) == other.get(key) for key in ("resolved_path", "size_bytes", "fingerprint_sha256", "schema_version"))
229
+
230
+
231
+ def classify_target(path: Path) -> dict[str, object]:
232
+ """Classify a target without creating, modifying, or repairing it."""
233
+ if not path.exists():
234
+ return {"state": "ABSENT", "path": str(path), "blocking_code": None}
235
+ if not path.is_file():
236
+ return {"state": "UNKNOWN", "path": str(path), "blocking_code": "TARGET_UNREADABLE"}
237
+ try:
238
+ for receipt_file in (installation_data_root() / "migration").glob("*.json"):
239
+ try:
240
+ receipt = json.loads(receipt_file.read_text(encoding="utf-8"))
241
+ forensic = receipt.get("central_forensic") if isinstance(receipt, dict) else None
242
+ if isinstance(forensic, dict) and forensic.get("path") == str(path.resolve()) and forensic.get("classification") == "FORENSIC_CONTAMINATED_NON_AUTHORITATIVE":
243
+ return {"state": "FORENSIC_CONTAMINATED_NON_AUTHORITATIVE", "path": str(path), "blocking_code": "TARGET_STORE_CONFLICT"}
244
+ except (OSError, json.JSONDecodeError):
245
+ continue
246
+ if path.stat().st_size == 0:
247
+ return {"state": "EMPTY_NEW", "path": str(path), "blocking_code": None}
248
+ with _readonly(path) as connection:
249
+ tables = _tables(connection)
250
+ if not tables:
251
+ return {"state": "EMPTY_NEW", "path": str(path), "blocking_code": None}
252
+ schema = _schema(connection, tables)
253
+ if schema == EXPECTED_SCHEMA and _required_tables(tables) <= tables:
254
+ return {"state": "COMPATIBLE_EXISTING", "path": str(path), "blocking_code": "TARGET_STORE_CONFLICT"}
255
+ return {"state": "CONFLICTING_EXISTING", "path": str(path), "blocking_code": "TARGET_STORE_CONFLICT"}
256
+ except (OSError, sqlite3.DatabaseError):
257
+ return {"state": "CORRUPT_UNREADABLE", "path": str(path), "blocking_code": "TARGET_UNREADABLE"}
258
+
259
+
260
+ def inspect_source(candidate: StoreCandidate) -> dict[str, object]:
261
+ """Read source schema/integrity/table facts without a write transaction."""
262
+ path = Path(candidate.resolved_path)
263
+ result: dict[str, object] = {"identity": asdict(source_identity(candidate)), "tables": [], "integrity": "FAILED", "blocking_codes": []}
264
+ try:
265
+ with _readonly(path) as connection:
266
+ tables = _tables(connection)
267
+ schema = _schema(connection, tables)
268
+ integrity = [str(row[0]) for row in connection.execute("PRAGMA integrity_check")]
269
+ result.update({"tables": sorted(tables), "schema_version": schema, "integrity": "PASS" if integrity == ["ok"] else "FAILED"})
270
+ if schema != EXPECTED_SCHEMA:
271
+ result["blocking_codes"].append("SOURCE_SCHEMA_MISMATCH")
272
+ required = _required_tables(tables)
273
+ if integrity != ["ok"] or not required <= tables:
274
+ result["blocking_codes"].append("SOURCE_INTEGRITY_FAILED")
275
+ result["required_tables_present"] = sorted(required & tables)
276
+ result["required_tables_missing"] = sorted(required - tables)
277
+ result["journal_mode"] = str(connection.execute("PRAGMA journal_mode").fetchone()[0]).upper()
278
+ except (OSError, sqlite3.DatabaseError):
279
+ result["blocking_codes"].append("SOURCE_INTEGRITY_FAILED")
280
+ return result
281
+
282
+
283
+ def _count(connection: sqlite3.Connection, query: str, params: tuple[object, ...] = ()) -> int:
284
+ return int(connection.execute(query, params).fetchone()[0])
285
+
286
+
287
+ def _consumer_authority_tables(tables: set[str]) -> tuple[str, str] | None:
288
+ """Return the sole authority pair, preferring the current neutral schema.
289
+
290
+ The retained historical pair is readable only for pre-schema-53 migration
291
+ evidence. Once current tables exist, all migration inspection uses the
292
+ neutral pair and never falls back to legacy data.
293
+ """
294
+
295
+ if set(_CURRENT_CONSUMER_TABLES) <= tables:
296
+ return _CURRENT_CONSUMER_TABLES
297
+ if set(_LEGACY_CONSUMER_TABLES) <= tables:
298
+ return _LEGACY_CONSUMER_TABLES
299
+ return None
300
+
301
+
302
+ def _required_tables(tables: set[str]) -> frozenset[str]:
303
+ pair = _consumer_authority_tables(tables)
304
+ return _BASE_REQUIRED_TABLES | frozenset(pair or _CURRENT_CONSUMER_TABLES)
305
+
306
+
307
+ def _consumer_authority_tables_for_path(path: Path) -> tuple[str, str] | None:
308
+ with _readonly(path) as connection:
309
+ return _consumer_authority_tables(_tables(connection))
310
+
311
+
312
+ def _lock_owner(lock: Path) -> tuple[str, int] | None:
313
+ try:
314
+ payload = json.loads(lock.read_text(encoding="utf-8"))
315
+ component = payload.get("component") if isinstance(payload, dict) else None
316
+ process_id = payload.get("pid") if isinstance(payload, dict) else None
317
+ if isinstance(component, str) and isinstance(process_id, int) and process_id > 0:
318
+ return component, process_id
319
+ except (OSError, json.JSONDecodeError):
320
+ pass
321
+ return None
322
+
323
+
324
+ def _process_command(process_id: int) -> str | None:
325
+ result = subprocess.run(["ps", "-p", str(process_id), "-o", "command="], capture_output=True, text=True, check=False)
326
+ return result.stdout.strip() if result.returncode == 0 and result.stdout.strip() else None
327
+
328
+
329
+ def _classify_lock(lock: Path, *, pre_stop: bool, services: LaunchAgentServiceControl | None) -> str:
330
+ """Classify a held component lock without treating its filename as ownership."""
331
+ try:
332
+ with lock.open("r", encoding="utf-8") as handle:
333
+ try:
334
+ fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
335
+ except BlockingIOError:
336
+ pass
337
+ else:
338
+ fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
339
+ owner = _lock_owner(lock)
340
+ expected = EXPECTED_RUNNING_LOCKS.get(lock.name)
341
+ if owner is not None and expected is not None and owner[0] == expected[0]:
342
+ try:
343
+ os.kill(owner[1], 0)
344
+ except ProcessLookupError:
345
+ return "INACTIVE_EXPECTED_SERVICE_LOCK"
346
+ except PermissionError:
347
+ return "UNKNOWN_LOCK"
348
+ return "STALE_UNOWNED_LOCK"
349
+ except OSError:
350
+ return "UNKNOWN_LOCK"
351
+ owner = _lock_owner(lock)
352
+ expected = EXPECTED_RUNNING_LOCKS.get(lock.name)
353
+ if not pre_stop or owner is None or expected is None or services is None:
354
+ return "UNKNOWN_LOCK"
355
+ component, process_id = owner
356
+ expected_component, expected_module, expected_service = expected
357
+ command = _process_command(process_id)
358
+ if component == expected_component and command is not None and expected_module in command and services.running(expected_service):
359
+ return "EXPECTED_RUNNING_SERVICE_LOCK"
360
+ return "UNEXPECTED_LIVE_LOCK"
361
+
362
+
363
+ def inspect_quiescence(path: Path, *, pre_stop: bool = False, services: LaunchAgentServiceControl | None = None) -> dict[str, object]:
364
+ """Inspect lifecycle blockers; only verified canonical service locks are allowed pre-stop."""
365
+ facts: dict[str, object] = {"non_terminal_transactions": 0, "active_leases": 0, "active_recovery": 0, "unsafe_locks": [], "lock_classifications": {}, "blocking_codes": []}
366
+ try:
367
+ with _readonly(path) as connection:
368
+ tables = _tables(connection)
369
+ if "engineering_transactions" in tables:
370
+ facts["non_terminal_transactions"] = _count(connection, "SELECT COUNT(*) FROM engineering_transactions WHERE phase NOT IN (?,?,?)", TERMINAL_PHASES)
371
+ if "execution_run_leases" in tables:
372
+ facts["active_leases"] = _count(connection, "SELECT COUNT(*) FROM execution_run_leases WHERE lease_state='ACTIVE'")
373
+ if "provider_recovery_attempts" in tables:
374
+ placeholders = ",".join("?" for _ in ACTIVE_RECOVERY_STATES)
375
+ facts["active_recovery"] = _count(connection, f"SELECT COUNT(*) FROM provider_recovery_attempts WHERE state IN ({placeholders})", ACTIVE_RECOVERY_STATES)
376
+ except (OSError, sqlite3.DatabaseError):
377
+ facts["blocking_codes"].append("AUTHORITY_HANDOFF_NOT_SAFE")
378
+ locks = path.parent / "locks"
379
+ if locks.is_dir():
380
+ for lock in sorted(locks.glob("*.lock")):
381
+ classification = _classify_lock(lock, pre_stop=pre_stop, services=services)
382
+ facts["lock_classifications"][lock.name] = classification
383
+ allowed = {"EXPECTED_RUNNING_SERVICE_LOCK"} if pre_stop else {"INACTIVE_EXPECTED_SERVICE_LOCK"}
384
+ if classification not in allowed:
385
+ facts["unsafe_locks"].append(lock.name)
386
+ if facts["non_terminal_transactions"]:
387
+ facts["blocking_codes"].append("ACTIVE_EXECUTION")
388
+ if facts["active_leases"]:
389
+ facts["blocking_codes"].append("ACTIVE_LEASE")
390
+ if facts["active_recovery"] or facts["unsafe_locks"]:
391
+ facts["blocking_codes"].append("AUTHORITY_HANDOFF_NOT_SAFE")
392
+ facts["eligible"] = not facts["blocking_codes"]
393
+ return facts
394
+
395
+
396
+ def project_scope_inventory(path: Path) -> dict[str, object]:
397
+ """Return project and credential metadata counts only; never credential values."""
398
+ result: dict[str, object] = {"project_ids": [], "consumer_registrations": 0, "credential_scopes": 0, "run_project_associations": "NOT_PERSISTED", "prompt_history_project_relationships": "NOT_PERSISTED", "plaintext_credential_columns": [], "blocking_codes": []}
399
+ try:
400
+ with _readonly(path) as connection:
401
+ tables = _tables(connection)
402
+ pair = _consumer_authority_tables(tables)
403
+ if pair is None:
404
+ result["blocking_codes"].append("SOURCE_INTEGRITY_FAILED")
405
+ return result
406
+ credentials_table, registrations_table = pair
407
+ if registrations_table in tables:
408
+ rows = connection.execute(f"SELECT DISTINCT project_id FROM {registrations_table} ORDER BY project_id").fetchall()
409
+ result["project_ids"] = [str(row[0]) for row in rows]
410
+ result["consumer_registrations"] = _count(connection, f"SELECT COUNT(*) FROM {registrations_table}")
411
+ if credentials_table in tables:
412
+ result["credential_scopes"] = _count(connection, f"SELECT COUNT(*) FROM {credentials_table}")
413
+ columns = {str(row[1]).casefold() for row in connection.execute(f"PRAGMA table_info({credentials_table})")}
414
+ result["plaintext_credential_columns"] = sorted(columns & {"credential", "token", "bearer", "secret", "plaintext"})
415
+ for table in ("engineering_transactions", "prompt_execution_history"):
416
+ if table in tables:
417
+ columns = {str(row[1]) for row in connection.execute(f"PRAGMA table_info({table})")}
418
+ key = "run_project_associations" if table == "engineering_transactions" else "prompt_history_project_relationships"
419
+ result[key] = "PERSISTED" if "project_id" in columns else "NOT_PERSISTED"
420
+ if result["plaintext_credential_columns"]:
421
+ result["blocking_codes"].append("SOURCE_INTEGRITY_FAILED")
422
+ except (OSError, sqlite3.DatabaseError):
423
+ result["blocking_codes"].append("PROJECT_SCOPE_UNRESOLVED")
424
+ return result
425
+
426
+
427
+ def backup_readiness(identity: StoreIdentity, root: Path) -> dict[str, object]:
428
+ backup = root / "backups" / f"legacy-schema40-{identity.fingerprint_sha256[:16]}-<migration-id>.db"
429
+ ancestor = root
430
+ while not ancestor.exists() and ancestor != ancestor.parent:
431
+ ancestor = ancestor.parent
432
+ writable = os.access(ancestor, os.W_OK | os.X_OK)
433
+ available = shutil.disk_usage(ancestor).free if ancestor.exists() else 0
434
+ ready = writable and available >= identity.size_bytes
435
+ return {"backup_path": str(backup), "root_exists": root.exists(), "writable_ancestor": str(ancestor), "available_bytes": available, "required_bytes": identity.size_bytes, "integrity_method": "PRAGMA integrity_check", "ready": ready, "blocking_code": None if ready else "BACKUP_NOT_READY"}
436
+
437
+
438
+ def snapshot_plan(source: dict[str, object]) -> dict[str, object]:
439
+ identity = source.get("identity", {})
440
+ path = Path(str(identity.get("resolved_path", "")))
441
+ return {"strategy": "sqlite_backup_api", "requires_future_quiescence": True, "source_read_only": True, "temporary_target_then_fsync_atomic_rename": True, "raw_copy_forbidden": True, "journal_mode": source.get("journal_mode"), "wal_present": path.with_name(path.name + "-wal").exists(), "shm_present": path.with_name(path.name + "-shm").exists(), "checkpoint_requirement": "PRAGMA wal_checkpoint(TRUNCATE) by the sole controlled writer before copy"}
442
+
443
+
444
+ def _table_counts(path: Path) -> dict[str, int]:
445
+ with _readonly(path) as connection:
446
+ return {table: _count(connection, f'SELECT COUNT(*) FROM "{table}"') for table in sorted(_tables(connection)) if not table.startswith("sqlite_")}
447
+
448
+
449
+ def validate_target_equivalence(source: Path, target: Path) -> dict[str, object]:
450
+ """Compare a candidate target read-only; intended for test/future cutover use."""
451
+ result: dict[str, object] = {"equivalent": False, "blocking_codes": [], "differences": []}
452
+ try:
453
+ source_facts, target_facts = inspect_source(StoreCandidate(str(source), str(source.resolve()), ("comparison_source",))), inspect_source(StoreCandidate(str(target), str(target.resolve()), ("comparison_target",)))
454
+ if source_facts.get("schema_version") != target_facts.get("schema_version"):
455
+ result["differences"].append("schema_version")
456
+ if source_facts.get("tables") != target_facts.get("tables"):
457
+ result["differences"].append("tables")
458
+ if _table_counts(source) != _table_counts(target):
459
+ result["differences"].append("table_counts")
460
+ if project_scope_inventory(source) != project_scope_inventory(target):
461
+ result["differences"].append("project_scope")
462
+ result["equivalent"] = not result["differences"]
463
+ except (OSError, sqlite3.DatabaseError):
464
+ result["differences"].append("target_unreadable")
465
+ result["blocking_codes"].append("TARGET_UNREADABLE")
466
+ if not result["equivalent"] and not result["blocking_codes"]:
467
+ result["blocking_codes"].append("TARGET_STORE_CONFLICT")
468
+ return result
469
+
470
+
471
+ def _now() -> str:
472
+ return datetime.now(timezone.utc).isoformat()
473
+
474
+
475
+ def _metadata(path: Path, key: str) -> dict[str, object] | None:
476
+ with sqlite3.connect(f"file:{path.resolve().as_posix()}?mode=ro", uri=True) as connection:
477
+ row = connection.execute("SELECT value FROM engineering_metadata WHERE key=?", (key,)).fetchone()
478
+ if row is None:
479
+ return None
480
+ value = json.loads(str(row[0]))
481
+ if not isinstance(value, dict):
482
+ raise CutoverError("ADMISSION_FREEZE_FAILED", "metadata is malformed")
483
+ return value
484
+
485
+
486
+ def admission_status(repo: Path) -> dict[str, object]:
487
+ try:
488
+ path = database_path(repo)
489
+ if not path.is_file():
490
+ raise CutoverError("ADMISSION_FREEZE_FAILED", "authority is unresolved")
491
+ payload = _metadata(path, CONTROL_KEY)
492
+ return payload or {"state": "INACTIVE"}
493
+ except (OSError, sqlite3.DatabaseError, EngineeringStorageError) as error:
494
+ raise CutoverError("ADMISSION_FREEZE_FAILED", "authority is unreadable") from error
495
+
496
+
497
+ def set_admission_freeze(repo: Path, *, migration_id: str | None = None, reason: str, operator: str = "operator") -> dict[str, object]:
498
+ """Explicit control-plane mutation; no prompt/provider path calls this."""
499
+ candidates = discover_legacy_stores(repo)
500
+ if len(candidates) != 1 or not reason.strip():
501
+ raise CutoverError("ADMISSION_FREEZE_FAILED")
502
+ migration_id = migration_id or str(uuid.uuid4())
503
+ active = admission_status(repo)
504
+ if active.get("state") == "ACTIVE" and active.get("migration_id") != migration_id:
505
+ raise CutoverError("ADMISSION_FREEZE_FAILED", "conflicting active migration")
506
+ path = Path(candidates[0].resolved_path)
507
+ payload = {"version": 1, "migration_id": migration_id, "state": "ACTIVE", "reason": reason.strip(), "operator": operator, "created_at": _now()}
508
+ with sqlite3.connect(path) as connection:
509
+ connection.execute("INSERT INTO engineering_metadata(key,value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", (CONTROL_KEY, json.dumps(payload, sort_keys=True, separators=(",", ":"))))
510
+ # This identity documents the pre-stop source only. Service shutdown is
511
+ # allowed to write bounded lifecycle evidence, so it is never the copy
512
+ # baseline or a source-drift gate.
513
+ receipt = load_receipt(migration_id)
514
+ if receipt is None:
515
+ receipt = {
516
+ "receipt_version": 1,
517
+ "migration_id": migration_id,
518
+ "schema": EXPECTED_SCHEMA,
519
+ "operator": operator,
520
+ "legacy_path": str(path),
521
+ "pre_stop_source_identity": asdict(source_identity(candidates[0])),
522
+ "rollback_mode": "PRE_WRITE_DIRECT",
523
+ }
524
+ transition_receipt(receipt, "PRECHECK")
525
+ transition_receipt(receipt, "ADMISSION_FROZEN", admission_freeze=payload)
526
+ return payload
527
+
528
+
529
+ def thaw_admission(repo: Path, *, migration_id: str, operator: str = "operator") -> dict[str, object]:
530
+ state = admission_status(repo)
531
+ if state.get("state") != "ACTIVE" or state.get("migration_id") != migration_id:
532
+ raise CutoverError("THAW_FAILED")
533
+ path = database_path(repo)
534
+ payload = {"version": 1, "migration_id": migration_id, "state": "INACTIVE", "operator": operator, "thawed_at": _now()}
535
+ with sqlite3.connect(path) as connection:
536
+ connection.execute("UPDATE engineering_metadata SET value=? WHERE key=?", (json.dumps(payload, sort_keys=True, separators=(",", ":")), CONTROL_KEY))
537
+ receipt = load_receipt(migration_id)
538
+ if receipt is not None and receipt.get("state") == "ABORTED_PRE_HANDOFF":
539
+ receipt["thaw"] = {"operator": operator, "timestamp": payload["thawed_at"], "state": "INACTIVE"}
540
+ _atomic_json(receipt_path(migration_id), receipt)
541
+ return payload
542
+
543
+
544
+ def _atomic_json(path: Path, payload: dict[str, object]) -> None:
545
+ path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
546
+ temporary = path.with_name(f".{path.name}.{uuid.uuid4().hex}.tmp")
547
+ try:
548
+ with temporary.open("x", encoding="utf-8") as handle:
549
+ json.dump(payload, handle, sort_keys=True, separators=(",", ":"))
550
+ handle.flush()
551
+ os.fsync(handle.fileno())
552
+ os.replace(temporary, path)
553
+ directory = os.open(path.parent, os.O_RDONLY)
554
+ try:
555
+ os.fsync(directory)
556
+ finally:
557
+ os.close(directory)
558
+ finally:
559
+ if temporary.exists():
560
+ temporary.unlink()
561
+
562
+
563
+ def receipt_path(migration_id: str) -> Path:
564
+ return installation_data_root() / "migration" / f"{migration_id}.json"
565
+
566
+
567
+ def load_receipt(migration_id: str) -> dict[str, object] | None:
568
+ path = receipt_path(migration_id)
569
+ if not path.is_file():
570
+ return None
571
+ try:
572
+ payload = json.loads(path.read_text(encoding="utf-8"))
573
+ except (OSError, json.JSONDecodeError) as error:
574
+ raise CutoverError("AUTHORITY_SWITCH_FAILED", "receipt is malformed") from error
575
+ if not isinstance(payload, dict) or payload.get("migration_id") != migration_id:
576
+ raise CutoverError("AUTHORITY_SWITCH_FAILED", "receipt identity is invalid")
577
+ return payload
578
+
579
+
580
+ def transition_receipt(receipt: dict[str, object], state: str, **details: object) -> dict[str, object]:
581
+ """Persist only adjacent forward transitions; state cannot be skipped/backtracked."""
582
+ if state not in STATES:
583
+ raise CutoverError("AUTHORITY_SWITCH_FAILED", "unknown state")
584
+ prior = receipt.get("state")
585
+ if prior is not None:
586
+ recovery_next = {
587
+ "SERVICES_RESTARTED": "CONTAMINATED_RECOVERY_PRECHECK",
588
+ "CONTAMINATED_RECOVERY_PRECHECK": "RECOVERY_SERVICES_QUIESCED",
589
+ "RECOVERY_SERVICES_QUIESCED": "RECOVERY_LEGACY_VERIFIED",
590
+ "RECOVERY_LEGACY_VERIFIED": "ROLLBACK_IN_PROGRESS",
591
+ "ROLLBACK_IN_PROGRESS": "ROLLBACK_COMPLETED",
592
+ }
593
+ if recovery_next.get(str(prior)) == state:
594
+ receipt["state"] = state
595
+ receipt.setdefault("transitions", []).append({"state": state, "timestamp": _now()})
596
+ receipt.update(details)
597
+ _atomic_json(receipt_path(str(receipt["migration_id"])), receipt)
598
+ return receipt
599
+ if state == "ABORTED_PRE_HANDOFF":
600
+ if prior not in ABORTABLE_STATES:
601
+ raise CutoverError("ABORT_PRE_HANDOFF_FAILED", "migration is not pre-handoff")
602
+ else:
603
+ try:
604
+ expected = STATES[STATES.index(str(prior)) + 1]
605
+ except (ValueError, IndexError) as error:
606
+ raise CutoverError("AUTHORITY_SWITCH_FAILED", "terminal or invalid transition") from error
607
+ if state != expected:
608
+ raise CutoverError("AUTHORITY_SWITCH_FAILED", "non-monotonic transition")
609
+ receipt["state"] = state
610
+ receipt.setdefault("transitions", []).append({"state": state, "timestamp": _now()})
611
+ receipt.update(details)
612
+ _atomic_json(receipt_path(str(receipt["migration_id"])), receipt)
613
+ return receipt
614
+
615
+
616
+ def _central_write_assessment(path: Path) -> dict[str, object]:
617
+ """Read only provenance assessment; unknown lineage blocks recovery."""
618
+ result: dict[str, object] = {"legitimate_write": False, "managed_legitimate_count": 0, "proven_contamination_count": 0, "unresolved_count": 0, "unknown_mutation": False, "signals": []}
619
+ try:
620
+ with _readonly(path) as connection:
621
+ tables = _tables(connection)
622
+ if "provider_invocation_receipts" in tables and _count(connection, "SELECT COUNT(*) FROM provider_invocation_receipts"):
623
+ # Receipts are evidence of a provider launch, not proof that its
624
+ # source was production. Without a canonical submission lineage
625
+ # they must be explained by the contamination attestation below.
626
+ result["signals"].append("provider_receipts_without_submission_lineage")
627
+ if "backup_probe" in tables:
628
+ result["signals"].append("test_only_backup_probe")
629
+ schema = _schema(connection, tables)
630
+ if schema is not None and schema > EXPECTED_SCHEMA:
631
+ result["signals"].append("unsupported_schema_marker")
632
+ result["proven_contamination_count"] = len([signal for signal in result["signals"] if signal in {"test_only_backup_probe", "unsupported_schema_marker"}])
633
+ except (OSError, sqlite3.DatabaseError) as error:
634
+ raise CutoverError("FORENSIC_CENTRAL_UNREADABLE") from error
635
+ return result
636
+
637
+
638
+ def _domain_digest(path: Path, table: str, columns: tuple[str, ...]) -> str:
639
+ """Hash a sorted, type-stable authority projection without secrets."""
640
+ with _readonly(path) as connection:
641
+ available = {str(row[1]) for row in connection.execute(f"PRAGMA table_info({table})")}
642
+ if not set(columns) <= available:
643
+ return "ABSENT"
644
+ rows = connection.execute(
645
+ f"SELECT {','.join(columns)} FROM {table} ORDER BY {','.join(columns)}"
646
+ ).fetchall()
647
+ normalized = []
648
+ for row in rows:
649
+ normalized.append([value.hex() if isinstance(value, bytes) else value for value in row])
650
+ return hashlib.sha256(json.dumps(normalized, ensure_ascii=False, separators=(",", ":"), sort_keys=False).encode("utf-8")).hexdigest()
651
+
652
+
653
+ def authority_independent_baseline_attestation(baseline: Path, central: Path) -> dict[str, object]:
654
+ """ADR-0025 exact baseline-delta classifier; never invents row origin."""
655
+ baseline_pair = _consumer_authority_tables_for_path(baseline)
656
+ central_pair = _consumer_authority_tables_for_path(central)
657
+ if baseline_pair is None or central_pair is None:
658
+ raise CutoverError("CONTAMINATION_PROVENANCE_UNRESOLVED", "consumer authority tables are unavailable")
659
+ domains = {
660
+ "credentials": (baseline_pair[0], central_pair[0], ("credential_id", "consumer_id", "project_id", "verifier", "fingerprint", "issued_at", "expires_at", "revoked_at", "replaced_by_credential_id")),
661
+ "registrations": (baseline_pair[1], central_pair[1], ("consumer_id", "project_id", "status", "created_at", "updated_at", "disabled_at", "revoked_at")),
662
+ "project_scope": (baseline_pair[1], central_pair[1], ("consumer_id", "project_id", "status")),
663
+ }
664
+ result: dict[str, object] = {"attestation_version": 1, "domains": {}, "credential_delta": False, "registration_delta": False, "project_scope_delta": False}
665
+ for name, (baseline_table, central_table, columns) in domains.items():
666
+ baseline_digest, central_digest = _domain_digest(baseline, baseline_table, columns), _domain_digest(central, central_table, columns)
667
+ delta = baseline_digest != central_digest
668
+ result["domains"][name] = {"baseline_digest": baseline_digest, "central_digest": central_digest, "classification": "CONTAMINATION_PROVENANCE_UNRESOLVED" if delta else "NO_POST_CUTOVER_MUTATION"}
669
+ result[f"{name[:-1] if name.endswith('s') else name}_delta"] = delta
670
+ return result
671
+
672
+
673
+ def _file_digest(path: Path) -> str:
674
+ try:
675
+ return hashlib.sha256(path.read_bytes()).hexdigest()
676
+ except OSError as error:
677
+ raise CutoverError("CONTAMINATION_PROVENANCE_UNRESOLVED", "required forensic file is unreadable") from error
678
+
679
+
680
+ def contamination_attestation_path(migration_id: str) -> Path:
681
+ """Return the external, immutable receipt path for one incident only."""
682
+ return installation_data_root() / "migration" / "contamination-attestations" / f"{migration_id}.json"
683
+
684
+
685
+ def _authority_rows(path: Path, table: str, columns: tuple[str, ...], key: tuple[str, ...]) -> dict[tuple[object, ...], dict[str, object]]:
686
+ with _readonly(path) as connection:
687
+ available = {str(row[1]) for row in connection.execute(f"PRAGMA table_info({_quote_identifier(table)})")}
688
+ if not set(columns) <= available:
689
+ raise CutoverError("CONTAMINATION_PROVENANCE_UNRESOLVED", f"authority table shape is unavailable: {table}")
690
+ rows = connection.execute(f"SELECT {','.join(_quote_identifier(column) for column in columns)} FROM {_quote_identifier(table)}").fetchall()
691
+ result: dict[tuple[object, ...], dict[str, object]] = {}
692
+ for values in rows:
693
+ row = dict(zip(columns, values, strict=True))
694
+ identity = tuple(row[column] for column in key)
695
+ if identity in result:
696
+ raise CutoverError("CONTAMINATION_PROVENANCE_UNRESOLVED", f"non-unique authority identity: {table}")
697
+ result[identity] = row
698
+ return result
699
+
700
+
701
+ def _safe_authority_row(row: dict[str, object]) -> dict[str, object]:
702
+ """Project authority evidence without retaining verifier or fingerprint bytes."""
703
+ safe: dict[str, object] = {}
704
+ for key, value in sorted(row.items()):
705
+ if key in {"verifier", "fingerprint", "audit_metadata"}:
706
+ if value is None:
707
+ safe[f"{key}_digest"] = None
708
+ elif isinstance(value, bytes):
709
+ safe[f"{key}_digest"] = hashlib.sha256(value).hexdigest()
710
+ else:
711
+ safe[f"{key}_digest"] = hashlib.sha256(str(value).encode("utf-8")).hexdigest()
712
+ else:
713
+ safe[key] = value
714
+ return safe
715
+
716
+
717
+ def _historical_fixture_components(baseline: Path, central: Path) -> list[dict[str, object]]:
718
+ """Mechanically recognize only the four authorized historical test subjects."""
719
+ credentials = ("credential_id", "consumer_id", "project_id", "verifier", "fingerprint", "issued_at", "expires_at", "revoked_at", "replaced_by_credential_id")
720
+ registrations = ("consumer_id", "project_id", "status", "created_at", "updated_at", "disabled_at", "revoked_at", "audit_metadata")
721
+ baseline_pair = _consumer_authority_tables_for_path(baseline)
722
+ central_pair = _consumer_authority_tables_for_path(central)
723
+ if baseline_pair is None or central_pair is None:
724
+ raise CutoverError("CONTAMINATION_PROVENANCE_UNRESOLVED", "consumer authority tables are unavailable")
725
+ old_credentials = _authority_rows(baseline, baseline_pair[0], credentials, ("credential_id",))
726
+ new_credentials = _authority_rows(central, central_pair[0], credentials, ("credential_id",))
727
+ old_registrations = _authority_rows(baseline, baseline_pair[1], registrations, ("consumer_id", "project_id"))
728
+ new_registrations = _authority_rows(central, central_pair[1], registrations, ("consumer_id", "project_id"))
729
+ if any(old_credentials[key] != new_credentials.get(key) for key in old_credentials) or any(old_registrations[key] != new_registrations.get(key) for key in old_registrations):
730
+ raise CutoverError("CONTAMINATION_PROVENANCE_UNRESOLVED", "baseline authority rows changed")
731
+ if set(new_credentials) - set(old_credentials) and not set(old_credentials) <= set(new_credentials):
732
+ raise CutoverError("CONTAMINATION_PROVENANCE_UNRESOLVED", "credential removal")
733
+ if set(new_registrations) - set(old_registrations) and not set(old_registrations) <= set(new_registrations):
734
+ raise CutoverError("CONTAMINATION_PROVENANCE_UNRESOLVED", "registration removal")
735
+ added_credentials = [row for key, row in new_credentials.items() if key not in old_credentials]
736
+ added_registrations = [row for key, row in new_registrations.items() if key not in old_registrations]
737
+ groups: dict[tuple[str, str], dict[str, list[dict[str, object]]]] = {}
738
+ for row in added_credentials:
739
+ groups.setdefault((str(row["consumer_id"]), str(row["project_id"])), {"credentials": [], "registrations": []})["credentials"].append(row)
740
+ for row in added_registrations:
741
+ groups.setdefault((str(row["consumer_id"]), str(row["project_id"])), {"credentials": [], "registrations": []})["registrations"].append(row)
742
+ expected = {
743
+ ("workspace-client", "project-alpha"), ("consumer", "project"),
744
+ ("rotate", "project"), ("qualification-client", "qualification-project"),
745
+ }
746
+ if set(groups) != expected:
747
+ raise CutoverError("CONTAMINATION_PROVENANCE_UNRESOLVED", "authority components are not the authorized historical fixture set")
748
+ components: list[dict[str, object]] = []
749
+ for subject in sorted(groups):
750
+ consumer_id, project_id = subject
751
+ values = groups[subject]
752
+ credentials_for_subject = sorted(values["credentials"], key=lambda row: str(row["credential_id"]))
753
+ registrations_for_subject = values["registrations"]
754
+ signals: list[str]
755
+ writer: str
756
+ if subject == ("workspace-client", "project-alpha"):
757
+ credential = credentials_for_subject[0] if len(credentials_for_subject) == 1 else None
758
+ if (credential is None or len(registrations_for_subject) != 1
759
+ or credential["credential_id"] != "credential-alpha" or credential["issued_at"] != "now"
760
+ or not isinstance(credential["verifier"], bytes) or len(credential["verifier"]) != 32
761
+ or not isinstance(credential["fingerprint"], bytes) or len(credential["fingerprint"]) != 32):
762
+ raise CutoverError("CONTAMINATION_PROVENANCE_UNRESOLVED", "workspace fixture mismatch")
763
+ signals, writer = ["exact_fixture_literals", "fixture_blob_shape"], "tests/engineering/test_central_store_migration.py"
764
+ elif subject == ("consumer", "project"):
765
+ credential = credentials_for_subject[0] if len(credentials_for_subject) == 1 else None
766
+ registration = registrations_for_subject[0] if len(registrations_for_subject) == 1 else None
767
+ if (credential is None or registration is None or credential["credential_id"] != "production-consumer"
768
+ or registration["status"] != "DISABLED" or credential["revoked_at"] is None
769
+ or registration["disabled_at"] is None or registration["audit_metadata"] != '{"action":"DISABLE"}'):
770
+ raise CutoverError("CONTAMINATION_PROVENANCE_UNRESOLVED", "consumer fixture mismatch")
771
+ signals, writer = ["exact_fixture_literals", "disable_revoke_lifecycle"], "tests/engineering/test_local_api_consumer_credentials.py"
772
+ elif subject == ("rotate", "project"):
773
+ credential_ids = {str(row["credential_id"]) for row in credentials_for_subject}
774
+ if (len(credentials_for_subject) != 2 or len(registrations_for_subject) != 1
775
+ or credential_ids != {"production-rotate-old", "production-rotate-new"}
776
+ or registrations_for_subject[0]["status"] != "ACTIVE"
777
+ or sum(row["revoked_at"] is None for row in credentials_for_subject) != 1):
778
+ raise CutoverError("CONTAMINATION_PROVENANCE_UNRESOLVED", "rotation fixture mismatch")
779
+ signals, writer = ["exact_fixture_literals", "rotation_lifecycle"], "tests/engineering/test_local_api_consumer_credentials.py"
780
+ else:
781
+ credential = credentials_for_subject[0] if len(credentials_for_subject) == 1 else None
782
+ try:
783
+ issued = datetime.strptime(str(credential["issued_at"]), "%Y-%m-%d %H:%M:%S") if credential else None
784
+ expires = datetime.strptime(str(credential["expires_at"]), "%Y-%m-%d %H:%M:%S") if credential else None
785
+ except ValueError as error:
786
+ raise CutoverError("CONTAMINATION_PROVENANCE_UNRESOLVED", "qualification fixture timestamp mismatch") from error
787
+ if (credential is None or registrations_for_subject or credential["credential_id"] != "qualification-fixture"
788
+ or issued is None or expires is None or expires - issued != timedelta(minutes=15)):
789
+ raise CutoverError("CONTAMINATION_PROVENANCE_UNRESOLVED", "qualification fixture mismatch")
790
+ signals, writer = ["exact_fixture_literals", "qualification_prefix_and_ttl"], "tests/engineering/test_local_api_qualification_credentials.py"
791
+ components.append({"consumer_id": consumer_id, "project_id": project_id, "credentials": [_safe_authority_row(row) for row in credentials_for_subject], "registrations": [_safe_authority_row(row) for row in registrations_for_subject], "test_writer": writer, "signals": signals})
792
+ return components
793
+
794
+
795
+ def _component_digest(components: list[dict[str, object]]) -> str:
796
+ return hashlib.sha256(json.dumps(components, ensure_ascii=False, separators=(",", ":"), sort_keys=True).encode("utf-8")).hexdigest()
797
+
798
+
799
+ def create_contamination_attestation(repo: Path, *, migration_id: str, operator: str) -> dict[str, object]:
800
+ """Persist one operator-owned, immutable external attestation for this incident."""
801
+ receipt = load_receipt(migration_id)
802
+ if receipt is None or receipt.get("state") != "SERVICES_RESTARTED" or not operator:
803
+ raise CutoverError("CONTAMINATION_PROVENANCE_UNRESOLVED", "eligible incident state and operator are required")
804
+ pointer_path = authority_pointer_path()
805
+ try:
806
+ pointer = json.loads(pointer_path.read_text(encoding="utf-8"))
807
+ except (OSError, json.JSONDecodeError) as error:
808
+ raise CutoverError("CONTAMINATION_PROVENANCE_UNRESOLVED", "authority pointer is unreadable") from error
809
+ legacy, central = Path(str(receipt.get("legacy_path", ""))), Path(str(pointer.get("authoritative_path", "")))
810
+ baseline = receipt.get("quiescent_source_baseline")
811
+ source = baseline.get("source") if isinstance(baseline, dict) else None
812
+ if not isinstance(source, dict) or _fingerprint(legacy) != source.get("fingerprint_sha256"):
813
+ raise CutoverError("LEGACY_BASELINE_MISMATCH")
814
+ components = _historical_fixture_components(legacy, central)
815
+ if any(len(component["signals"]) < 2 for component in components):
816
+ raise CutoverError("CONTAMINATION_PROVENANCE_UNRESOLVED", "weak fixture evidence")
817
+ with _readonly(central) as connection:
818
+ schema = _schema(connection, _tables(connection))
819
+ binding = {"migration_id": migration_id, "legacy_fingerprint_sha256": _fingerprint(legacy), "central_fingerprint_sha256": _fingerprint(central), "authority_pointer_fingerprint_sha256": _file_digest(pointer_path), "central_schema": schema, "delta_digest": _component_digest(components)}
820
+ payload = {"attestation_id": f"historical-contamination-{migration_id}", "attestation_version": HISTORICAL_ATTESTATION_VERSION, "classifier_version": HISTORICAL_ATTESTATION_CLASSIFIER_VERSION, "origin_class": "OPERATOR_FORENSIC_CONTROL", "operator": operator, "created_at": _now(), "binding": binding, "components": components, "evidence_strength": "TWO_OR_MORE_DETERMINISTIC_SIGNALS_PER_COMPONENT", "eligibility": "PROVEN_NON_PRODUCTION_CONTAMINATION"}
821
+ path = contamination_attestation_path(migration_id)
822
+ if path.exists():
823
+ raise CutoverError("CONTAMINATION_PROVENANCE_UNRESOLVED", "attestation is immutable and already exists")
824
+ _atomic_json(path, payload)
825
+ return payload
826
+
827
+
828
+ def _valid_contamination_attestation(repo: Path, *, migration_id: str, legacy: Path, central: Path, pointer_path: Path) -> dict[str, object] | None:
829
+ path = contamination_attestation_path(migration_id)
830
+ try:
831
+ payload = json.loads(path.read_text(encoding="utf-8"))
832
+ except (OSError, json.JSONDecodeError):
833
+ return None
834
+ if not isinstance(payload, dict) or payload.get("attestation_version") != HISTORICAL_ATTESTATION_VERSION or payload.get("classifier_version") != HISTORICAL_ATTESTATION_CLASSIFIER_VERSION:
835
+ return None
836
+ try:
837
+ components = _historical_fixture_components(legacy, central)
838
+ with _readonly(central) as connection:
839
+ schema = _schema(connection, _tables(connection))
840
+ expected = {"migration_id": migration_id, "legacy_fingerprint_sha256": _fingerprint(legacy), "central_fingerprint_sha256": _fingerprint(central), "authority_pointer_fingerprint_sha256": _file_digest(pointer_path), "central_schema": schema, "delta_digest": _component_digest(components)}
841
+ except CutoverError:
842
+ return None
843
+ return payload if payload.get("binding") == expected and payload.get("components") == components else None
844
+
845
+
846
+ def _quote_identifier(value: str) -> str:
847
+ return '"' + value.replace('"', '""') + '"'
848
+
849
+
850
+ def _normalized_value(value: object) -> object:
851
+ """Return a type-stable, deterministic representation of a SQLite value."""
852
+ if value is None:
853
+ return {"type": "null"}
854
+ if isinstance(value, bytes):
855
+ return {"type": "bytes", "hex": value.hex()}
856
+ if isinstance(value, str):
857
+ text = unicodedata.normalize("NFC", value)
858
+ try:
859
+ parsed = json.loads(text)
860
+ except json.JSONDecodeError:
861
+ return {"type": "text", "value": text}
862
+ if isinstance(parsed, (dict, list)):
863
+ return {"type": "json", "value": parsed}
864
+ return {"type": "text", "value": text}
865
+ if isinstance(value, bool):
866
+ return {"type": "boolean", "value": value}
867
+ if isinstance(value, int):
868
+ return {"type": "integer", "value": value}
869
+ if isinstance(value, float):
870
+ return {"type": "real", "value": value}
871
+ return {"type": type(value).__name__, "value": str(value)}
872
+
873
+
874
+ def _table_key(connection: sqlite3.Connection, table: str) -> tuple[str, ...]:
875
+ """Resolve a declared primary key or non-partial enforced unique key."""
876
+ quoted = _quote_identifier(table)
877
+ columns = connection.execute(f"PRAGMA table_info({quoted})").fetchall()
878
+ primary = tuple(str(row[1]) for row in sorted(columns, key=lambda row: int(row[5])) if int(row[5]))
879
+ if primary:
880
+ return primary
881
+ candidates: list[tuple[str, ...]] = []
882
+ for index in connection.execute(f"PRAGMA index_list({quoted})"):
883
+ # seq, name, unique, origin, partial
884
+ if not int(index[2]) or (len(index) > 4 and int(index[4])):
885
+ continue
886
+ index_name = _quote_identifier(str(index[1]))
887
+ key = tuple(str(row[2]) for row in connection.execute(f"PRAGMA index_info({index_name})"))
888
+ if key:
889
+ candidates.append(key)
890
+ if candidates:
891
+ return min(candidates, key=lambda key: (len(key), key))
892
+ raise CutoverError("CONTAMINATION_PROVENANCE_UNRESOLVED", f"run-bound table has no deterministic key: {table}")
893
+
894
+
895
+ def _row_map(connection: sqlite3.Connection, table: str, key: tuple[str, ...]) -> dict[str, dict[str, object]]:
896
+ quoted = _quote_identifier(table)
897
+ columns = tuple(str(row[1]) for row in connection.execute(f"PRAGMA table_info({quoted})"))
898
+ rows = connection.execute(f"SELECT * FROM {quoted}").fetchall()
899
+ result: dict[str, dict[str, object]] = {}
900
+ for row in rows:
901
+ raw = dict(zip(columns, row, strict=True))
902
+ key_value = [_normalized_value(raw[column]) for column in key]
903
+ encoded_key = json.dumps(key_value, ensure_ascii=False, separators=(",", ":"), sort_keys=True)
904
+ if encoded_key in result:
905
+ raise CutoverError("CONTAMINATION_PROVENANCE_UNRESOLVED", f"non-unique deterministic key: {table}")
906
+ result[encoded_key] = {column: _normalized_value(raw[column]) for column in sorted(raw)}
907
+ return result
908
+
909
+
910
+ def _run_bound_tables(connection: sqlite3.Connection) -> tuple[str, ...]:
911
+ tables = _tables(connection)
912
+ result = []
913
+ for table in tables:
914
+ columns = {str(row[1]) for row in connection.execute(f"PRAGMA table_info({_quote_identifier(table)})")}
915
+ if "run_id" in columns or (table == "execution_submissions" and "execution_run_id" in columns):
916
+ result.append(table)
917
+ return tuple(sorted(result))
918
+
919
+
920
+ def _row_deltas(baseline: Path, central: Path) -> dict[str, list[dict[str, object]]]:
921
+ """Compare all run-bound evidence rows by their declared stable identity."""
922
+ with _readonly(baseline) as baseline_connection, _readonly(central) as central_connection:
923
+ tables = set(_run_bound_tables(baseline_connection)) | set(_run_bound_tables(central_connection))
924
+ deltas: dict[str, list[dict[str, object]]] = {}
925
+ for table in sorted(tables):
926
+ baseline_present = table in _tables(baseline_connection)
927
+ central_present = table in _tables(central_connection)
928
+ key_connection = central_connection if central_present else baseline_connection
929
+ key = _table_key(key_connection, table)
930
+ baseline_rows = _row_map(baseline_connection, table, key) if baseline_present else {}
931
+ central_rows = _row_map(central_connection, table, key) if central_present else {}
932
+ changes: list[dict[str, object]] = []
933
+ for encoded_key in sorted(set(baseline_rows) | set(central_rows)):
934
+ before, after = baseline_rows.get(encoded_key), central_rows.get(encoded_key)
935
+ if before is None:
936
+ changes.append({"change_type": "ADDED", "key": json.loads(encoded_key), "row": after})
937
+ elif after is None:
938
+ changes.append({"change_type": "REMOVED", "key": json.loads(encoded_key), "row": before})
939
+ elif before != after:
940
+ changes.append({"change_type": "MODIFIED", "key": json.loads(encoded_key), "before": before, "after": after})
941
+ if changes:
942
+ deltas[table] = changes
943
+ return deltas
944
+
945
+
946
+ def _plain_value(value: object) -> object:
947
+ return value.get("value") if isinstance(value, dict) and "value" in value else None
948
+
949
+
950
+ def _row_text(row: object, column: str) -> str | None:
951
+ value = _plain_value(row.get(column)) if isinstance(row, dict) else None
952
+ return value if isinstance(value, str) and value else None
953
+
954
+
955
+ def _changed_run_ids(table: str, changes: list[dict[str, object]]) -> set[str]:
956
+ run_column = "execution_run_id" if table == "execution_submissions" else "run_id"
957
+ result: set[str] = set()
958
+ for change in changes:
959
+ rows = [change.get("row"), change.get("before"), change.get("after")]
960
+ for row in rows:
961
+ if isinstance(row, dict):
962
+ value = _plain_value(row.get(run_column))
963
+ if isinstance(value, str) and value:
964
+ result.add(value)
965
+ return result
966
+
967
+
968
+ def _lineage_category(table: str, change: dict[str, object]) -> str:
969
+ if table == "provider_recovery_attempts":
970
+ return "recovery"
971
+ if "provider" in table:
972
+ return "provider"
973
+ if "validation" in table:
974
+ return "validation"
975
+ if "qualification" in table:
976
+ return "qualification"
977
+ if table == "prompt_execution_history":
978
+ return "prompt_history"
979
+ if "reconciliation" in table:
980
+ return "reconciliation"
981
+ if table == "engineering_transactions":
982
+ rows = (change.get("row"), change.get("before"), change.get("after"))
983
+ for row in rows:
984
+ payload = _plain_value(row.get("payload")) if isinstance(row, dict) else None
985
+ if isinstance(payload, dict) and any("FINAL" in str(value).upper() for value in payload.values()):
986
+ return "finalization"
987
+ return "implementation"
988
+
989
+
990
+ def managed_lineage_attestation(baseline: Path, central: Path) -> dict[str, object]:
991
+ """Classify only row-level post-baseline nodes by their persisted roots."""
992
+ deltas = _row_deltas(baseline, central)
993
+ categories = {name: 0 for name in ("provider", "recovery", "validation", "qualification", "implementation", "finalization", "reconciliation", "prompt_history")}
994
+ changed_runs: dict[str, set[str]] = {}
995
+ for table, changes in deltas.items():
996
+ runs = _changed_run_ids(table, changes)
997
+ if runs:
998
+ changed_runs[table] = runs
999
+ for change in changes:
1000
+ categories[_lineage_category(table, change)] += 1
1001
+ with _readonly(central) as connection:
1002
+ tables = _tables(connection)
1003
+ producers: dict[str, set[str]] = {}
1004
+ if "execution_submissions" in tables:
1005
+ columns = {str(row[1]) for row in connection.execute("PRAGMA table_info(execution_submissions)")}
1006
+ if {"execution_run_id", "producer_type"} <= columns:
1007
+ for run_id, producer_type in connection.execute("SELECT execution_run_id,producer_type FROM execution_submissions WHERE execution_run_id IS NOT NULL"):
1008
+ producers.setdefault(str(run_id), set()).add(str(producer_type))
1009
+ if "execution_submission_links" in tables and "execution_submissions" in tables:
1010
+ for run_id, producer_type in connection.execute("SELECT link.run_id,submission.producer_type FROM execution_submission_links AS link JOIN execution_submissions AS submission ON submission.submission_id=link.submission_id"):
1011
+ producers.setdefault(str(run_id), set()).add(str(producer_type))
1012
+ nodes = {(table, run_id) for table, runs in changed_runs.items() for run_id in runs}
1013
+ # A new submission is itself a canonical root even before a run link has
1014
+ # been written. Treating it as invisible would permit a real post-cutover
1015
+ # human submission to evade the recovery gate during its earliest phase.
1016
+ submission_origins: dict[str, set[str]] = {}
1017
+ for change in deltas.get("execution_submissions", []):
1018
+ for row in (change.get("row"), change.get("before"), change.get("after")):
1019
+ submission_id, producer_type = _row_text(row, "submission_id"), _row_text(row, "producer_type")
1020
+ execution_run_id = _row_text(row, "execution_run_id")
1021
+ if submission_id and producer_type and execution_run_id is None:
1022
+ node = ("execution_submissions", f"submission:{submission_id}")
1023
+ nodes.add(node)
1024
+ submission_origins.setdefault(node[1], set()).add(producer_type)
1025
+ production_origins = {"HUMAN", "MANAGED", "ICLOUD", "HUMAN_OPERATOR"}
1026
+ test_origins = {"TEST_HARNESS"}
1027
+ def origins(node: tuple[str, str]) -> set[str]:
1028
+ return submission_origins.get(node[1], producers.get(node[1], set()))
1029
+
1030
+ production = {node for node in nodes if origins(node) & production_origins}
1031
+ test = {node for node in nodes if not (origins(node) & production_origins) and origins(node) & test_origins}
1032
+ unresolved = nodes - production - test
1033
+
1034
+ def components(node_set: set[tuple[str, str]]) -> set[str]:
1035
+ return {node[1] for node in node_set}
1036
+ return {
1037
+ "row_delta_version": 1,
1038
+ "changed_rows": {table: len(changes) for table, changes in deltas.items()},
1039
+ "changed_run_nodes": {table: sorted(runs) for table, runs in changed_runs.items()},
1040
+ "production_component_count": len(components(production)),
1041
+ "production_node_count": len(production),
1042
+ "unresolved_component_count": len(components(unresolved)),
1043
+ "unresolved_node_count": len(unresolved),
1044
+ "test_component_count": len(components(test)),
1045
+ "test_node_count": len(test),
1046
+ "categories": categories,
1047
+ }
1048
+
1049
+
1050
+ def contaminated_prewrite_status(repo: Path, *, migration_id: str) -> dict[str, object]:
1051
+ """Read-only eligibility for the narrowly bounded forensic recovery."""
1052
+ receipt = load_receipt(migration_id)
1053
+ if receipt is None or receipt.get("state") not in {"SERVICES_RESTARTED", "ROLLBACK_COMPLETED"}:
1054
+ raise CutoverError("CONTAMINATION_PROVENANCE_UNRESOLVED", "legal predecessor is SERVICES_RESTARTED")
1055
+ freeze = admission_status(repo)
1056
+ if freeze.get("state") != "ACTIVE" or freeze.get("migration_id") != migration_id:
1057
+ raise CutoverError("CONTAMINATION_PROVENANCE_UNRESOLVED", "matching active freeze is required")
1058
+ pointer_path = authority_pointer_path()
1059
+ try:
1060
+ pointer = json.loads(pointer_path.read_text(encoding="utf-8"))
1061
+ except (OSError, json.JSONDecodeError) as error:
1062
+ raise CutoverError("CONTAMINATION_PROVENANCE_UNRESOLVED", "central pointer is required") from error
1063
+ central = Path(str(pointer.get("authoritative_path", "")))
1064
+ legacy = Path(str(receipt.get("legacy_path", "")))
1065
+ if receipt.get("state") == "ROLLBACK_COMPLETED":
1066
+ return {"eligible": True, "idempotent": True, "state": "ROLLBACK_COMPLETED", "freeze": freeze, "authority": "LEGACY"}
1067
+ if pointer.get("migration_id") != migration_id or not central.is_file() or database_path(repo).resolve() != central.resolve():
1068
+ raise CutoverError("CONTAMINATION_PROVENANCE_UNRESOLVED", "central authority does not match migration")
1069
+ assessment = _central_write_assessment(central)
1070
+ if assessment["legitimate_write"]:
1071
+ raise CutoverError("LEGITIMATE_CENTRAL_WRITE_PRESENT")
1072
+ with _readonly(central) as central_connection:
1073
+ tables = _tables(central_connection)
1074
+ central_schema = _schema(central_connection, tables)
1075
+ provenance = "PROVEN_NON_PRODUCTION" if {"test_only_backup_probe", "unsupported_schema_marker"} & set(assessment["signals"]) and not assessment["unknown_mutation"] else "UNRESOLVED"
1076
+ if provenance != "PROVEN_NON_PRODUCTION":
1077
+ raise CutoverError("CONTAMINATION_PROVENANCE_UNRESOLVED")
1078
+ baseline = receipt.get("quiescent_source_baseline")
1079
+ source = baseline.get("source") if isinstance(baseline, dict) else receipt.get("source")
1080
+ if not legacy.is_file() or not isinstance(source, dict) or _fingerprint(legacy) != source.get("fingerprint_sha256"):
1081
+ raise CutoverError("LEGACY_BASELINE_MISMATCH")
1082
+ # Production and unresolved managed descendants always take precedence:
1083
+ # an historical attestation may explain only the bounded authority rows.
1084
+ lineage = managed_lineage_attestation(legacy, central)
1085
+ if lineage["production_node_count"]:
1086
+ raise CutoverError("LEGITIMATE_CENTRAL_WRITE_PRESENT")
1087
+ if lineage["unresolved_node_count"]:
1088
+ raise CutoverError("CONTAMINATION_PROVENANCE_UNRESOLVED", "orphan managed evidence")
1089
+ domain_attestation = authority_independent_baseline_attestation(legacy, central)
1090
+ historical_attestation = _valid_contamination_attestation(
1091
+ repo, migration_id=migration_id, legacy=legacy, central=central, pointer_path=pointer_path,
1092
+ )
1093
+ if any((domain_attestation["credential_delta"], domain_attestation["registration_delta"], domain_attestation["project_scope_delta"])) and historical_attestation is None:
1094
+ raise CutoverError("CONTAMINATION_PROVENANCE_UNRESOLVED", "authority-independent baseline delta")
1095
+ facts = inspect_source(StoreCandidate(str(central), str(central.resolve()), ("forensic_central",)))
1096
+ if facts.get("integrity") != "PASS":
1097
+ raise CutoverError("FORENSIC_CENTRAL_UNREADABLE")
1098
+ return {"eligible": True, "idempotent": False, "authority": "CENTRAL", "freeze": freeze, "central": {"path": str(central.resolve()), "fingerprint_sha256": _fingerprint(central), "schema": central_schema, "integrity": facts["integrity"], "critical_counts": _table_counts(central)}, "legacy": {"path": str(legacy.resolve()), "fingerprint_sha256": _fingerprint(legacy), "critical_counts": _table_counts(legacy), "project_scope": project_scope_inventory(legacy)}, "legitimate_write_assessment": assessment, "managed_lineage": lineage, "authority_independent_baseline": domain_attestation, "historical_contamination_attestation": historical_attestation, "contamination_provenance": provenance, "forensic_tables": sorted(tables)}
1099
+
1100
+
1101
+ def recover_contaminated_prewrite(repo: Path, *, migration_id: str, operator: str = "operator", services: LaunchAgentServiceControl | None = None) -> dict[str, object]:
1102
+ """Operator-only CENTRAL-to-LEGACY forensic recovery; never copies data."""
1103
+ status = contaminated_prewrite_status(repo, migration_id=migration_id)
1104
+ if status["idempotent"]:
1105
+ return load_receipt(migration_id) or status
1106
+ receipt = load_receipt(migration_id)
1107
+ assert receipt is not None
1108
+ central = Path(str(status["central"]["path"]))
1109
+ legacy = Path(str(status["legacy"]["path"]))
1110
+ control = services or LaunchAgentServiceControl()
1111
+ transition_receipt(receipt, "CONTAMINATED_RECOVERY_PRECHECK", recovery_class="CONTAMINATED_PRE_WRITE_CENTRAL_RECOVERY", recovery_precheck=status, operator=operator)
1112
+ try:
1113
+ for label in SERVICE_STOP_ORDER:
1114
+ control.stop(label)
1115
+ if not control.stopped(label):
1116
+ raise CutoverError("RECOVERY_SERVICE_QUIESCENCE_FAILED", label)
1117
+ except CutoverError as error:
1118
+ raise CutoverError("RECOVERY_SERVICE_QUIESCENCE_FAILED", error.code) from error
1119
+ transition_receipt(receipt, "RECOVERY_SERVICES_QUIESCED", service_quiescence={label: control.stopped(label) for label in SERVICE_STOP_ORDER})
1120
+ if _fingerprint(legacy) != status["legacy"]["fingerprint_sha256"]:
1121
+ raise CutoverError("LEGACY_BASELINE_MISMATCH")
1122
+ transition_receipt(receipt, "RECOVERY_LEGACY_VERIFIED", legacy_baseline=status["legacy"])
1123
+ transition_receipt(receipt, "ROLLBACK_IN_PROGRESS")
1124
+ try:
1125
+ pointer = write_authority_pointer(migration_id=migration_id, authority=legacy, legacy=legacy, state="ROLLBACK_COMPLETED")
1126
+ except CutoverError as error:
1127
+ raise CutoverError("RECOVERY_AUTHORITY_SWITCH_FAILED") from error
1128
+ central_after = _fingerprint(central)
1129
+ if central_after != status["central"]["fingerprint_sha256"]:
1130
+ raise CutoverError("RECOVERY_POSTCHECK_FAILED", "central changed")
1131
+ try:
1132
+ for label in SERVICE_START_ORDER:
1133
+ control.start(label)
1134
+ binding = service_binding_proof(repo, expected=legacy, services=SERVICE_START_ORDER)
1135
+ except CutoverError as error:
1136
+ raise CutoverError("RECOVERY_SERVICE_RESTART_FAILED", error.code) from error
1137
+ if not binding.get("consistent") or not all(control.running(label) for label in SERVICE_START_ORDER):
1138
+ raise CutoverError("RECOVERY_MIXED_BINDING")
1139
+ freeze = admission_status(repo)
1140
+ if freeze.get("state") != "ACTIVE" or freeze.get("migration_id") != migration_id:
1141
+ raise CutoverError("RECOVERY_POSTCHECK_FAILED", "freeze changed")
1142
+ return transition_receipt(receipt, "ROLLBACK_COMPLETED", rollback={"operator": operator, "authority_pointer": pointer, "timestamp": _now()}, central_forensic={**status["central"], "classification": "FORENSIC_CONTAMINATED_NON_AUTHORITATIVE", "contamination_provenance": status["contamination_provenance"], "legitimate_write_assessment": status["legitimate_write_assessment"], "fingerprint_after": central_after}, post_recovery_binding=binding, freeze_after=freeze)
1143
+
1144
+
1145
+ def abort_pre_handoff(
1146
+ repo: Path,
1147
+ *,
1148
+ migration_id: str,
1149
+ reason: str,
1150
+ operator: str = "operator",
1151
+ services: LaunchAgentServiceControl | None = None,
1152
+ ) -> dict[str, object]:
1153
+ """Retire one frozen migration before any backup, target, or authority handoff."""
1154
+ if reason not in ABORT_REASONS:
1155
+ raise CutoverError("ABORT_PRE_HANDOFF_FAILED", "reason is not allowed")
1156
+ try:
1157
+ freeze = admission_status(repo)
1158
+ except CutoverError as error:
1159
+ if authority_pointer_path().exists():
1160
+ raise CutoverError("ABORT_PRE_HANDOFF_FAILED", "authority handoff is present") from error
1161
+ raise CutoverError("ABORT_PRE_HANDOFF_FAILED", "freeze authority is unresolved") from error
1162
+ receipt = load_receipt(migration_id)
1163
+ if freeze.get("state") == "ACTIVE" and freeze.get("migration_id") != migration_id:
1164
+ raise CutoverError("ABORT_PRE_HANDOFF_FAILED", "active freeze belongs to another migration")
1165
+ if receipt is not None and receipt.get("state") == "ABORTED_PRE_HANDOFF":
1166
+ return {"migration_id": migration_id, "state": "ABORTED_PRE_HANDOFF", "already_aborted": True}
1167
+ if freeze.get("state") != "ACTIVE" or freeze.get("migration_id") != migration_id:
1168
+ raise CutoverError("ABORT_PRE_HANDOFF_FAILED", "matching active freeze is required")
1169
+ candidates = discover_legacy_stores(repo)
1170
+ if len(candidates) != 1:
1171
+ raise CutoverError("ABORT_PRE_HANDOFF_FAILED", "legacy authority is unresolved")
1172
+ source = Path(candidates[0].resolved_path)
1173
+ if database_path(repo).resolve() != source.resolve() or authority_pointer_path().exists():
1174
+ raise CutoverError("ABORT_PRE_HANDOFF_FAILED", "authority handoff is present")
1175
+ target_state = classify_target(central_store_path())["state"]
1176
+ backups = installation_data_root() / "backups"
1177
+ if target_state != "ABSENT" or (backups.exists() and any(backups.glob(f"*{migration_id}*"))):
1178
+ raise CutoverError("ABORT_PRE_HANDOFF_FAILED", "target or backup is present")
1179
+ if receipt is not None and receipt.get("state") not in ABORTABLE_STATES:
1180
+ raise CutoverError("ABORT_PRE_HANDOFF_FAILED", "migration is beyond the abort boundary")
1181
+ quiescence = inspect_quiescence(source, pre_stop=True, services=services or LaunchAgentServiceControl())
1182
+ if not quiescence["eligible"]:
1183
+ raise CutoverError("ABORT_PRE_HANDOFF_FAILED", "active execution state is unsafe")
1184
+ if receipt is None:
1185
+ receipt = {
1186
+ "receipt_version": 1,
1187
+ "migration_id": migration_id,
1188
+ "schema": EXPECTED_SCHEMA,
1189
+ "legacy_path": str(source),
1190
+ "historical_freeze": freeze,
1191
+ "pre_stop_source": asdict(source_identity(candidates[0])),
1192
+ "rollback_mode": "NOT_REQUIRED_PRE_HANDOFF",
1193
+ }
1194
+ transition_receipt(receipt, "PRECHECK")
1195
+ transition_receipt(receipt, "ADMISSION_FROZEN", admission_freeze=freeze)
1196
+ receipt.setdefault("historical_freeze", freeze)
1197
+ return transition_receipt(
1198
+ receipt,
1199
+ "ABORTED_PRE_HANDOFF",
1200
+ abort={
1201
+ "reason": reason,
1202
+ "operator": operator,
1203
+ "timestamp": _now(),
1204
+ "authority": "LEGACY",
1205
+ "central_target_state": target_state,
1206
+ "authority_pointer": "ABSENT",
1207
+ "quiescence": quiescence,
1208
+ "tool_version": TOOL_VERSION,
1209
+ },
1210
+ )
1211
+
1212
+
1213
+ def authority_pointer_path() -> Path:
1214
+ return installation_data_root() / "runtime" / "store-authority.json"
1215
+
1216
+
1217
+ def write_authority_pointer(*, migration_id: str, authority: Path, legacy: Path, state: str) -> dict[str, object]:
1218
+ if state not in STATES or not authority.is_file():
1219
+ raise CutoverError("AUTHORITY_SWITCH_FAILED")
1220
+ payload = {"version": POINTER_VERSION, "migration_id": migration_id, "authoritative_path": str(authority.resolve()), "legacy_path": str(legacy.resolve()), "schema": EXPECTED_SCHEMA, "state": state, "timestamp": _now(), "fingerprint_sha256": _fingerprint(authority)}
1221
+ _atomic_json(authority_pointer_path(), payload)
1222
+ return payload
1223
+
1224
+
1225
+ def copy_snapshot(source: Path, destination: Path) -> None:
1226
+ """Create an fsynced SQLite backup snapshot, never a raw file copy."""
1227
+ temporary = destination.with_name(f".{destination.name}.{uuid.uuid4().hex}.tmp")
1228
+ destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
1229
+ try:
1230
+ with _readonly(source) as read_connection, sqlite3.connect(temporary) as write_connection:
1231
+ read_connection.backup(write_connection)
1232
+ with temporary.open("rb") as handle:
1233
+ os.fsync(handle.fileno())
1234
+ os.replace(temporary, destination)
1235
+ except (OSError, sqlite3.DatabaseError) as error:
1236
+ raise CutoverError("BACKUP_FAILED", str(error)) from error
1237
+ finally:
1238
+ if temporary.exists():
1239
+ temporary.unlink()
1240
+
1241
+
1242
+ def service_binding_proof(repo: Path, *, expected: Path, services: tuple[str, ...] = SERVICE_STOP_ORDER) -> dict[str, object]:
1243
+ """All runtime surfaces share storage.database_path; mixed paths block."""
1244
+ try:
1245
+ resolved = database_path(repo).resolve()
1246
+ except Exception as error:
1247
+ raise CutoverError("CENTRAL_STORE_NOT_IN_USE") from error
1248
+ consistent = resolved == expected.resolve()
1249
+ return {"consistent": consistent, "authoritative_store": str(resolved), "services": {label: str(resolved) for label in services}}
1250
+
1251
+
1252
+ def quiescent_source_baseline(candidate: StoreCandidate) -> dict[str, object]:
1253
+ """Capture the one authoritative source identity after strict quiescence."""
1254
+ source = Path(candidate.resolved_path)
1255
+ facts = inspect_source(candidate)
1256
+ scope = project_scope_inventory(source)
1257
+ if facts["blocking_codes"] or scope["blocking_codes"]:
1258
+ raise CutoverError("SOURCE_CHANGED_AFTER_PREFLIGHT")
1259
+ return {
1260
+ "state": "QUIESCENT_SOURCE_BASELINE",
1261
+ "captured_at": _now(),
1262
+ "source": asdict(source_identity(candidate)),
1263
+ "integrity": facts["integrity"],
1264
+ "critical_table_counts": _table_counts(source),
1265
+ "project_scope": scope,
1266
+ }
1267
+
1268
+
1269
+ def _require_quiescent_source_stable(candidate: StoreCandidate, baseline: dict[str, object]) -> StoreIdentity:
1270
+ identity = baseline.get("source")
1271
+ if not isinstance(identity, dict) or not _same_source_content(source_identity(candidate), identity):
1272
+ raise CutoverError("SOURCE_CHANGED_AFTER_PREFLIGHT")
1273
+ return source_identity(candidate)
1274
+
1275
+
1276
+ def controlled_cutover(repo: Path, *, operator: str = "operator", services: LaunchAgentServiceControl | None = None) -> dict[str, object]:
1277
+ """Perform one frozen cutover transaction through staged handoff gates."""
1278
+ candidate = discover_legacy_stores(repo)
1279
+ if len(candidate) != 1:
1280
+ raise CutoverError("QUIESCENCE_FAILED")
1281
+ source = Path(candidate[0].resolved_path)
1282
+ freeze = admission_status(repo)
1283
+ migration_id = freeze.get("migration_id")
1284
+ if freeze.get("state") != "ACTIVE" or not isinstance(migration_id, str) or not migration_id:
1285
+ raise CutoverError("ADMISSION_FREEZE_FAILED")
1286
+ existing = load_receipt(migration_id)
1287
+ if existing is not None and existing.get("state") not in {"PRECHECK", "ADMISSION_FROZEN", "QUIESCENT_SOURCE_BASELINE"}:
1288
+ if existing.get("state") in STATES:
1289
+ return existing
1290
+ raise CutoverError("AUTHORITY_SWITCH_FAILED", "migration receipt is invalid")
1291
+ receipt: dict[str, object] = existing or {
1292
+ "receipt_version": 1, "migration_id": migration_id, "schema": EXPECTED_SCHEMA,
1293
+ "operator": operator, "legacy_path": str(source), "rollback_mode": "PRE_WRITE_DIRECT",
1294
+ }
1295
+ if receipt.get("state") != "QUIESCENT_SOURCE_BASELINE":
1296
+ # A live watcher/dashboard lock is expected before maintenance, but a
1297
+ # failed pre-baseline attempt may already have durably unloaded every
1298
+ # owned LaunchAgent. That bounded state is safe to resume only when
1299
+ # the strict post-stop gate proves every service is still stopped.
1300
+ pre_stop = inspect_quiescence(source, pre_stop=True, services=services)
1301
+ already_quiesced = False
1302
+ if not pre_stop["eligible"]:
1303
+ post_stop = inspect_quiescence(source, services=services)
1304
+ services_stopped = services is not None and all(services.stopped(label) for label in SERVICE_STOP_ORDER)
1305
+ already_quiesced = existing is not None and post_stop["eligible"] and services_stopped
1306
+ if not already_quiesced:
1307
+ raise CutoverError("QUIESCENCE_FAILED")
1308
+ if services is not None and not already_quiesced:
1309
+ for label in SERVICE_STOP_ORDER:
1310
+ services.stop(label)
1311
+ if not services.stopped(label):
1312
+ raise CutoverError("SERVICE_STOP_FAILED", label)
1313
+ quiescence = inspect_quiescence(source, services=services)
1314
+ if not quiescence["eligible"]:
1315
+ raise CutoverError("QUIESCENCE_FAILED")
1316
+ baseline = quiescent_source_baseline(candidate[0])
1317
+ transition_receipt(
1318
+ receipt,
1319
+ "QUIESCENT_SOURCE_BASELINE",
1320
+ quiescence=quiescence,
1321
+ source=baseline["source"],
1322
+ quiescent_source_baseline=baseline,
1323
+ )
1324
+ else:
1325
+ quiescence = inspect_quiescence(source, services=services)
1326
+ if not quiescence["eligible"]:
1327
+ raise CutoverError("QUIESCENCE_FAILED")
1328
+ baseline = receipt.get("quiescent_source_baseline")
1329
+ if not isinstance(baseline, dict):
1330
+ raise CutoverError("AUTHORITY_SWITCH_FAILED", "quiescent source baseline is missing")
1331
+ _require_quiescent_source_stable(candidate[0], baseline)
1332
+ target = central_store_path()
1333
+ if classify_target(target)["state"] != "ABSENT":
1334
+ raise CutoverError("TARGET_CREATE_FAILED")
1335
+ _require_quiescent_source_stable(candidate[0], baseline)
1336
+ backup = installation_data_root() / "backups" / f"legacy-schema40-{datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')}-{migration_id}.db"
1337
+ copy_snapshot(source, backup)
1338
+ if not validate_target_equivalence(source, backup)["equivalent"]:
1339
+ raise CutoverError("BACKUP_FAILED")
1340
+ transition_receipt(receipt, "BACKUP_VERIFIED", backup={"path": str(backup), "fingerprint_sha256": _fingerprint(backup)})
1341
+ copy_snapshot(source, target)
1342
+ transition_receipt(receipt, "CENTRAL_STORE_CREATED", target={"path": str(target)})
1343
+ equivalent = validate_target_equivalence(source, target)
1344
+ if not equivalent["equivalent"]:
1345
+ raise CutoverError("TARGET_EQUIVALENCE_FAILED")
1346
+ transition_receipt(receipt, "TARGET_VERIFIED", equivalence=equivalent)
1347
+ pointer = write_authority_pointer(migration_id=migration_id, authority=target, legacy=source, state="AUTHORITY_SWITCHED")
1348
+ transition_receipt(receipt, "AUTHORITY_SWITCHED", authority_pointer=pointer)
1349
+ if services is not None:
1350
+ for label in SERVICE_START_ORDER:
1351
+ services.start(label)
1352
+ transition_receipt(receipt, "SERVICES_RESTARTED", service_binding=service_binding_proof(repo, expected=target))
1353
+ return receipt
1354
+
1355
+
1356
+ def rollback(repo: Path, *, migration_id: str, operator: str = "operator") -> dict[str, object]:
1357
+ """Restore legacy authority only before the first central production write."""
1358
+ receipt = load_receipt(migration_id)
1359
+ if receipt is None or receipt.get("state") != "LEGACY_ROLLBACK_COMPATIBLE":
1360
+ raise CutoverError("DIRECT_ROLLBACK_NOT_SAFE")
1361
+ freeze = admission_status(repo)
1362
+ if freeze.get("state") != "ACTIVE" or freeze.get("migration_id") != migration_id:
1363
+ raise CutoverError("ROLLBACK_FAILED", "freeze is not active")
1364
+ legacy = Path(str(receipt["legacy_path"]))
1365
+ source = receipt.get("source")
1366
+ if not legacy.is_file() or not isinstance(source, dict) or _fingerprint(legacy) != source.get("fingerprint_sha256"):
1367
+ raise CutoverError("ROLLBACK_FAILED", "legacy identity changed")
1368
+ target = central_store_path()
1369
+ if not inspect_quiescence(target)["eligible"]:
1370
+ raise CutoverError("ROLLBACK_FAILED", "central is not quiescent")
1371
+ pointer = write_authority_pointer(migration_id=migration_id, authority=legacy, legacy=legacy, state="LEGACY_ROLLBACK_COMPATIBLE")
1372
+ receipt["rollback"] = {"operator": operator, "timestamp": _now(), "authority_pointer": pointer}
1373
+ _atomic_json(receipt_path(migration_id), receipt)
1374
+ return receipt
1375
+
1376
+
1377
+ def _desired_state_matches(repo: Path) -> bool:
1378
+ """Fail closed until a current Engineering Platform verifier is supplied.
1379
+
1380
+ The predecessor host bootstrap is intentionally not a current EP authority
1381
+ or readiness oracle. Callers may inject the installed EP verification
1382
+ boundary explicitly; this legacy migration module never invokes it.
1383
+ """
1384
+ del repo
1385
+ return False
1386
+
1387
+
1388
+ def stage_a_readiness(
1389
+ repo: Path,
1390
+ *,
1391
+ migration_id: str,
1392
+ receipt: dict[str, object],
1393
+ services: LaunchAgentServiceControl | None = None,
1394
+ desired_state_check: object | None = None,
1395
+ ) -> dict[str, object]:
1396
+ """Prove the read-only Stage-A gates without changing runtime authority."""
1397
+ target = central_store_path()
1398
+ control = services or LaunchAgentServiceControl()
1399
+ try:
1400
+ running = {label: control.running(label) for label in SERVICE_START_ORDER}
1401
+ except Exception:
1402
+ raise CutoverError("POST_CUTOVER_READINESS_FAILED") from None
1403
+ try:
1404
+ pointer = json.loads(authority_pointer_path().read_text(encoding="utf-8"))
1405
+ except (OSError, json.JSONDecodeError):
1406
+ pointer = {}
1407
+ try:
1408
+ binding = service_binding_proof(repo, expected=target, services=SERVICE_START_ORDER)
1409
+ facts = inspect_source(StoreCandidate(str(target), str(target.resolve()), ("central_authority",)))
1410
+ freeze = admission_status(repo)
1411
+ resolved_authority = database_path(repo).resolve()
1412
+ except CutoverError:
1413
+ raise
1414
+ except (OSError, ValueError):
1415
+ raise CutoverError("POST_CUTOVER_READINESS_FAILED") from None
1416
+
1417
+ baseline = receipt.get("quiescent_source_baseline")
1418
+ baseline_source = baseline.get("source") if isinstance(baseline, dict) else None
1419
+ legacy = Path(str(receipt.get("legacy_path", "")))
1420
+ try:
1421
+ legacy_unchanged = (
1422
+ legacy.is_file()
1423
+ and isinstance(baseline_source, dict)
1424
+ and _same_source_content(
1425
+ source_identity(StoreCandidate(str(legacy), str(legacy.resolve()), ("legacy_authority",))),
1426
+ baseline_source,
1427
+ )
1428
+ )
1429
+ except OSError:
1430
+ legacy_unchanged = False
1431
+ equivalence = receipt.get("equivalence")
1432
+ verified_equivalence = isinstance(equivalence, dict) and equivalence.get("equivalent") is True
1433
+ pointer_matches = (
1434
+ pointer.get("migration_id") == migration_id
1435
+ and pointer.get("authoritative_path") == str(target.resolve())
1436
+ and pointer.get("schema") == EXPECTED_SCHEMA
1437
+ )
1438
+ checker = desired_state_check or _desired_state_matches
1439
+ try:
1440
+ desired_state_match = bool(checker(repo))
1441
+ except Exception:
1442
+ desired_state_match = False
1443
+ result = {
1444
+ "authority": "CENTRAL" if pointer_matches and resolved_authority == target.resolve() else "NOT_CENTRAL",
1445
+ "central_integrity": facts.get("integrity"),
1446
+ "central_schema": facts.get("schema_version"),
1447
+ "service_binding": binding,
1448
+ "services": running,
1449
+ "desired_state": "MATCH" if desired_state_match else "NOT_MATCH",
1450
+ "legacy_unchanged": legacy_unchanged,
1451
+ "freeze": freeze.get("state"),
1452
+ "pre_write_rollback_safe": receipt.get("rollback_mode") == "PRE_WRITE_DIRECT",
1453
+ "target_equivalence": verified_equivalence,
1454
+ "central_managed_production_writes": 0,
1455
+ }
1456
+ eligible = (
1457
+ result["authority"] == "CENTRAL"
1458
+ and not facts["blocking_codes"]
1459
+ and binding.get("consistent") is True
1460
+ and all(running.values())
1461
+ and desired_state_match
1462
+ and legacy_unchanged
1463
+ and freeze.get("state") == "ACTIVE"
1464
+ and freeze.get("migration_id") == migration_id
1465
+ and result["pre_write_rollback_safe"]
1466
+ and verified_equivalence
1467
+ )
1468
+ result["eligible"] = eligible
1469
+ return result
1470
+
1471
+
1472
+ def complete_stage_a(
1473
+ repo: Path,
1474
+ *,
1475
+ migration_id: str,
1476
+ services: LaunchAgentServiceControl | None = None,
1477
+ desired_state_check: object | None = None,
1478
+ ) -> dict[str, object]:
1479
+ """Record Stage-A only after the persisted restart and read-only gates pass."""
1480
+ receipt = load_receipt(migration_id)
1481
+ if receipt is None:
1482
+ raise CutoverError("POST_CUTOVER_READINESS_FAILED")
1483
+ if receipt.get("state") == "LEGACY_ROLLBACK_COMPATIBLE":
1484
+ return receipt
1485
+ if receipt.get("state") != "SERVICES_RESTARTED":
1486
+ raise CutoverError("POST_CUTOVER_READINESS_FAILED")
1487
+ readiness = stage_a_readiness(
1488
+ repo,
1489
+ migration_id=migration_id,
1490
+ receipt=receipt,
1491
+ services=services,
1492
+ desired_state_check=desired_state_check,
1493
+ )
1494
+ if not readiness["eligible"]:
1495
+ raise CutoverError("POST_CUTOVER_READINESS_FAILED")
1496
+ transition_receipt(receipt, "POST_CUTOVER_VERIFIED", readonly_qualification=readiness)
1497
+ return transition_receipt(receipt, "LEGACY_ROLLBACK_COMPATIBLE", rollback_mode="PRE_WRITE_DIRECT")
1498
+
1499
+
1500
+ def mark_central_post_write(repo: Path) -> None:
1501
+ """One-way data-loss guard called only after an admitted central write."""
1502
+ pointer_path = authority_pointer_path()
1503
+ if not pointer_path.is_file():
1504
+ return
1505
+ try:
1506
+ pointer = json.loads(pointer_path.read_text(encoding="utf-8"))
1507
+ migration_id = str(pointer["migration_id"])
1508
+ except (OSError, KeyError, TypeError, json.JSONDecodeError) as error:
1509
+ raise CutoverError("CENTRAL_STORE_NOT_IN_USE") from error
1510
+ receipt = load_receipt(migration_id)
1511
+ if receipt is not None and receipt.get("state") == "LEGACY_ROLLBACK_COMPATIBLE":
1512
+ transition_receipt(receipt, "CENTRAL_STORE_ACTIVE_POST_WRITE", rollback_mode="REVERSE_MIGRATION_REQUIRED")
1513
+
1514
+
1515
+ def preflight(repo: Path, *, extra_runtime_roots: tuple[Path, ...] = ()) -> dict[str, object]:
1516
+ """Compute the complete migration plan strictly read-only."""
1517
+ candidates = discover_legacy_stores(repo, extra_runtime_roots=extra_runtime_roots)
1518
+ try:
1519
+ freeze = admission_status(repo)
1520
+ except CutoverError:
1521
+ freeze = {"state": "UNRESOLVED"}
1522
+ receipt: dict[str, object] = {"receipt_version": 1, "mode": "DRY_RUN", "timestamp": datetime.now(timezone.utc).isoformat(), "tool_version": TOOL_VERSION, "target_data_root": str(installation_data_root()), "target_store": classify_target(central_store_path()), "source_candidates": [asdict(item) for item in candidates], "blocking_codes": [], "service_stop_plan": ["inbox_watcher", "separately_managed_execution_service", "local_consumer_api", "dashboard_relay", "dashboard"], "admission_freeze": freeze}
1523
+ if not candidates:
1524
+ receipt["blocking_codes"].append("LEGACY_STORE_NOT_FOUND")
1525
+ receipt["eligible"] = False
1526
+ return receipt
1527
+ if len(candidates) > 1:
1528
+ receipt["blocking_codes"].append("LEGACY_STORE_AMBIGUOUS")
1529
+ receipt["eligible"] = False
1530
+ return receipt
1531
+ candidate = candidates[0]
1532
+ source = inspect_source(candidate)
1533
+ identity = source_identity(candidate)
1534
+ quiescence = inspect_quiescence(Path(candidate.resolved_path))
1535
+ inventory = project_scope_inventory(Path(candidate.resolved_path))
1536
+ backup = backup_readiness(identity, installation_data_root())
1537
+ try:
1538
+ critical_counts = _table_counts(Path(candidate.resolved_path))
1539
+ except (OSError, sqlite3.DatabaseError):
1540
+ # The source inspection above has already recorded the fail-closed
1541
+ # integrity finding. Preflight must report that finding, never crash
1542
+ # while attempting optional inventory detail for the same bad source.
1543
+ critical_counts = {}
1544
+ receipt.update({"source": source, "quiescence": quiescence, "backup_readiness": backup, "snapshot_strategy": snapshot_plan(source), "project_scope": inventory, "critical_table_counts": critical_counts})
1545
+ codes = list(source["blocking_codes"]) + list(quiescence["blocking_codes"]) + list(inventory["blocking_codes"])
1546
+ if backup["blocking_code"]:
1547
+ codes.append(backup["blocking_code"])
1548
+ target_code = receipt["target_store"].get("blocking_code")
1549
+ if target_code:
1550
+ codes.append(str(target_code))
1551
+ receipt["blocking_codes"] = sorted(set(codes))
1552
+ receipt["eligible"] = not receipt["blocking_codes"]
1553
+ return receipt
1554
+
1555
+
1556
+ def main(argv: list[str] | None = None) -> int:
1557
+ parser = argparse.ArgumentParser(prog="engineering-central-store-migration")
1558
+ parser.add_argument("command", choices=("preflight", "dry-run", "freeze", "freeze-status", "abort", "thaw", "cutover", "stage-a", "rollback", "recover-contaminated-prewrite", "create-contamination-attestation", "forensic-delta", "forensic-attribution", "forensic-attribution-v2", "status"))
1559
+ parser.add_argument("--repo", type=Path, default=Path.cwd())
1560
+ parser.add_argument("--json", action="store_true")
1561
+ parser.add_argument("--migration-id")
1562
+ parser.add_argument("--baseline", type=Path, help="read-only baseline SQLite database for forensic-delta")
1563
+ parser.add_argument("--candidate", type=Path, help="read-only candidate SQLite database for forensic-delta")
1564
+ parser.add_argument("--output", type=Path, help="optional JSON output file for forensic-delta")
1565
+ parser.add_argument("--report", type=Path, help="immutable forensic-delta JSON input for forensic-attribution")
1566
+ parser.add_argument("--expected-report-digest", help="required expected report digest for forensic-attribution")
1567
+ parser.add_argument("--evidence-bundle", type=Path, help="optional immutable ancestry evidence JSON for forensic-attribution")
1568
+ parser.add_argument("--strict", action="store_true", help="fail forensic-delta when a table has no deterministic key")
1569
+ parser.add_argument("--reason")
1570
+ parser.add_argument("--operator", default="operator")
1571
+ parser.add_argument("--execute", action="store_true", help="required for a mutating production operation")
1572
+ args = parser.parse_args(argv)
1573
+ repo = args.repo.resolve()
1574
+ try:
1575
+ if args.command == "forensic-delta":
1576
+ if not args.baseline or not args.candidate or not args.migration_id:
1577
+ parser.error("forensic-delta requires --baseline, --candidate, and --migration-id")
1578
+ result = export_forensic_delta(args.baseline, args.candidate, migration_id=args.migration_id)
1579
+ if args.output:
1580
+ args.output.write_text(canonical_report_json(result) + "\n", encoding="utf-8")
1581
+ if args.json:
1582
+ print(canonical_report_json(result))
1583
+ else:
1584
+ print(json.dumps(result, indent=2, sort_keys=True))
1585
+ return 2 if args.strict and result["summary"]["tables_key_unresolved"] else 0
1586
+ if args.command == "forensic-attribution":
1587
+ if not args.report or not args.expected_report_digest:
1588
+ parser.error("forensic-attribution requires --report and --expected-report-digest")
1589
+ bindings = None
1590
+ if args.evidence_bundle:
1591
+ bindings = json.loads(args.evidence_bundle.read_text(encoding="utf-8"))
1592
+ if not isinstance(bindings, dict):
1593
+ raise ForensicAttributionError("FORENSIC_EVIDENCE_BUNDLE_INVALID")
1594
+ result = load_and_attribute(args.report, repository_root=repo, expected_report_digest=args.expected_report_digest, evidence_bindings=bindings)
1595
+ if args.output:
1596
+ args.output.write_text(canonical_attribution_json(result) + "\n", encoding="utf-8")
1597
+ if args.json:
1598
+ print(canonical_attribution_json(result))
1599
+ else:
1600
+ print(json.dumps(result, indent=2, sort_keys=True))
1601
+ return 0
1602
+ if args.command == "forensic-attribution-v2":
1603
+ if not args.report or not args.expected_report_digest:
1604
+ parser.error("forensic-attribution-v2 requires --report and --expected-report-digest")
1605
+ result = load_and_enrich_v2(args.report, repository_root=repo, expected_attribution_digest=args.expected_report_digest)
1606
+ if args.output:
1607
+ args.output.write_text(canonical_attribution_v2_json(result) + "\n", encoding="utf-8")
1608
+ if args.json:
1609
+ print(canonical_attribution_v2_json(result))
1610
+ else:
1611
+ print(json.dumps(result, indent=2, sort_keys=True))
1612
+ return 0
1613
+ if args.command in {"preflight", "dry-run"}:
1614
+ result = preflight(repo)
1615
+ elif args.command == "freeze-status":
1616
+ result = admission_status(repo)
1617
+ elif args.command == "status":
1618
+ result = {"admission_freeze": admission_status(repo), "authority_pointer": str(authority_pointer_path()), "authoritative_store": str(database_path(repo))}
1619
+ elif args.command == "recover-contaminated-prewrite" and not args.execute:
1620
+ if not args.migration_id:
1621
+ raise CutoverError("CONTAMINATION_PROVENANCE_UNRESOLVED")
1622
+ result = contaminated_prewrite_status(repo, migration_id=args.migration_id)
1623
+ elif args.command == "create-contamination-attestation" and not args.execute:
1624
+ raise CutoverError("ADMISSION_FREEZE_FAILED", "--execute is required")
1625
+ elif not args.execute:
1626
+ raise CutoverError("ADMISSION_FREEZE_FAILED", "--execute is required")
1627
+ elif args.command == "freeze":
1628
+ if not args.reason:
1629
+ raise CutoverError("ADMISSION_FREEZE_FAILED")
1630
+ result = set_admission_freeze(repo, migration_id=args.migration_id, reason=args.reason, operator=args.operator)
1631
+ elif args.command == "abort":
1632
+ if not args.migration_id or not args.reason:
1633
+ raise CutoverError("ABORT_PRE_HANDOFF_FAILED")
1634
+ result = abort_pre_handoff(repo, migration_id=args.migration_id, reason=args.reason, operator=args.operator)
1635
+ elif args.command == "thaw":
1636
+ if not args.migration_id:
1637
+ raise CutoverError("THAW_FAILED")
1638
+ result = thaw_admission(repo, migration_id=args.migration_id, operator=args.operator)
1639
+ elif args.command == "rollback":
1640
+ if not args.migration_id:
1641
+ raise CutoverError("ROLLBACK_FAILED")
1642
+ result = rollback(repo, migration_id=args.migration_id, operator=args.operator)
1643
+ elif args.command == "recover-contaminated-prewrite":
1644
+ if not args.migration_id:
1645
+ raise CutoverError("CONTAMINATION_PROVENANCE_UNRESOLVED")
1646
+ result = recover_contaminated_prewrite(repo, migration_id=args.migration_id, operator=args.operator, services=LaunchAgentServiceControl())
1647
+ elif args.command == "create-contamination-attestation":
1648
+ if not args.migration_id:
1649
+ raise CutoverError("CONTAMINATION_PROVENANCE_UNRESOLVED")
1650
+ result = create_contamination_attestation(repo, migration_id=args.migration_id, operator=args.operator)
1651
+ elif args.command == "stage-a":
1652
+ if not args.migration_id:
1653
+ raise CutoverError("POST_CUTOVER_READINESS_FAILED")
1654
+ result = complete_stage_a(repo, migration_id=args.migration_id, services=LaunchAgentServiceControl())
1655
+ else:
1656
+ result = controlled_cutover(repo, operator=args.operator, services=LaunchAgentServiceControl())
1657
+ except (CutoverError, ForensicDeltaError, ForensicAttributionError, ForensicAttributionV2Error) as error:
1658
+ result = {"ok": False, "code": error.code}
1659
+ if args.json:
1660
+ print(json.dumps(result, sort_keys=True, separators=(",", ":")))
1661
+ else:
1662
+ print(error.code)
1663
+ return 2
1664
+ if args.json:
1665
+ print(json.dumps(result, sort_keys=True, separators=(",", ":")))
1666
+ else:
1667
+ print(json.dumps(result, sort_keys=True))
1668
+ return 0 if args.command not in {"preflight", "dry-run"} or result["eligible"] else 2
1669
+
1670
+
1671
+ if __name__ == "__main__":
1672
+ raise SystemExit(main())