fdai-deployment-cli 0.1.2__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 (203) hide show
  1. fdai_deployment_cli/__about__.py +3 -0
  2. fdai_deployment_cli/__init__.py +5 -0
  3. fdai_deployment_cli/__main__.py +5 -0
  4. fdai_deployment_cli/acr_publish.py +585 -0
  5. fdai_deployment_cli/aks_catalog_review.py +137 -0
  6. fdai_deployment_cli/aks_historical_reconciliation.py +732 -0
  7. fdai_deployment_cli/aks_job_environment.py +59 -0
  8. fdai_deployment_cli/aks_job_execution.py +794 -0
  9. fdai_deployment_cli/aks_kubernetes_defaults.py +66 -0
  10. fdai_deployment_cli/aks_preflight.py +422 -0
  11. fdai_deployment_cli/aks_readiness.py +220 -0
  12. fdai_deployment_cli/aks_service_update.py +180 -0
  13. fdai_deployment_cli/aks_workload_jobs.py +281 -0
  14. fdai_deployment_cli/application_state_adoption.py +347 -0
  15. fdai_deployment_cli/azure_actor.py +61 -0
  16. fdai_deployment_cli/azure_naming.py +252 -0
  17. fdai_deployment_cli/azure_retail_prices.py +173 -0
  18. fdai_deployment_cli/bootstrap_approval.py +99 -0
  19. fdai_deployment_cli/bootstrap_channel.py +321 -0
  20. fdai_deployment_cli/bootstrap_executor.py +311 -0
  21. fdai_deployment_cli/bootstrap_plan.py +237 -0
  22. fdai_deployment_cli/bootstrap_reconcile.py +402 -0
  23. fdai_deployment_cli/bootstrap_support.py +138 -0
  24. fdai_deployment_cli/bootstrap_worker.py +448 -0
  25. fdai_deployment_cli/bundle.py +352 -0
  26. fdai_deployment_cli/capacity.py +143 -0
  27. fdai_deployment_cli/catalog_review_profile.py +306 -0
  28. fdai_deployment_cli/cli.py +798 -0
  29. fdai_deployment_cli/cli_help.py +214 -0
  30. fdai_deployment_cli/cli_parser.py +791 -0
  31. fdai_deployment_cli/cli_plan.py +450 -0
  32. fdai_deployment_cli/compiler.py +99 -0
  33. fdai_deployment_cli/console_artifact.py +218 -0
  34. fdai_deployment_cli/console_config.py +214 -0
  35. fdai_deployment_cli/console_update.py +832 -0
  36. fdai_deployment_cli/contracts.py +307 -0
  37. fdai_deployment_cli/control_package.py +140 -0
  38. fdai_deployment_cli/deadline_transport.py +70 -0
  39. fdai_deployment_cli/deployment_cost.py +131 -0
  40. fdai_deployment_cli/deployment_deadline.py +28 -0
  41. fdai_deployment_cli/deployment_kit.py +790 -0
  42. fdai_deployment_cli/deployment_kit_cache.py +191 -0
  43. fdai_deployment_cli/deployment_progress.py +648 -0
  44. fdai_deployment_cli/development_authority_cli.py +56 -0
  45. fdai_deployment_cli/development_authority_profile.py +319 -0
  46. fdai_deployment_cli/doctor.py +136 -0
  47. fdai_deployment_cli/entitlement_preflight.py +68 -0
  48. fdai_deployment_cli/entitlement_transport.py +49 -0
  49. fdai_deployment_cli/entra_bootstrap.py +579 -0
  50. fdai_deployment_cli/entra_control_profile.py +214 -0
  51. fdai_deployment_cli/entra_graph.py +349 -0
  52. fdai_deployment_cli/entra_profiles.py +215 -0
  53. fdai_deployment_cli/entra_source.py +244 -0
  54. fdai_deployment_cli/execution_bundle.py +478 -0
  55. fdai_deployment_cli/execution_copies.py +63 -0
  56. fdai_deployment_cli/foundation_adoption.py +438 -0
  57. fdai_deployment_cli/foundation_adoption_evidence.py +245 -0
  58. fdai_deployment_cli/foundation_adoption_host.py +157 -0
  59. fdai_deployment_cli/foundation_adoption_transport.py +51 -0
  60. fdai_deployment_cli/foundation_failure.py +72 -0
  61. fdai_deployment_cli/foundation_image.py +168 -0
  62. fdai_deployment_cli/foundation_input.py +235 -0
  63. fdai_deployment_cli/foundation_output.py +154 -0
  64. fdai_deployment_cli/foundation_plan.py +381 -0
  65. fdai_deployment_cli/foundation_process.py +64 -0
  66. fdai_deployment_cli/foundation_progress.py +231 -0
  67. fdai_deployment_cli/github_actions.py +171 -0
  68. fdai_deployment_cli/github_apply_artifact.py +259 -0
  69. fdai_deployment_cli/github_artifact_io.py +48 -0
  70. fdai_deployment_cli/github_dispatch.py +248 -0
  71. fdai_deployment_cli/github_plan_artifact.py +181 -0
  72. fdai_deployment_cli/github_provider_artifact.py +170 -0
  73. fdai_deployment_cli/github_transport.py +46 -0
  74. fdai_deployment_cli/github_workflow_values.py +312 -0
  75. fdai_deployment_cli/hub_image_deploy.py +354 -0
  76. fdai_deployment_cli/hub_image_stage.py +162 -0
  77. fdai_deployment_cli/hub_image_trust.py +281 -0
  78. fdai_deployment_cli/hub_ownership.py +161 -0
  79. fdai_deployment_cli/hub_workload_handoff.py +264 -0
  80. fdai_deployment_cli/hub_workload_handoff_inputs.py +227 -0
  81. fdai_deployment_cli/identity_profile.py +227 -0
  82. fdai_deployment_cli/image_targets.py +197 -0
  83. fdai_deployment_cli/installation_scope.py +280 -0
  84. fdai_deployment_cli/license.py +311 -0
  85. fdai_deployment_cli/license_issue.py +239 -0
  86. fdai_deployment_cli/lifecycle_authorization.py +252 -0
  87. fdai_deployment_cli/lifecycle_commands.py +173 -0
  88. fdai_deployment_cli/lifecycle_configuration.py +722 -0
  89. fdai_deployment_cli/lifecycle_configuration_artifact.py +45 -0
  90. fdai_deployment_cli/lifecycle_configuration_cli.py +324 -0
  91. fdai_deployment_cli/lifecycle_configuration_document.py +74 -0
  92. fdai_deployment_cli/lifecycle_configuration_entities.py +146 -0
  93. fdai_deployment_cli/lifecycle_configuration_package.py +492 -0
  94. fdai_deployment_cli/lifecycle_configuration_publication.py +277 -0
  95. fdai_deployment_cli/lifecycle_configuration_publication_cli.py +191 -0
  96. fdai_deployment_cli/lifecycle_configuration_sealing.py +127 -0
  97. fdai_deployment_cli/lifecycle_enrollment.py +45 -0
  98. fdai_deployment_cli/lifecycle_infrastructure_agent.py +174 -0
  99. fdai_deployment_cli/lifecycle_installation_binding.py +65 -0
  100. fdai_deployment_cli/lifecycle_native.py +247 -0
  101. fdai_deployment_cli/lifecycle_native_reconciliation.py +568 -0
  102. fdai_deployment_cli/lifecycle_native_transition.py +339 -0
  103. fdai_deployment_cli/lifecycle_ownership_exclusion.py +78 -0
  104. fdai_deployment_cli/lifecycle_plan.py +800 -0
  105. fdai_deployment_cli/lifecycle_promotion.py +185 -0
  106. fdai_deployment_cli/lifecycle_release.py +308 -0
  107. fdai_deployment_cli/lifecycle_self_upgrade.py +202 -0
  108. fdai_deployment_cli/lifecycle_upgrade_bundle.py +176 -0
  109. fdai_deployment_cli/notification_recipients_input.py +222 -0
  110. fdai_deployment_cli/observer_artifact.py +106 -0
  111. fdai_deployment_cli/oci_archive.py +353 -0
  112. fdai_deployment_cli/offline_kit.py +797 -0
  113. fdai_deployment_cli/offline_prepare.py +135 -0
  114. fdai_deployment_cli/operational_evidence_verifier_input.py +575 -0
  115. fdai_deployment_cli/plan_input.py +101 -0
  116. fdai_deployment_cli/private_output.py +192 -0
  117. fdai_deployment_cli/profile.py +70 -0
  118. fdai_deployment_cli/progress.py +183 -0
  119. fdai_deployment_cli/readiness.py +286 -0
  120. fdai_deployment_cli/run_command_receiver.py +418 -0
  121. fdai_deployment_cli/runtime_build.py +386 -0
  122. fdai_deployment_cli/runtime_profile.py +287 -0
  123. fdai_deployment_cli/runtime_release.py +745 -0
  124. fdai_deployment_cli/runtime_stage.py +82 -0
  125. fdai_deployment_cli/runtime_support_installation.py +215 -0
  126. fdai_deployment_cli/simulation.py +80 -0
  127. fdai_deployment_cli/source_application_inputs.py +397 -0
  128. fdai_deployment_cli/source_azure.py +586 -0
  129. fdai_deployment_cli/source_change_cli.py +107 -0
  130. fdai_deployment_cli/source_change_parser.py +75 -0
  131. fdai_deployment_cli/source_change_plan.py +181 -0
  132. fdai_deployment_cli/source_changed_update.py +196 -0
  133. fdai_deployment_cli/source_deploy.py +170 -0
  134. fdai_deployment_cli/source_foundation.py +174 -0
  135. fdai_deployment_cli/source_host_tools.py +163 -0
  136. fdai_deployment_cli/source_image_stage.py +400 -0
  137. fdai_deployment_cli/source_input.py +173 -0
  138. fdai_deployment_cli/source_projection.py +164 -0
  139. fdai_deployment_cli/source_receiver.py +76 -0
  140. fdai_deployment_cli/source_recovery.py +264 -0
  141. fdai_deployment_cli/source_runtime.py +81 -0
  142. fdai_deployment_cli/source_runtime_support.py +368 -0
  143. fdai_deployment_cli/source_service_update.py +726 -0
  144. fdai_deployment_cli/source_service_update_cli.py +47 -0
  145. fdai_deployment_cli/source_snapshot.py +178 -0
  146. fdai_deployment_cli/source_teardown.py +574 -0
  147. fdai_deployment_cli/source_terraform.py +85 -0
  148. fdai_deployment_cli/source_transport.py +399 -0
  149. fdai_deployment_cli/standalone_aks_inventory.py +558 -0
  150. fdai_deployment_cli/standalone_aks_job_execution.py +769 -0
  151. fdai_deployment_cli/standalone_aks_nodepool_guard.py +141 -0
  152. fdai_deployment_cli/standalone_application.py +977 -0
  153. fdai_deployment_cli/standalone_application_completion.py +151 -0
  154. fdai_deployment_cli/standalone_application_input_recovery.py +107 -0
  155. fdai_deployment_cli/standalone_application_recovery.py +116 -0
  156. fdai_deployment_cli/standalone_approval_input.py +24 -0
  157. fdai_deployment_cli/standalone_catalog_checkpoint.py +122 -0
  158. fdai_deployment_cli/standalone_catalog_review.py +251 -0
  159. fdai_deployment_cli/standalone_checkpoint_failure.py +127 -0
  160. fdai_deployment_cli/standalone_console_publish.py +174 -0
  161. fdai_deployment_cli/standalone_database_principals.py +41 -0
  162. fdai_deployment_cli/standalone_deploy.py +638 -0
  163. fdai_deployment_cli/standalone_development_authority.py +166 -0
  164. fdai_deployment_cli/standalone_foundation_adoption.py +87 -0
  165. fdai_deployment_cli/standalone_foundation_transition.py +767 -0
  166. fdai_deployment_cli/standalone_host.py +4608 -0
  167. fdai_deployment_cli/standalone_host_rebind.py +226 -0
  168. fdai_deployment_cli/standalone_host_residual.py +104 -0
  169. fdai_deployment_cli/standalone_host_state.py +145 -0
  170. fdai_deployment_cli/standalone_host_values.py +563 -0
  171. fdai_deployment_cli/standalone_kubernetes_tools.py +100 -0
  172. fdai_deployment_cli/standalone_license_installation.py +177 -0
  173. fdai_deployment_cli/standalone_management_egress.py +83 -0
  174. fdai_deployment_cli/standalone_migration_evidence.py +38 -0
  175. fdai_deployment_cli/standalone_operational_evidence.py +356 -0
  176. fdai_deployment_cli/standalone_planned_outputs.py +147 -0
  177. fdai_deployment_cli/standalone_private_access_probe.py +100 -0
  178. fdai_deployment_cli/standalone_product_profile.py +110 -0
  179. fdai_deployment_cli/standalone_remote_prepare.py +442 -0
  180. fdai_deployment_cli/standalone_residual_apply.py +540 -0
  181. fdai_deployment_cli/standalone_residual_successor.py +431 -0
  182. fdai_deployment_cli/standalone_review.py +266 -0
  183. fdai_deployment_cli/standalone_service_update_finalization.py +116 -0
  184. fdai_deployment_cli/standalone_service_update_recovery.py +277 -0
  185. fdai_deployment_cli/standalone_stage_targets.py +325 -0
  186. fdai_deployment_cli/standalone_status.py +67 -0
  187. fdai_deployment_cli/standalone_terraform_environment.py +237 -0
  188. fdai_deployment_cli/standalone_transfer_cleanup.py +237 -0
  189. fdai_deployment_cli/standalone_trial_activation.py +193 -0
  190. fdai_deployment_cli/state.py +434 -0
  191. fdai_deployment_cli/state_handoff.py +253 -0
  192. fdai_deployment_cli/status_projection.py +97 -0
  193. fdai_deployment_cli/support_install.py +257 -0
  194. fdai_deployment_cli/target.py +46 -0
  195. fdai_deployment_cli/trust/capabilities.json +62 -0
  196. fdai_deployment_cli/trust/deployment-bundle-root.pub +3 -0
  197. fdai_deployment_cli/trust/deployment-release-root.pub +3 -0
  198. fdai_deployment_cli/trust/upstream-signing-key.pub +3 -0
  199. fdai_deployment_cli/trust_roots.py +50 -0
  200. fdai_deployment_cli-0.1.2.dist-info/METADATA +112 -0
  201. fdai_deployment_cli-0.1.2.dist-info/RECORD +203 -0
  202. fdai_deployment_cli-0.1.2.dist-info/WHEEL +4 -0
  203. fdai_deployment_cli-0.1.2.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,286 @@
