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.
- fdai_deployment_cli/__about__.py +3 -0
- fdai_deployment_cli/__init__.py +5 -0
- fdai_deployment_cli/__main__.py +5 -0
- fdai_deployment_cli/acr_publish.py +585 -0
- fdai_deployment_cli/aks_catalog_review.py +137 -0
- fdai_deployment_cli/aks_historical_reconciliation.py +732 -0
- fdai_deployment_cli/aks_job_environment.py +59 -0
- fdai_deployment_cli/aks_job_execution.py +794 -0
- fdai_deployment_cli/aks_kubernetes_defaults.py +66 -0
- fdai_deployment_cli/aks_preflight.py +422 -0
- fdai_deployment_cli/aks_readiness.py +220 -0
- fdai_deployment_cli/aks_service_update.py +180 -0
- fdai_deployment_cli/aks_workload_jobs.py +281 -0
- fdai_deployment_cli/application_state_adoption.py +347 -0
- fdai_deployment_cli/azure_actor.py +61 -0
- fdai_deployment_cli/azure_naming.py +252 -0
- fdai_deployment_cli/azure_retail_prices.py +173 -0
- fdai_deployment_cli/bootstrap_approval.py +99 -0
- fdai_deployment_cli/bootstrap_channel.py +321 -0
- fdai_deployment_cli/bootstrap_executor.py +311 -0
- fdai_deployment_cli/bootstrap_plan.py +237 -0
- fdai_deployment_cli/bootstrap_reconcile.py +402 -0
- fdai_deployment_cli/bootstrap_support.py +138 -0
- fdai_deployment_cli/bootstrap_worker.py +448 -0
- fdai_deployment_cli/bundle.py +352 -0
- fdai_deployment_cli/capacity.py +143 -0
- fdai_deployment_cli/catalog_review_profile.py +306 -0
- fdai_deployment_cli/cli.py +798 -0
- fdai_deployment_cli/cli_help.py +214 -0
- fdai_deployment_cli/cli_parser.py +791 -0
- fdai_deployment_cli/cli_plan.py +450 -0
- fdai_deployment_cli/compiler.py +99 -0
- fdai_deployment_cli/console_artifact.py +218 -0
- fdai_deployment_cli/console_config.py +214 -0
- fdai_deployment_cli/console_update.py +832 -0
- fdai_deployment_cli/contracts.py +307 -0
- fdai_deployment_cli/control_package.py +140 -0
- fdai_deployment_cli/deadline_transport.py +70 -0
- fdai_deployment_cli/deployment_cost.py +131 -0
- fdai_deployment_cli/deployment_deadline.py +28 -0
- fdai_deployment_cli/deployment_kit.py +790 -0
- fdai_deployment_cli/deployment_kit_cache.py +191 -0
- fdai_deployment_cli/deployment_progress.py +648 -0
- fdai_deployment_cli/development_authority_cli.py +56 -0
- fdai_deployment_cli/development_authority_profile.py +319 -0
- fdai_deployment_cli/doctor.py +136 -0
- fdai_deployment_cli/entitlement_preflight.py +68 -0
- fdai_deployment_cli/entitlement_transport.py +49 -0
- fdai_deployment_cli/entra_bootstrap.py +579 -0
- fdai_deployment_cli/entra_control_profile.py +214 -0
- fdai_deployment_cli/entra_graph.py +349 -0
- fdai_deployment_cli/entra_profiles.py +215 -0
- fdai_deployment_cli/entra_source.py +244 -0
- fdai_deployment_cli/execution_bundle.py +478 -0
- fdai_deployment_cli/execution_copies.py +63 -0
- fdai_deployment_cli/foundation_adoption.py +438 -0
- fdai_deployment_cli/foundation_adoption_evidence.py +245 -0
- fdai_deployment_cli/foundation_adoption_host.py +157 -0
- fdai_deployment_cli/foundation_adoption_transport.py +51 -0
- fdai_deployment_cli/foundation_failure.py +72 -0
- fdai_deployment_cli/foundation_image.py +168 -0
- fdai_deployment_cli/foundation_input.py +235 -0
- fdai_deployment_cli/foundation_output.py +154 -0
- fdai_deployment_cli/foundation_plan.py +381 -0
- fdai_deployment_cli/foundation_process.py +64 -0
- fdai_deployment_cli/foundation_progress.py +231 -0
- fdai_deployment_cli/github_actions.py +171 -0
- fdai_deployment_cli/github_apply_artifact.py +259 -0
- fdai_deployment_cli/github_artifact_io.py +48 -0
- fdai_deployment_cli/github_dispatch.py +248 -0
- fdai_deployment_cli/github_plan_artifact.py +181 -0
- fdai_deployment_cli/github_provider_artifact.py +170 -0
- fdai_deployment_cli/github_transport.py +46 -0
- fdai_deployment_cli/github_workflow_values.py +312 -0
- fdai_deployment_cli/hub_image_deploy.py +354 -0
- fdai_deployment_cli/hub_image_stage.py +162 -0
- fdai_deployment_cli/hub_image_trust.py +281 -0
- fdai_deployment_cli/hub_ownership.py +161 -0
- fdai_deployment_cli/hub_workload_handoff.py +264 -0
- fdai_deployment_cli/hub_workload_handoff_inputs.py +227 -0
- fdai_deployment_cli/identity_profile.py +227 -0
- fdai_deployment_cli/image_targets.py +197 -0
- fdai_deployment_cli/installation_scope.py +280 -0
- fdai_deployment_cli/license.py +311 -0
- fdai_deployment_cli/license_issue.py +239 -0
- fdai_deployment_cli/lifecycle_authorization.py +252 -0
- fdai_deployment_cli/lifecycle_commands.py +173 -0
- fdai_deployment_cli/lifecycle_configuration.py +722 -0
- fdai_deployment_cli/lifecycle_configuration_artifact.py +45 -0
- fdai_deployment_cli/lifecycle_configuration_cli.py +324 -0
- fdai_deployment_cli/lifecycle_configuration_document.py +74 -0
- fdai_deployment_cli/lifecycle_configuration_entities.py +146 -0
- fdai_deployment_cli/lifecycle_configuration_package.py +492 -0
- fdai_deployment_cli/lifecycle_configuration_publication.py +277 -0
- fdai_deployment_cli/lifecycle_configuration_publication_cli.py +191 -0
- fdai_deployment_cli/lifecycle_configuration_sealing.py +127 -0
- fdai_deployment_cli/lifecycle_enrollment.py +45 -0
- fdai_deployment_cli/lifecycle_infrastructure_agent.py +174 -0
- fdai_deployment_cli/lifecycle_installation_binding.py +65 -0
- fdai_deployment_cli/lifecycle_native.py +247 -0
- fdai_deployment_cli/lifecycle_native_reconciliation.py +568 -0
- fdai_deployment_cli/lifecycle_native_transition.py +339 -0
- fdai_deployment_cli/lifecycle_ownership_exclusion.py +78 -0
- fdai_deployment_cli/lifecycle_plan.py +800 -0
- fdai_deployment_cli/lifecycle_promotion.py +185 -0
- fdai_deployment_cli/lifecycle_release.py +308 -0
- fdai_deployment_cli/lifecycle_self_upgrade.py +202 -0
- fdai_deployment_cli/lifecycle_upgrade_bundle.py +176 -0
- fdai_deployment_cli/notification_recipients_input.py +222 -0
- fdai_deployment_cli/observer_artifact.py +106 -0
- fdai_deployment_cli/oci_archive.py +353 -0
- fdai_deployment_cli/offline_kit.py +797 -0
- fdai_deployment_cli/offline_prepare.py +135 -0
- fdai_deployment_cli/operational_evidence_verifier_input.py +575 -0
- fdai_deployment_cli/plan_input.py +101 -0
- fdai_deployment_cli/private_output.py +192 -0
- fdai_deployment_cli/profile.py +70 -0
- fdai_deployment_cli/progress.py +183 -0
- fdai_deployment_cli/readiness.py +286 -0
- fdai_deployment_cli/run_command_receiver.py +418 -0
- fdai_deployment_cli/runtime_build.py +386 -0
- fdai_deployment_cli/runtime_profile.py +287 -0
- fdai_deployment_cli/runtime_release.py +745 -0
- fdai_deployment_cli/runtime_stage.py +82 -0
- fdai_deployment_cli/runtime_support_installation.py +215 -0
- fdai_deployment_cli/simulation.py +80 -0
- fdai_deployment_cli/source_application_inputs.py +397 -0
- fdai_deployment_cli/source_azure.py +586 -0
- fdai_deployment_cli/source_change_cli.py +107 -0
- fdai_deployment_cli/source_change_parser.py +75 -0
- fdai_deployment_cli/source_change_plan.py +181 -0
- fdai_deployment_cli/source_changed_update.py +196 -0
- fdai_deployment_cli/source_deploy.py +170 -0
- fdai_deployment_cli/source_foundation.py +174 -0
- fdai_deployment_cli/source_host_tools.py +163 -0
- fdai_deployment_cli/source_image_stage.py +400 -0
- fdai_deployment_cli/source_input.py +173 -0
- fdai_deployment_cli/source_projection.py +164 -0
- fdai_deployment_cli/source_receiver.py +76 -0
- fdai_deployment_cli/source_recovery.py +264 -0
- fdai_deployment_cli/source_runtime.py +81 -0
- fdai_deployment_cli/source_runtime_support.py +368 -0
- fdai_deployment_cli/source_service_update.py +726 -0
- fdai_deployment_cli/source_service_update_cli.py +47 -0
- fdai_deployment_cli/source_snapshot.py +178 -0
- fdai_deployment_cli/source_teardown.py +574 -0
- fdai_deployment_cli/source_terraform.py +85 -0
- fdai_deployment_cli/source_transport.py +399 -0
- fdai_deployment_cli/standalone_aks_inventory.py +558 -0
- fdai_deployment_cli/standalone_aks_job_execution.py +769 -0
- fdai_deployment_cli/standalone_aks_nodepool_guard.py +141 -0
- fdai_deployment_cli/standalone_application.py +977 -0
- fdai_deployment_cli/standalone_application_completion.py +151 -0
- fdai_deployment_cli/standalone_application_input_recovery.py +107 -0
- fdai_deployment_cli/standalone_application_recovery.py +116 -0
- fdai_deployment_cli/standalone_approval_input.py +24 -0
- fdai_deployment_cli/standalone_catalog_checkpoint.py +122 -0
- fdai_deployment_cli/standalone_catalog_review.py +251 -0
- fdai_deployment_cli/standalone_checkpoint_failure.py +127 -0
- fdai_deployment_cli/standalone_console_publish.py +174 -0
- fdai_deployment_cli/standalone_database_principals.py +41 -0
- fdai_deployment_cli/standalone_deploy.py +638 -0
- fdai_deployment_cli/standalone_development_authority.py +166 -0
- fdai_deployment_cli/standalone_foundation_adoption.py +87 -0
- fdai_deployment_cli/standalone_foundation_transition.py +767 -0
- fdai_deployment_cli/standalone_host.py +4608 -0
- fdai_deployment_cli/standalone_host_rebind.py +226 -0
- fdai_deployment_cli/standalone_host_residual.py +104 -0
- fdai_deployment_cli/standalone_host_state.py +145 -0
- fdai_deployment_cli/standalone_host_values.py +563 -0
- fdai_deployment_cli/standalone_kubernetes_tools.py +100 -0
- fdai_deployment_cli/standalone_license_installation.py +177 -0
- fdai_deployment_cli/standalone_management_egress.py +83 -0
- fdai_deployment_cli/standalone_migration_evidence.py +38 -0
- fdai_deployment_cli/standalone_operational_evidence.py +356 -0
- fdai_deployment_cli/standalone_planned_outputs.py +147 -0
- fdai_deployment_cli/standalone_private_access_probe.py +100 -0
- fdai_deployment_cli/standalone_product_profile.py +110 -0
- fdai_deployment_cli/standalone_remote_prepare.py +442 -0
- fdai_deployment_cli/standalone_residual_apply.py +540 -0
- fdai_deployment_cli/standalone_residual_successor.py +431 -0
- fdai_deployment_cli/standalone_review.py +266 -0
- fdai_deployment_cli/standalone_service_update_finalization.py +116 -0
- fdai_deployment_cli/standalone_service_update_recovery.py +277 -0
- fdai_deployment_cli/standalone_stage_targets.py +325 -0
- fdai_deployment_cli/standalone_status.py +67 -0
- fdai_deployment_cli/standalone_terraform_environment.py +237 -0
- fdai_deployment_cli/standalone_transfer_cleanup.py +237 -0
- fdai_deployment_cli/standalone_trial_activation.py +193 -0
- fdai_deployment_cli/state.py +434 -0
- fdai_deployment_cli/state_handoff.py +253 -0
- fdai_deployment_cli/status_projection.py +97 -0
- fdai_deployment_cli/support_install.py +257 -0
- fdai_deployment_cli/target.py +46 -0
- fdai_deployment_cli/trust/capabilities.json +62 -0
- fdai_deployment_cli/trust/deployment-bundle-root.pub +3 -0
- fdai_deployment_cli/trust/deployment-release-root.pub +3 -0
- fdai_deployment_cli/trust/upstream-signing-key.pub +3 -0
- fdai_deployment_cli/trust_roots.py +50 -0
- fdai_deployment_cli-0.1.2.dist-info/METADATA +112 -0
- fdai_deployment_cli-0.1.2.dist-info/RECORD +203 -0
- fdai_deployment_cli-0.1.2.dist-info/WHEEL +4 -0
- 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
|
+
}
|