1
+ """Aggregate evidence contract for subscription genesis readiness."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Mapping
7
+ from dataclasses import dataclass
8
+ from datetime import datetime, timedelta
9
+ from types import MappingProxyType
10
+
11
+ from fdai_deployment_cli.contracts import canonical_digest
12
+ from fdai_deployment_cli.progress import InventoryClosure
13
+
14
+ REQUIRED_READINESS_EVIDENCE = frozenset(
15
+ {
16
+ "foundation_plan",
17
+ "foundation_apply",
18
+ "foundation_readback",
19
+ "application_plan",
20
+ "application_apply",
21
+ "application_readback",
22
+ "migration_readback",
23
+ "semantic_readback",
24
+ "model_readback",
25
+ "inventory_readback",
26
+ "rollback_rehearsal",
27
+ "second_run_no_change",
28
+ "system_verification",
29
+ }
30
+ )
31
+ _SHA256 = re.compile(r"^[0-9a-f]{64}$")
32
+ _COMMIT = re.compile(r"^[0-9a-f]{40}$")
33
+ REQUIRED_TERRAFORM_ROOTS = frozenset(
34
+ {
35
+ "bootstrap",
36
+ "platform",
37
+ "service.core-control-plane",
38
+ "service.document-ingestion-api",
39
+ "service.document-processing-worker",
40
+ "service.isolated-executor",
41
+ "service.operator-service",
42
+ }
43
+ )
44
+ REQUIRED_SERVICE_MIGRATIONS = frozenset(
45
+ {
46
+ "core-control-plane",
47
+ "document-ingestion-api",
48
+ "document-processing-worker",
49
+ "isolated-executor",
50
+ "operator-service",
51
+ }
52
+ )
53
+ REQUIRED_POSTGRES_EXTENSIONS = frozenset({"pg_trgm", "plpgsql", "vector"})
54
+
55
+
56
+ @dataclass(frozen=True, slots=True)
57
+ class NoChangeReadback:
58
+ """Terraform change totals from the complete second genesis run."""
59
+
60
+ root_changes: Mapping[str, tuple[int, int, int]]
61
+
62
+ def __post_init__(self) -> None:
63
+ root_changes = dict(self.root_changes)
64
+ if set(root_changes) != REQUIRED_TERRAFORM_ROOTS:
65
+ raise ValueError("second-run readback MUST verify every Terraform root")
66
+ for values in root_changes.values():
67
+ if (
68
+ not isinstance(values, tuple)
69
+ or len(values) != 3
70
+ or any(
71
+ isinstance(value, bool) or not isinstance(value, int) or value < 0
72
+ for value in values
73
+ )
74
+ ):
75
+ raise ValueError("second-run change counts MUST be non-negative integer triples")
76
+ if values != (0, 0, 0):
77
+ raise ValueError("every Terraform root in the second genesis run MUST be no-change")
78
+ object.__setattr__(self, "root_changes", MappingProxyType(root_changes))
79
+
80
+ def to_mapping(self) -> dict[str, object]:
81
+ """Return canonical no-change counts."""
82
+
83
+ return {
84
+ "roots_total": len(self.root_changes),
85
+ "roots": {
86
+ root: {"add": values[0], "change": values[1], "destroy": values[2]}
87
+ for root, values in sorted(self.root_changes.items())
88
+ },
89
+ }
90
+
91
+
92
+ @dataclass(frozen=True, slots=True)
93
+ class DatabaseSemanticReadback:
94
+ """Independent database and semantic gates required before runtime readiness."""
95
+
96
+ expected_legacy_head: str
97
+ legacy_head: str
98
+ expected_service_heads: Mapping[str, str]
99
+ service_heads: Mapping[str, str]
100
+ extensions: tuple[str, ...]
101
+ expected_runtime_role_checks: tuple[str, ...]
102
+ runtime_role_checks: Mapping[str, bool]
103
+ ontology_release_digest: str
104
+ catalog_digest: str
105
+ defaults_digest: str
106
+ role_manifest_digest: str
107
+ expected_ontology_release_digest: str
108
+ expected_catalog_digest: str
109
+ expected_defaults_digest: str
110
+ expected_role_manifest_digest: str
111
+ shadow_only: bool
112
+ observer_distinct: bool
113
+
114
+ def __post_init__(self) -> None:
115
+ if (
116
+ not self.expected_legacy_head
117
+ or len(self.expected_legacy_head) > 128
118
+ or self.legacy_head != self.expected_legacy_head
119
+ ):
120
+ raise ValueError("legacy migration head is invalid")
121
+ expected_service_heads = dict(self.expected_service_heads)
122
+ service_heads = dict(self.service_heads)
123
+ if (
124
+ set(expected_service_heads) != REQUIRED_SERVICE_MIGRATIONS
125
+ or service_heads != expected_service_heads
126
+ ):
127
+ raise ValueError("database readback MUST contain every service migration head")
128
+ if any(not value or len(value) > 128 for value in service_heads.values()):
129
+ raise ValueError("service migration head is invalid")
130
+ object.__setattr__(
131
+ self,
132
+ "expected_service_heads",
133
+ MappingProxyType(expected_service_heads),
134
+ )
135
+ object.__setattr__(self, "service_heads", MappingProxyType(service_heads))
136
+ extensions = tuple(sorted(set(self.extensions)))
137
+ if not REQUIRED_POSTGRES_EXTENSIONS <= set(extensions):
138
+ raise ValueError("database readback is missing required PostgreSQL extensions")
139
+ object.__setattr__(self, "extensions", extensions)
140
+ role_checks = dict(self.runtime_role_checks)
141
+ expected_role_checks = tuple(sorted(set(self.expected_runtime_role_checks)))
142
+ if (
143
+ not expected_role_checks
144
+ or set(role_checks) != set(expected_role_checks)
145
+ or not all(
146
+ isinstance(name, str) and 0 < len(name) <= 128 and isinstance(value, bool) and value
147
+ for name, value in role_checks.items()
148
+ )
149
+ ):
150
+ raise ValueError("runtime role readback MUST contain only passing checks")
151
+ object.__setattr__(self, "expected_runtime_role_checks", expected_role_checks)
152
+ object.__setattr__(self, "runtime_role_checks", MappingProxyType(role_checks))
153
+ for label, value in (
154
+ ("ontology_release_digest", self.ontology_release_digest),
155
+ ("catalog_digest", self.catalog_digest),
156
+ ("defaults_digest", self.defaults_digest),
157
+ ("role_manifest_digest", self.role_manifest_digest),
158
+ ):
159
+ if _SHA256.fullmatch(value) is None:
160
+ raise ValueError(f"{label} MUST be a lowercase SHA-256")
161
+ expected_digests = (
162
+ self.expected_ontology_release_digest,
163
+ self.expected_catalog_digest,
164
+ self.expected_defaults_digest,
165
+ self.expected_role_manifest_digest,
166
+ )
167
+ if any(_SHA256.fullmatch(value) is None for value in expected_digests):
168
+ raise ValueError("expected semantic digests MUST be lowercase SHA-256 values")
169
+ if expected_digests != (
170
+ self.ontology_release_digest,
171
+ self.catalog_digest,
172
+ self.defaults_digest,
173
+ self.role_manifest_digest,
174
+ ):
175
+ raise ValueError("database semantic readback does not match the sealed manifest")
176
+ if not self.shadow_only:
177
+ raise ValueError("subscription genesis semantic defaults MUST remain shadow-only")
178
+ if not self.observer_distinct:
179
+ raise ValueError("database semantic readback observer MUST be independent")
180
+
181
+ def to_mapping(self) -> dict[str, object]:
182
+ """Return sanitized database and semantic readiness evidence."""
183
+
184
+ return {
185
+ "expected_manifest_digest": canonical_digest(
186
+ {
187
+ "legacy_head": self.expected_legacy_head,
188
+ "service_heads": dict(sorted(self.expected_service_heads.items())),
189
+ "runtime_role_checks": list(self.expected_runtime_role_checks),
190
+ "ontology_release_digest": self.expected_ontology_release_digest,
191
+ "catalog_digest": self.expected_catalog_digest,
192
+ "defaults_digest": self.expected_defaults_digest,
193
+ "role_manifest_digest": self.expected_role_manifest_digest,
194
+ }
195
+ ),
196
+ "legacy_head": self.legacy_head,
197
+ "service_heads": dict(sorted(self.service_heads.items())),
198
+ "extensions": list(self.extensions),
199
+ "runtime_role_checks": dict(sorted(self.runtime_role_checks.items())),
200
+ "ontology_release_digest": self.ontology_release_digest,
201
+ "catalog_digest": self.catalog_digest,
202
+ "defaults_digest": self.defaults_digest,
203
+ "role_manifest_digest": self.role_manifest_digest,
204
+ "shadow_only": self.shadow_only,
205
+ "observer_distinct": self.observer_distinct,
206
+ }
207
+
208
+
209
+ @dataclass(frozen=True, slots=True)
210
+ class GenesisReadinessReceipt:
211
+ """Sanitized proof set required before a run can transition to ready."""
212
+
213
+ source_commit: str
214
+ manifest_digest: str
215
+ target_binding: str
216
+ generated_at: str
217
+ evidence_digests: Mapping[str, str]
218
+ database_semantic: DatabaseSemanticReadback
219
+ inventory_closure: InventoryClosure
220
+ second_run: NoChangeReadback
221
+
222
+ def __post_init__(self) -> None:
223
+ if _COMMIT.fullmatch(self.source_commit) is None:
224
+ raise ValueError("readiness source_commit MUST be a lowercase commit SHA")
225
+ for label, value in (
226
+ ("manifest_digest", self.manifest_digest),
227
+ ("target_binding", self.target_binding),
228
+ ):
229
+ if _SHA256.fullmatch(value) is None:
230
+ raise ValueError(f"readiness {label} MUST be a lowercase SHA-256")
231
+ evidence_digests = dict(self.evidence_digests)
232
+ if set(evidence_digests) != REQUIRED_READINESS_EVIDENCE:
233
+ raise ValueError("readiness receipt evidence set is incomplete")
234
+ if any(
235
+ not isinstance(value, str) or _SHA256.fullmatch(value) is None
236
+ for value in evidence_digests.values()
237
+ ):
238
+ raise ValueError("readiness evidence digests MUST be lowercase SHA-256 values")
239
+ object.__setattr__(self, "evidence_digests", MappingProxyType(evidence_digests))
240
+ if not isinstance(self.generated_at, str):
241
+ raise ValueError("readiness generated_at MUST be ISO 8601")
242
+ try:
243
+ moment = datetime.fromisoformat(self.generated_at)
244
+ except ValueError as exc:
245
+ raise ValueError("readiness generated_at MUST be ISO 8601") from exc
246
+ if moment.tzinfo is None:
247
+ raise ValueError("readiness generated_at MUST be timezone-aware")
248
+ if moment.utcoffset() != timedelta(0):
249
+ raise ValueError("readiness generated_at MUST use UTC")
250
+ if not self.inventory_closure.complete:
251
+ raise ValueError(
252
+ "readiness inventory closure is incomplete: "
253
+ + ",".join(self.inventory_closure.blockers())
254
+ )
255
+
256
+ @property
257
+ def digest(self) -> str:
258
+ """Return the digest bound to the terminal provision event."""
259
+
260
+ return canonical_digest(self.to_mapping())
261
+
262
+ def to_mapping(self) -> dict[str, object]:
263
+ """Return stable receipt content without tenant or resource identifiers."""
264
+
265
+ return {
266
+ "schema_version": "fdai.genesis-readiness-receipt.v1",
267
+ "source_commit": self.source_commit,
268
+ "manifest_digest": self.manifest_digest,
269
+ "target_binding": self.target_binding,
270
+ "generated_at": self.generated_at,
271
+ "evidence_digests": dict(sorted(self.evidence_digests.items())),
272
+ "database_semantic": self.database_semantic.to_mapping(),
273
+ "inventory_closure": {
274
+ "subscription_root": self.inventory_closure.subscription_root,
275
+ "resource_type_filter": self.inventory_closure.resource_type_filter,
276
+ "final_fence": self.inventory_closure.final_fence,
277
+ "provider_coverage_complete": (self.inventory_closure.provider_coverage_complete),
278
+ "truncated": self.inventory_closure.truncated,
279
+ "active_generation_matches": self.inventory_closure.active_generation_matches,
280
+ "overlay_open": self.inventory_closure.overlay_open,
281
+ "child_sources_complete": self.inventory_closure.child_sources_complete,
282
+ "observer_distinct": self.inventory_closure.observer_distinct,
283
+ },
284
+ "second_run": self.second_run.to_mapping(),
285
+ "status": "verified",
286
+ }