platformforge 0.1.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 (659) hide show
  1. platformforge/__init__.py +7 -0
  2. platformforge/__main__.py +5 -0
  3. platformforge/_forge_portal.py +913 -0
  4. platformforge/_graphstudio.py +376 -0
  5. platformforge/_graphview.py +181 -0
  6. platformforge/_installkit.py +1706 -0
  7. platformforge/agents/__init__.py +156 -0
  8. platformforge/agents/bench.py +61 -0
  9. platformforge/agents/contextpack.py +137 -0
  10. platformforge/agents/contracts.py +394 -0
  11. platformforge/agents/coordinators.py +98 -0
  12. platformforge/agents/critic.py +113 -0
  13. platformforge/agents/debate.py +161 -0
  14. platformforge/agents/executors.py +160 -0
  15. platformforge/agents/guardian.py +48 -0
  16. platformforge/agents/mirrors.py +240 -0
  17. platformforge/agents/orchestrator.py +295 -0
  18. platformforge/agents/planner.py +116 -0
  19. platformforge/agents/playbook.py +51 -0
  20. platformforge/agents/referee.py +132 -0
  21. platformforge/agents/reviewers.py +113 -0
  22. platformforge/agents/roster.py +893 -0
  23. platformforge/agents/runledger.py +85 -0
  24. platformforge/agents/runstore.py +74 -0
  25. platformforge/agents/specialists.py +73 -0
  26. platformforge/agents/taskspec.py +94 -0
  27. platformforge/agents/uniqueness.py +115 -0
  28. platformforge/agents/verifier.py +106 -0
  29. platformforge/aiplat/__init__.py +4 -0
  30. platformforge/aiplat/models.py +175 -0
  31. platformforge/analytics/__init__.py +6 -0
  32. platformforge/analytics/capacity.py +109 -0
  33. platformforge/analytics/debt.py +92 -0
  34. platformforge/analytics/dx.py +69 -0
  35. platformforge/analytics/finops_v4.py +171 -0
  36. platformforge/analytics/goldenpath.py +131 -0
  37. platformforge/analytics/history.py +233 -0
  38. platformforge/analytics/measurement.py +149 -0
  39. platformforge/analytics/models.py +127 -0
  40. platformforge/analytics/opsanalytics.py +119 -0
  41. platformforge/analytics/policyintel.py +139 -0
  42. platformforge/analytics/reliability.py +106 -0
  43. platformforge/analytics/soak.py +135 -0
  44. platformforge/analytics/store.py +254 -0
  45. platformforge/bench.py +174 -0
  46. platformforge/cases/__init__.py +13 -0
  47. platformforge/cases/casefile.py +106 -0
  48. platformforge/cases/context_audit.py +134 -0
  49. platformforge/cases/ledgers.py +174 -0
  50. platformforge/cases/replay.py +142 -0
  51. platformforge/cases/routing_audit.py +162 -0
  52. platformforge/caveman/__init__.py +11 -0
  53. platformforge/caveman/compress.py +156 -0
  54. platformforge/cicd/__init__.py +16 -0
  55. platformforge/cicd/backstage.py +51 -0
  56. platformforge/cicd/dockerfile.py +64 -0
  57. platformforge/cicd/github_actions.py +122 -0
  58. platformforge/cicd/gitlab_ci.py +56 -0
  59. platformforge/cicd/gitops.py +130 -0
  60. platformforge/cli/__init__.py +0 -0
  61. platformforge/cli/fleetcmd.py +457 -0
  62. platformforge/cli/main.py +3832 -0
  63. platformforge/cloud/__init__.py +8 -0
  64. platformforge/cloud/aws.py +254 -0
  65. platformforge/cloud/common.py +69 -0
  66. platformforge/cloud/multicloud.py +143 -0
  67. platformforge/collect/__init__.py +11 -0
  68. platformforge/collect/detect.py +254 -0
  69. platformforge/context/__init__.py +10 -0
  70. platformforge/context/capsule.py +60 -0
  71. platformforge/context/gateway.py +227 -0
  72. platformforge/context/refs.py +49 -0
  73. platformforge/context/sufficiency.py +50 -0
  74. platformforge/core/__init__.py +1 -0
  75. platformforge/core/hashing.py +39 -0
  76. platformforge/core/ownership.py +220 -0
  77. platformforge/core/receipts.py +74 -0
  78. platformforge/core/redaction.py +97 -0
  79. platformforge/core/store.py +119 -0
  80. platformforge/core/workspace.py +89 -0
  81. platformforge/data/contracts/agent-handoff.schema.json +26 -0
  82. platformforge/data/contracts/agent-run.schema.json +27 -0
  83. platformforge/data/contracts/agent-spec.schema.json +43 -0
  84. platformforge/data/contracts/capability.schema.json +23 -0
  85. platformforge/data/contracts/change-event.schema.json +22 -0
  86. platformforge/data/contracts/change-intent.schema.json +62 -0
  87. platformforge/data/contracts/change-plan.schema.json +43 -0
  88. platformforge/data/contracts/collector-receipt.schema.json +36 -0
  89. platformforge/data/contracts/context-pack.schema.json +29 -0
  90. platformforge/data/contracts/debate.schema.json +31 -0
  91. platformforge/data/contracts/distribution-manifest-v1.schema.json +81 -0
  92. platformforge/data/contracts/drift-event.schema.json +21 -0
  93. platformforge/data/contracts/fact.schema.json +35 -0
  94. platformforge/data/contracts/finding.schema.json +20 -0
  95. platformforge/data/contracts/graph-v2.schema.json +67 -0
  96. platformforge/data/contracts/graph.schema.json +40 -0
  97. platformforge/data/contracts/install-receipt-v1.schema.json +49 -0
  98. platformforge/data/contracts/observation.schema.json +93 -0
  99. platformforge/data/contracts/receipt.schema.json +21 -0
  100. platformforge/data/contracts/recommendation.schema.json +32 -0
  101. platformforge/data/contracts/refusal.schema.json +16 -0
  102. platformforge/data/contracts/slo-contract.schema.json +25 -0
  103. platformforge/data/contracts/task-spec.schema.json +37 -0
  104. platformforge/data/contracts/workspace-manifest-v1.schema.json +62 -0
  105. platformforge/data/evals/cases/agent-budget-exhaustion/case.yaml +6 -0
  106. platformforge/data/evals/cases/agent-conflicting-specialists/case.yaml +11 -0
  107. platformforge/data/evals/cases/agent-fleet-optimization/case.yaml +13 -0
  108. platformforge/data/evals/cases/agent-host-no-subagents/case.yaml +6 -0
  109. platformforge/data/evals/cases/agent-incident-multi/case.yaml +13 -0
  110. platformforge/data/evals/cases/agent-missing-evidence/case.yaml +6 -0
  111. platformforge/data/evals/cases/agent-multi-domain-audit/case.yaml +14 -0
  112. platformforge/data/evals/cases/agent-production-change/case.yaml +15 -0
  113. platformforge/data/evals/cases/agent-security-mandatory/case.yaml +12 -0
  114. platformforge/data/evals/cases/agent-simple-iac/case.yaml +14 -0
  115. platformforge/data/evals/cases/approval-integrity-seal/case.yaml +6 -0
  116. platformforge/data/evals/cases/capacity-hot/case.yaml +7 -0
  117. platformforge/data/evals/cases/capacity-hot/fixture/capacity.json +5 -0
  118. platformforge/data/evals/cases/catalog-noowner/case.yaml +7 -0
  119. platformforge/data/evals/cases/catalog-noowner/fixture/catalog-info.yaml +6 -0
  120. platformforge/data/evals/cases/contract-fact/case.yaml +5 -0
  121. platformforge/data/evals/cases/contract-fact/fixture/doc.json +2 -0
  122. platformforge/data/evals/cases/cosign-unverified/case.yaml +8 -0
  123. platformforge/data/evals/cases/cosign-unverified/fixture/sig.json +33 -0
  124. platformforge/data/evals/cases/expected-delta-refusal/case.yaml +6 -0
  125. platformforge/data/evals/cases/finops-noattr/case.yaml +9 -0
  126. platformforge/data/evals/cases/finops-noattr/fixture/costs.json +7 -0
  127. platformforge/data/evals/cases/fleet-inv-absent-allocation-unknown/case.yaml +7 -0
  128. platformforge/data/evals/cases/fleet-inv-centrality-not-criticality/case.yaml +7 -0
  129. platformforge/data/evals/cases/fleet-inv-correlation-not-causality/case.yaml +7 -0
  130. platformforge/data/evals/cases/fleet-inv-dx-no-person-metrics/case.yaml +7 -0
  131. platformforge/data/evals/cases/fleet-inv-federation-restricted-denied/case.yaml +7 -0
  132. platformforge/data/evals/cases/fleet-inv-federation-secret-denied/case.yaml +7 -0
  133. platformforge/data/evals/cases/fleet-inv-history-window-deterministic/case.yaml +7 -0
  134. platformforge/data/evals/cases/fleet-inv-low-util-not-deletable/case.yaml +7 -0
  135. platformforge/data/evals/cases/fleet-inv-member-key-deterministic/case.yaml +7 -0
  136. platformforge/data/evals/cases/fleet-inv-missing-denominator/case.yaml +7 -0
  137. platformforge/data/evals/cases/fleet-inv-partial-coverage-visible/case.yaml +7 -0
  138. platformforge/data/evals/cases/fleet-inv-plan-changeintent-only/case.yaml +7 -0
  139. platformforge/data/evals/cases/fleet-inv-report-all-cited/case.yaml +7 -0
  140. platformforge/data/evals/cases/fleet-inv-small-sample-confidence/case.yaml +7 -0
  141. platformforge/data/evals/cases/fleet-inv-uncertain-optimization-suppressed/case.yaml +7 -0
  142. platformforge/data/evals/cases/gha-dangerous/case.yaml +12 -0
  143. platformforge/data/evals/cases/gha-dangerous/fixture/.github/workflows/ci.yaml +11 -0
  144. platformforge/data/evals/cases/gitops-v2/case.yaml +11 -0
  145. platformforge/data/evals/cases/gitops-v2/fixture/argo.yaml +41 -0
  146. platformforge/data/evals/cases/graph-k8s-blast/case.yaml +7 -0
  147. platformforge/data/evals/cases/graph-k8s-blast/fixture/deploy.yaml +15 -0
  148. platformforge/data/evals/cases/iac-module-nopin/case.yaml +8 -0
  149. platformforge/data/evals/cases/iac-module-nopin/fixture/main.tf +3 -0
  150. platformforge/data/evals/cases/iac-plan-replace/case.yaml +8 -0
  151. platformforge/data/evals/cases/iac-plan-replace/fixture/plan.json +12 -0
  152. platformforge/data/evals/cases/iac-sg-open/case.yaml +8 -0
  153. platformforge/data/evals/cases/iac-sg-open/fixture/main.tf +9 -0
  154. platformforge/data/evals/cases/iam-oidc/case.yaml +8 -0
  155. platformforge/data/evals/cases/iam-oidc/fixture/policy.json +11 -0
  156. platformforge/data/evals/cases/iam-wildcard-positive/case.yaml +8 -0
  157. platformforge/data/evals/cases/iam-wildcard-positive/fixture/policy.json +3 -0
  158. platformforge/data/evals/cases/k8s-bad-actor/case.yaml +19 -0
  159. platformforge/data/evals/cases/k8s-bad-actor/fixture/app.yaml +97 -0
  160. platformforge/data/evals/cases/k8s-beta-api/case.yaml +8 -0
  161. platformforge/data/evals/cases/k8s-beta-api/fixture/cr.yaml +7 -0
  162. platformforge/data/evals/cases/k8s-dep-api-boundary/case.yaml +15 -0
  163. platformforge/data/evals/cases/k8s-dep-api-boundary/fixture/app.yaml +25 -0
  164. platformforge/data/evals/cases/k8s-dep-api-clean/case.yaml +12 -0
  165. platformforge/data/evals/cases/k8s-dep-api-clean/fixture/app.yaml +29 -0
  166. platformforge/data/evals/cases/k8s-dep-api-unknown/case.yaml +11 -0
  167. platformforge/data/evals/cases/k8s-dep-api-unknown/fixture/app.yaml +25 -0
  168. platformforge/data/evals/cases/k8s-dep-api-v120/case.yaml +12 -0
  169. platformforge/data/evals/cases/k8s-dep-api-v120/fixture/app.yaml +25 -0
  170. platformforge/data/evals/cases/k8s-dep-api-v129/case.yaml +9 -0
  171. platformforge/data/evals/cases/k8s-dep-api-v129/fixture/app.yaml +25 -0
  172. platformforge/data/evals/cases/k8s-hpa-noreq/case.yaml +8 -0
  173. platformforge/data/evals/cases/k8s-hpa-noreq/fixture/app.yaml +28 -0
  174. platformforge/data/evals/cases/k8s-limits-boundary/case.yaml +10 -0
  175. platformforge/data/evals/cases/k8s-limits-boundary/fixture/app.yaml +15 -0
  176. platformforge/data/evals/cases/k8s-limits-negative/case.yaml +8 -0
  177. platformforge/data/evals/cases/k8s-limits-negative/fixture/deploy.yaml +15 -0
  178. platformforge/data/evals/cases/k8s-limits-positive/case.yaml +8 -0
  179. platformforge/data/evals/cases/k8s-limits-positive/fixture/deploy.yaml +14 -0
  180. platformforge/data/evals/cases/k8s-nopdb/case.yaml +7 -0
  181. platformforge/data/evals/cases/k8s-nopdb/fixture/app.yaml +34 -0
  182. platformforge/data/evals/cases/knowledge-registry/case.yaml +10 -0
  183. platformforge/data/evals/cases/kyverno-dep/case.yaml +10 -0
  184. platformforge/data/evals/cases/kyverno-dep/fixture/pol.yaml +18 -0
  185. platformforge/data/evals/cases/kyverno-legacy/case.yaml +8 -0
  186. platformforge/data/evals/cases/kyverno-legacy/fixture/pol.yaml +7 -0
  187. platformforge/data/evals/cases/live-aws-collect/case.yaml +11 -0
  188. platformforge/data/evals/cases/live-aws-collect/fixture/aws/ec2_describe-vpcs.json +15 -0
  189. platformforge/data/evals/cases/live-aws-collect/fixture/aws/iam_list-users.denied.txt +1 -0
  190. platformforge/data/evals/cases/live-aws-collect/fixture/aws/sts_get-caller-identity.json +5 -0
  191. platformforge/data/evals/cases/live-drift/case.yaml +8 -0
  192. platformforge/data/evals/cases/live-drift/fixture/after.json +22 -0
  193. platformforge/data/evals/cases/live-drift/fixture/before.json +22 -0
  194. platformforge/data/evals/cases/live-incident/case.yaml +7 -0
  195. platformforge/data/evals/cases/live-incident/fixture/changes.json +8 -0
  196. platformforge/data/evals/cases/live-incident/fixture/incident.json +7 -0
  197. platformforge/data/evals/cases/live-k8s-collect/case.yaml +10 -0
  198. platformforge/data/evals/cases/live-k8s-collect/fixture/kubectl/get_configmaps.denied.txt +1 -0
  199. platformforge/data/evals/cases/live-k8s-collect/fixture/kubectl/get_deployments.apps.json +25 -0
  200. platformforge/data/evals/cases/live-k8s-collect/fixture/kubectl/get_pods.json +17 -0
  201. platformforge/data/evals/cases/live-k8s-collect/fixture/kubectl/get_secrets.json +13 -0
  202. platformforge/data/evals/cases/live-multicluster/case.yaml +7 -0
  203. platformforge/data/evals/cases/live-multicluster/fixture/clusters.json +6 -0
  204. platformforge/data/evals/cases/live-plan/case.yaml +7 -0
  205. platformforge/data/evals/cases/live-plan/fixture/drift.json +8 -0
  206. platformforge/data/evals/cases/live-reconcile/case.yaml +7 -0
  207. platformforge/data/evals/cases/live-reconcile/fixture/desired.json +7 -0
  208. platformforge/data/evals/cases/live-reconcile/fixture/observed.json +18 -0
  209. platformforge/data/evals/cases/live-secrets-boundary/case.yaml +8 -0
  210. platformforge/data/evals/cases/live-secrets-boundary/fixture/envelope.json +16 -0
  211. platformforge/data/evals/cases/live-temporal-graph/case.yaml +10 -0
  212. platformforge/data/evals/cases/live-temporal-graph/fixture/graph.json +22 -0
  213. platformforge/data/evals/cases/ops-approval/case.yaml +6 -0
  214. platformforge/data/evals/cases/ops-convergence/case.yaml +6 -0
  215. platformforge/data/evals/cases/ops-execution-safety/case.yaml +6 -0
  216. platformforge/data/evals/cases/ops-intent/case.yaml +6 -0
  217. platformforge/data/evals/cases/ops-planning/case.yaml +6 -0
  218. platformforge/data/evals/cases/ops-policy/case.yaml +6 -0
  219. platformforge/data/evals/cases/ops-rollback/case.yaml +6 -0
  220. platformforge/data/evals/cases/ops-verification/case.yaml +6 -0
  221. platformforge/data/evals/cases/rollback-argocd-revision/case.yaml +6 -0
  222. platformforge/data/evals/cases/rollback-blind-inverse/case.yaml +6 -0
  223. platformforge/data/evals/cases/rollback-git-commit/case.yaml +6 -0
  224. platformforge/data/evals/cases/rollback-impossible/case.yaml +6 -0
  225. platformforge/data/evals/cases/rollback-k8s-annotation-restore/case.yaml +6 -0
  226. platformforge/data/evals/cases/rollback-k8s-scale/case.yaml +6 -0
  227. platformforge/data/evals/cases/rollback-material-tamper/case.yaml +6 -0
  228. platformforge/data/evals/cases/rollback-terraform-replan/case.yaml +6 -0
  229. platformforge/data/evals/cases/rollback-unknown/case.yaml +6 -0
  230. platformforge/data/evals/cases/routing-specialist/case.yaml +9 -0
  231. platformforge/data/evals/cases/rtk-roundtrip/case.yaml +5 -0
  232. platformforge/data/evals/cases/rtk-roundtrip/fixture/verbose.txt +300 -0
  233. platformforge/data/evals/cases/sbom-vuln/case.yaml +9 -0
  234. platformforge/data/evals/cases/sbom-vuln/fixture/sbom.json +9 -0
  235. platformforge/data/evals/cases/secret-prose-mixed/fixture/app.env +1 -0
  236. platformforge/data/evals/cases/secret-prose-mixed/fixture/policy-notes.md +7 -0
  237. platformforge/data/evals/cases/secrets-noleak/case.yaml +5 -0
  238. platformforge/data/evals/cases/secrets-noleak/fixture/app.env +2 -0
  239. platformforge/data/evals/cases/slo-fastburn/case.yaml +8 -0
  240. platformforge/data/evals/cases/slo-fastburn/fixture/events.yaml +2 -0
  241. platformforge/data/evals/cases/slo-fastburn/fixture/slo.yaml +4 -0
  242. platformforge/data/evals/cases/slo-noinput/case.yaml +8 -0
  243. platformforge/data/evals/cases/slo-noinput/fixture/events.yaml +1 -0
  244. platformforge/data/evals/cases/slo-noinput/fixture/slo.yaml +4 -0
  245. platformforge/data/evals/cases/sot-conflict/case.yaml +6 -0
  246. platformforge/data/evals/cases/token-budget/case.yaml +6 -0
  247. platformforge/data/evals/cases/token-budget/fixture/big.py +50 -0
  248. platformforge/data/evals/cases/xr-delete/case.yaml +8 -0
  249. platformforge/data/evals/cases/xr-delete/fixture/xr.yaml +10 -0
  250. platformforge/data/evals/cases/xr-graph-chain/case.yaml +8 -0
  251. platformforge/data/evals/cases/xr-graph-chain/fixture/xrd.yaml +90 -0
  252. platformforge/data/evals/cases/xr-negative-crd/case.yaml +8 -0
  253. platformforge/data/evals/cases/xr-negative-crd/fixture/widget.yaml +19 -0
  254. platformforge/data/evals/cases/xr-upbound-mr/case.yaml +8 -0
  255. platformforge/data/evals/cases/xr-upbound-mr/fixture/bucket.yaml +12 -0
  256. platformforge/data/evals/qpt-bench/change-review/case.yaml +5 -0
  257. platformforge/data/evals/qpt-bench/change-review/fixture/plan.json +9 -0
  258. platformforge/data/evals/qpt-bench/gha-security/case.yaml +5 -0
  259. platformforge/data/evals/qpt-bench/gha-security/fixture/ci.yml +10 -0
  260. platformforge/data/evals/qpt-bench/iam-investigate/case.yaml +5 -0
  261. platformforge/data/evals/qpt-bench/iam-investigate/fixture/policy.json +6 -0
  262. platformforge/data/evals/qpt-bench/incident-analysis/case.yaml +5 -0
  263. platformforge/data/evals/qpt-bench/incident-analysis/fixture/query.json +5 -0
  264. platformforge/data/evals/qpt-bench/incident-analysis/fixture/rules.yaml +6 -0
  265. platformforge/data/evals/qpt-bench/k8s-review/case.yaml +6 -0
  266. platformforge/data/evals/qpt-bench/k8s-review/fixture/deploy.yaml +15 -0
  267. platformforge/data/evals/qpt-bench/tf-security/case.yaml +5 -0
  268. platformforge/data/evals/qpt-bench/tf-security/fixture/main.tf +17 -0
  269. platformforge/data/knowledge/aws/eks.yaml +20 -0
  270. platformforge/data/knowledge/aws/iam.yaml +28 -0
  271. platformforge/data/knowledge/aws/network-exposure.yaml +19 -0
  272. platformforge/data/knowledge/aws/organizations.yaml +19 -0
  273. platformforge/data/knowledge/crossplane/composition-providers.yaml +20 -0
  274. platformforge/data/knowledge/crossplane/managed-resources.yaml +26 -0
  275. platformforge/data/knowledge/crossplane/v1-v2.yaml +25 -0
  276. platformforge/data/knowledge/kubernetes/api-deprecations.yaml +33 -0
  277. platformforge/data/knowledge/kubernetes/autoscaling-rbac.yaml +27 -0
  278. platformforge/data/knowledge/kubernetes/gateway-api.yaml +20 -0
  279. platformforge/data/knowledge/kubernetes/pod-security.yaml +26 -0
  280. platformforge/data/knowledge/sources.yaml +516 -0
  281. platformforge/data/knowledge/terraform/lifecycle.yaml +18 -0
  282. platformforge/data/knowledge/terraform/plan-semantics.yaml +26 -0
  283. platformforge/data/lab/chaos-pod-kill/chaos.yaml +3 -0
  284. platformforge/data/lab/chaos-pod-kill/facts.json +13 -0
  285. platformforge/data/lab/fleets/acme/capacity.yaml +35 -0
  286. platformforge/data/lab/fleets/acme/costs.yaml +25 -0
  287. platformforge/data/lab/fleets/acme/events.yaml +41 -0
  288. platformforge/data/lab/fleets/acme/fleet.yaml +51 -0
  289. platformforge/data/lab/fleets/acme/graph.yaml +45 -0
  290. platformforge/data/lab/fleets/acme/policies.yaml +28 -0
  291. platformforge/data/lab/fleets/acme/requests.yaml +24 -0
  292. platformforge/data/lab/scenarios/argo-bad-rollout/expected.yaml +6 -0
  293. platformforge/data/lab/scenarios/argo-bad-rollout/fixture/app.yaml +9 -0
  294. platformforge/data/lab/scenarios/aws-dump/expected.yaml +19 -0
  295. platformforge/data/lab/scenarios/aws-dump/fixture/compute.json +4 -0
  296. platformforge/data/lab/scenarios/aws-dump/fixture/iam.json +7 -0
  297. platformforge/data/lab/scenarios/aws-dump/fixture/network.json +6 -0
  298. platformforge/data/lab/scenarios/aws-dump/fixture/org.json +4 -0
  299. platformforge/data/lab/scenarios/aws-dump/fixture/storage.json +4 -0
  300. platformforge/data/lab/scenarios/container-unsigned/expected.yaml +5 -0
  301. platformforge/data/lab/scenarios/container-unsigned/fixture/supply.json +11 -0
  302. platformforge/data/lab/scenarios/cost-idle-workload/expected.yaml +5 -0
  303. platformforge/data/lab/scenarios/cost-idle-workload/fixture/costs.json +12 -0
  304. platformforge/data/lab/scenarios/crossplane-platform/expected.yaml +16 -0
  305. platformforge/data/lab/scenarios/crossplane-platform/fixture/negative.yaml +18 -0
  306. platformforge/data/lab/scenarios/crossplane-platform/fixture/xrd.yaml +90 -0
  307. platformforge/data/lab/scenarios/demo-platform/expected.yaml +16 -0
  308. platformforge/data/lab/scenarios/demo-platform/fixture/.github/workflows/ci.yml +10 -0
  309. platformforge/data/lab/scenarios/demo-platform/fixture/deploy.yaml +16 -0
  310. platformforge/data/lab/scenarios/demo-platform/fixture/infra.tf +5 -0
  311. platformforge/data/lab/scenarios/fleet-ai/expected.yaml +12 -0
  312. platformforge/data/lab/scenarios/fleet-capacity/expected.yaml +12 -0
  313. platformforge/data/lab/scenarios/fleet-cost/expected.yaml +18 -0
  314. platformforge/data/lab/scenarios/fleet-federation/expected.yaml +11 -0
  315. platformforge/data/lab/scenarios/fleet-golden-path/expected.yaml +18 -0
  316. platformforge/data/lab/scenarios/fleet-op-hotspot/expected.yaml +11 -0
  317. platformforge/data/lab/scenarios/fleet-ownership/expected.yaml +17 -0
  318. platformforge/data/lab/scenarios/fleet-partial/expected.yaml +10 -0
  319. platformforge/data/lab/scenarios/fleet-policy-friction/expected.yaml +10 -0
  320. platformforge/data/lab/scenarios/fleet-recurring-incident/expected.yaml +12 -0
  321. platformforge/data/lab/scenarios/fleet-report/expected.yaml +13 -0
  322. platformforge/data/lab/scenarios/fleet-risk/expected.yaml +18 -0
  323. platformforge/data/lab/scenarios/gitops-drift/expected.yaml +6 -0
  324. platformforge/data/lab/scenarios/gitops-drift/fixture/app.yaml +12 -0
  325. platformforge/data/lab/scenarios/k8s-bad-readiness/expected.yaml +6 -0
  326. platformforge/data/lab/scenarios/k8s-bad-readiness/fixture/deploy.yaml +23 -0
  327. platformforge/data/lab/scenarios/k8s-bad-readiness/fixture/netpol.yaml +9 -0
  328. platformforge/data/lab/scenarios/k8s-missing-limits/expected.yaml +6 -0
  329. platformforge/data/lab/scenarios/k8s-missing-limits/fixture/deploy.yaml +26 -0
  330. platformforge/data/lab/scenarios/k8s-missing-limits/fixture/netpol.yaml +9 -0
  331. platformforge/data/lab/scenarios/k8s-single-replica/expected.yaml +5 -0
  332. platformforge/data/lab/scenarios/k8s-single-replica/fixture/deploy.yaml +26 -0
  333. platformforge/data/lab/scenarios/k8s-single-replica/fixture/netpol.yaml +4 -0
  334. platformforge/data/lab/scenarios/live-aws-collect/expected.yaml +4 -0
  335. platformforge/data/lab/scenarios/live-aws-collect/fixture/aws/ec2_describe-vpcs.json +15 -0
  336. platformforge/data/lab/scenarios/live-aws-collect/fixture/aws/iam_list-users.denied.txt +1 -0
  337. platformforge/data/lab/scenarios/live-aws-collect/fixture/aws/sts_get-caller-identity.json +5 -0
  338. platformforge/data/lab/scenarios/live-k8s-collect/expected.yaml +4 -0
  339. platformforge/data/lab/scenarios/live-k8s-collect/fixture/kubectl/get_configmaps.denied.txt +1 -0
  340. platformforge/data/lab/scenarios/live-k8s-collect/fixture/kubectl/get_deployments.apps.json +25 -0
  341. platformforge/data/lab/scenarios/live-k8s-collect/fixture/kubectl/get_pods.json +17 -0
  342. platformforge/data/lab/scenarios/live-k8s-collect/fixture/kubectl/get_secrets.json +13 -0
  343. platformforge/data/lab/scenarios/live-kind/expected.yaml +14 -0
  344. platformforge/data/lab/scenarios/live-kind/fixture/kubectl/get_configmaps.denied.txt +1 -0
  345. platformforge/data/lab/scenarios/live-kind/fixture/kubectl/get_deployments.apps.json +25 -0
  346. platformforge/data/lab/scenarios/live-kind/fixture/kubectl/get_pods.json +17 -0
  347. platformforge/data/lab/scenarios/live-kind/fixture/kubectl/get_secrets.json +13 -0
  348. platformforge/data/lab/scenarios/network-policy-missing/expected.yaml +6 -0
  349. platformforge/data/lab/scenarios/network-policy-missing/fixture/deploy.yaml +26 -0
  350. platformforge/data/lab/scenarios/ops-bad-rollout/expected.yaml +3 -0
  351. platformforge/data/lab/scenarios/ops-bad-rollout/fixture/ops.yaml +33 -0
  352. platformforge/data/lab/scenarios/ops-break-glass/expected.yaml +3 -0
  353. platformforge/data/lab/scenarios/ops-break-glass/fixture/ops.yaml +21 -0
  354. platformforge/data/lab/scenarios/ops-broken-selector/expected.yaml +3 -0
  355. platformforge/data/lab/scenarios/ops-broken-selector/fixture/ops.yaml +21 -0
  356. platformforge/data/lab/scenarios/ops-changed-plan-hash/expected.yaml +3 -0
  357. platformforge/data/lab/scenarios/ops-changed-plan-hash/fixture/ops.yaml +20 -0
  358. platformforge/data/lab/scenarios/ops-conflicting-operation/expected.yaml +3 -0
  359. platformforge/data/lab/scenarios/ops-conflicting-operation/fixture/ops.yaml +21 -0
  360. platformforge/data/lab/scenarios/ops-expired-approval/expected.yaml +3 -0
  361. platformforge/data/lab/scenarios/ops-expired-approval/fixture/ops.yaml +20 -0
  362. platformforge/data/lab/scenarios/ops-partial-failure/expected.yaml +3 -0
  363. platformforge/data/lab/scenarios/ops-partial-failure/fixture/ops.yaml +24 -0
  364. platformforge/data/lab/scenarios/ops-policy-denial/expected.yaml +3 -0
  365. platformforge/data/lab/scenarios/ops-policy-denial/fixture/ops.yaml +17 -0
  366. platformforge/data/lab/scenarios/ops-replica-drift/expected.yaml +3 -0
  367. platformforge/data/lab/scenarios/ops-replica-drift/fixture/ops.yaml +28 -0
  368. platformforge/data/lab/scenarios/ops-rollback-annotate/expected.yaml +3 -0
  369. platformforge/data/lab/scenarios/ops-rollback-annotate/fixture/ops.yaml +41 -0
  370. platformforge/data/lab/scenarios/ops-rollback-gitops/expected.yaml +3 -0
  371. platformforge/data/lab/scenarios/ops-rollback-gitops/fixture/ops.yaml +38 -0
  372. platformforge/data/lab/scenarios/ops-rollback-impossible/expected.yaml +3 -0
  373. platformforge/data/lab/scenarios/ops-rollback-impossible/fixture/ops.yaml +34 -0
  374. platformforge/data/lab/scenarios/ops-rollback-scale/expected.yaml +3 -0
  375. platformforge/data/lab/scenarios/ops-rollback-scale/fixture/ops.yaml +40 -0
  376. platformforge/data/lab/scenarios/ops-rollback-terraform/expected.yaml +3 -0
  377. platformforge/data/lab/scenarios/ops-rollback-terraform/fixture/ops.yaml +38 -0
  378. platformforge/data/lab/scenarios/ops-rollback-terraform/fixture/tf/tfplan.bin +1 -0
  379. platformforge/data/lab/scenarios/ops-stale-observation/expected.yaml +3 -0
  380. platformforge/data/lab/scenarios/ops-stale-observation/fixture/ops.yaml +20 -0
  381. platformforge/data/lab/scenarios/secret-exposure/expected.yaml +5 -0
  382. platformforge/data/lab/scenarios/secret-exposure/fixture/app.env +2 -0
  383. platformforge/data/lab/scenarios/slo-burn/expected.yaml +5 -0
  384. platformforge/data/lab/scenarios/slo-burn/fixture/events.yaml +2 -0
  385. platformforge/data/lab/scenarios/slo-burn/fixture/slo.yaml +4 -0
  386. platformforge/data/lab/scenarios/terraform-iam-wildcard/expected.yaml +5 -0
  387. platformforge/data/lab/scenarios/terraform-iam-wildcard/fixture/policy.json +14 -0
  388. platformforge/data/lab/scenarios/terraform-public-bucket/expected.yaml +6 -0
  389. platformforge/data/lab/scenarios/terraform-public-bucket/fixture/main.tf +5 -0
  390. platformforge/data/portable_skills/platformforge-agents/SKILL.md +47 -0
  391. platformforge/data/portable_skills/platformforge-change/SKILL.md +41 -0
  392. platformforge/data/portable_skills/platformforge-core/SKILL.md +62 -0
  393. platformforge/data/portable_skills/platformforge-economy/SKILL.md +42 -0
  394. platformforge/data/portable_skills/platformforge-finops/SKILL.md +34 -0
  395. platformforge/data/portable_skills/platformforge-fleet/SKILL.md +39 -0
  396. platformforge/data/portable_skills/platformforge-governance/SKILL.md +76 -0
  397. platformforge/data/portable_skills/platformforge-graph/SKILL.md +46 -0
  398. platformforge/data/portable_skills/platformforge-lab/SKILL.md +39 -0
  399. platformforge/data/portable_skills/platformforge-sdd/SKILL.md +39 -0
  400. platformforge/data/portable_skills/platformforge-security/SKILL.md +38 -0
  401. platformforge/data/portable_skills/platformforge-sre/SKILL.md +41 -0
  402. platformforge/data/rules/catalog/finops.yaml +26 -0
  403. platformforge/data/rules/catalog/gitops.yaml +161 -0
  404. platformforge/data/rules/catalog/iac.yaml +84 -0
  405. platformforge/data/rules/catalog/k8s.yaml +322 -0
  406. platformforge/data/rules/catalog/orchestration.yaml +99 -0
  407. platformforge/data/rules/catalog/product.yaml +42 -0
  408. platformforge/data/rules/catalog/routing.yaml +122 -0
  409. platformforge/data/rules/catalog/security.yaml +159 -0
  410. platformforge/data/rules/catalog/sre.yaml +67 -0
  411. platformforge/diagnose/__init__.py +75 -0
  412. platformforge/distribution/__init__.py +18 -0
  413. platformforge/distribution/bundle.py +127 -0
  414. platformforge/distribution/hostflow.py +247 -0
  415. platformforge/distribution/models.py +80 -0
  416. platformforge/distribution/service.py +225 -0
  417. platformforge/economy/__init__.py +27 -0
  418. platformforge/economy/bench.py +66 -0
  419. platformforge/economy/budget.py +198 -0
  420. platformforge/economy/cache.py +287 -0
  421. platformforge/economy/checkpoint.py +112 -0
  422. platformforge/economy/collection.py +142 -0
  423. platformforge/economy/doctor.py +207 -0
  424. platformforge/economy/engine.py +158 -0
  425. platformforge/economy/envelope.py +61 -0
  426. platformforge/economy/fleetpack.py +86 -0
  427. platformforge/economy/ledger.py +232 -0
  428. platformforge/economy/plan.py +131 -0
  429. platformforge/economy/pricing.py +101 -0
  430. platformforge/economy/qpt.py +286 -0
  431. platformforge/economy/reconcile.py +86 -0
  432. platformforge/economy/verifyplan.py +107 -0
  433. platformforge/economy/waste.py +123 -0
  434. platformforge/evals/__init__.py +16 -0
  435. platformforge/evals/coverage.py +140 -0
  436. platformforge/evals/fleet_invariants.py +260 -0
  437. platformforge/evals/ops_invariants.py +158 -0
  438. platformforge/evals/runner.py +541 -0
  439. platformforge/federation/__init__.py +4 -0
  440. platformforge/federation/node.py +135 -0
  441. platformforge/finops/__init__.py +14 -0
  442. platformforge/finops/alloc.py +31 -0
  443. platformforge/finops/costs.py +79 -0
  444. platformforge/finops/focus.py +100 -0
  445. platformforge/finops/graph_cost.py +51 -0
  446. platformforge/finops/ingest.py +144 -0
  447. platformforge/finops/insights.py +193 -0
  448. platformforge/finops/unit.py +36 -0
  449. platformforge/fleet/__init__.py +5 -0
  450. platformforge/fleet/loader.py +73 -0
  451. platformforge/fleet/models.py +155 -0
  452. platformforge/fleet/orggraph.py +172 -0
  453. platformforge/fleet/query.py +154 -0
  454. platformforge/forge/__init__.py +9 -0
  455. platformforge/forge/collect.py +88 -0
  456. platformforge/forge/delegate.py +77 -0
  457. platformforge/forge/manifest.py +106 -0
  458. platformforge/freeze/__init__.py +22 -0
  459. platformforge/freeze/exceptions.py +56 -0
  460. platformforge/freeze/manifest.py +167 -0
  461. platformforge/freeze/snapshots.py +108 -0
  462. platformforge/graph/__init__.py +39 -0
  463. platformforge/graph/backend.py +174 -0
  464. platformforge/graph/bench.py +245 -0
  465. platformforge/graph/build.py +130 -0
  466. platformforge/graph/delivery.py +183 -0
  467. platformforge/graph/diff.py +292 -0
  468. platformforge/graph/events.py +116 -0
  469. platformforge/graph/migrate.py +44 -0
  470. platformforge/graph/model.py +264 -0
  471. platformforge/graph/persist.py +97 -0
  472. platformforge/graph/query.py +290 -0
  473. platformforge/graph/temporal.py +78 -0
  474. platformforge/graph/vocab.py +63 -0
  475. platformforge/graphview.py +94 -0
  476. platformforge/iac/__init__.py +7 -0
  477. platformforge/iac/drift.py +53 -0
  478. platformforge/iac/plan.py +84 -0
  479. platformforge/iac/terraform.py +123 -0
  480. platformforge/install/__init__.py +1 -0
  481. platformforge/install/service.py +421 -0
  482. platformforge/k8s/__init__.py +8 -0
  483. platformforge/k8s/helm.py +152 -0
  484. platformforge/k8s/hubble.py +87 -0
  485. platformforge/k8s/manifests.py +513 -0
  486. platformforge/knowledge/__init__.py +1 -0
  487. platformforge/knowledge/packs.py +157 -0
  488. platformforge/knowledge/registry.py +249 -0
  489. platformforge/knowledge/select.py +127 -0
  490. platformforge/lab/__init__.py +5 -0
  491. platformforge/lab/chaos.py +71 -0
  492. platformforge/lab/fleetlab.py +351 -0
  493. platformforge/lab/opslab.py +271 -0
  494. platformforge/lab/runner.py +219 -0
  495. platformforge/live/__init__.py +36 -0
  496. platformforge/live/budget.py +92 -0
  497. platformforge/live/capability.py +80 -0
  498. platformforge/live/collectors/__init__.py +6 -0
  499. platformforge/live/collectors/aws.py +370 -0
  500. platformforge/live/collectors/aws_transport.py +98 -0
  501. platformforge/live/collectors/k8s.py +389 -0
  502. platformforge/live/collectors/k8s_transport.py +85 -0
  503. platformforge/live/cursors.py +48 -0
  504. platformforge/live/drift.py +130 -0
  505. platformforge/live/envelope.py +85 -0
  506. platformforge/live/federation.py +113 -0
  507. platformforge/live/fixtures.py +168 -0
  508. platformforge/live/identity.py +258 -0
  509. platformforge/live/incident.py +200 -0
  510. platformforge/live/models.py +435 -0
  511. platformforge/live/reconcile.py +382 -0
  512. platformforge/live/remediate.py +169 -0
  513. platformforge/live/store.py +256 -0
  514. platformforge/live/topology.py +249 -0
  515. platformforge/mcp/__init__.py +7 -0
  516. platformforge/mcp/parity.py +69 -0
  517. platformforge/mcp/registry.py +427 -0
  518. platformforge/mcp/server.py +84 -0
  519. platformforge/mcp/tools.py +732 -0
  520. platformforge/models/__init__.py +21 -0
  521. platformforge/models/base.py +214 -0
  522. platformforge/observe/__init__.py +26 -0
  523. platformforge/observe/capacity.py +39 -0
  524. platformforge/observe/dr.py +67 -0
  525. platformforge/observe/grafana.py +76 -0
  526. platformforge/observe/incident.py +58 -0
  527. platformforge/observe/otel.py +135 -0
  528. platformforge/observe/postmortem.py +72 -0
  529. platformforge/observe/prometheus.py +88 -0
  530. platformforge/observe/slo.py +125 -0
  531. platformforge/ops/__init__.py +8 -0
  532. platformforge/ops/actions.py +215 -0
  533. platformforge/ops/analytics.py +83 -0
  534. platformforge/ops/approval.py +275 -0
  535. platformforge/ops/autorem.py +101 -0
  536. platformforge/ops/config.py +175 -0
  537. platformforge/ops/engine.py +758 -0
  538. platformforge/ops/envelope.py +135 -0
  539. platformforge/ops/executors/__init__.py +1 -0
  540. platformforge/ops/executors/base.py +211 -0
  541. platformforge/ops/executors/git.py +77 -0
  542. platformforge/ops/executors/gitops.py +134 -0
  543. platformforge/ops/executors/terraform.py +84 -0
  544. platformforge/ops/gates.py +135 -0
  545. platformforge/ops/goldenpath.py +180 -0
  546. platformforge/ops/material.py +181 -0
  547. platformforge/ops/models.py +421 -0
  548. platformforge/ops/operation.py +335 -0
  549. platformforge/ops/opgraph.py +233 -0
  550. platformforge/ops/policy.py +275 -0
  551. platformforge/ops/preconditions.py +109 -0
  552. platformforge/ops/registry.py +101 -0
  553. platformforge/ops/risk.py +216 -0
  554. platformforge/ops/rollback.py +287 -0
  555. platformforge/ops/rollback_builders.py +163 -0
  556. platformforge/ops/runbook.py +269 -0
  557. platformforge/ops/simulate.py +125 -0
  558. platformforge/ops/source_of_truth.py +198 -0
  559. platformforge/ops/store.py +157 -0
  560. platformforge/ops/verify.py +190 -0
  561. platformforge/optimize/__init__.py +4 -0
  562. platformforge/optimize/engine.py +109 -0
  563. platformforge/optimize/models.py +68 -0
  564. platformforge/optimize/scan.py +196 -0
  565. platformforge/plan/__init__.py +107 -0
  566. platformforge/portable_cli.py +199 -0
  567. platformforge/portal_adapter.py +740 -0
  568. platformforge/portal_assets/chunk-7_l-b4Tr.js +1 -0
  569. platformforge/portal_assets/chunk-B0Iad6pD.js +1 -0
  570. platformforge/portal_assets/chunk-B605XnCF.js +1 -0
  571. platformforge/portal_assets/chunk-B7N3TXcn.js +1 -0
  572. platformforge/portal_assets/chunk-BAj6yGem.js +1 -0
  573. platformforge/portal_assets/chunk-BGUitn2r.js +1 -0
  574. platformforge/portal_assets/chunk-BVKnCv85.js +1 -0
  575. platformforge/portal_assets/chunk-BYyM9V_w.js +1 -0
  576. platformforge/portal_assets/chunk-BaQMhq-G.js +1 -0
  577. platformforge/portal_assets/chunk-BavOrxVm.js +1 -0
  578. platformforge/portal_assets/chunk-Bb0C24YV.js +1 -0
  579. platformforge/portal_assets/chunk-BfCFGPzs.js +1 -0
  580. platformforge/portal_assets/chunk-C2t4OUVu.js +1 -0
  581. platformforge/portal_assets/chunk-C3pI4QjU.js +1 -0
  582. platformforge/portal_assets/chunk-C4gpbtbP.js +1 -0
  583. platformforge/portal_assets/chunk-CGWeHhKY.js +1 -0
  584. platformforge/portal_assets/chunk-CJCT86Dr.js +1 -0
  585. platformforge/portal_assets/chunk-CXcsLDPJ.js +1 -0
  586. platformforge/portal_assets/chunk-CcjxoLxw.js +1 -0
  587. platformforge/portal_assets/chunk-D-gBPXHa.js +1 -0
  588. platformforge/portal_assets/chunk-D3eZogIe.js +1 -0
  589. platformforge/portal_assets/chunk-DQU0VyoQ.js +1 -0
  590. platformforge/portal_assets/chunk-DqWNkF1O.js +1 -0
  591. platformforge/portal_assets/chunk-DuPPkSIE.js +3 -0
  592. platformforge/portal_assets/chunk-DyIHOW_a.js +1 -0
  593. platformforge/portal_assets/chunk-Fx4zSGL5.js +1 -0
  594. platformforge/portal_assets/chunk-UBKt05Rg.js +1 -0
  595. platformforge/portal_assets/chunk-htnJDQEY.js +1 -0
  596. platformforge/portal_assets/chunk-kXRFMGqe.js +1 -0
  597. platformforge/portal_assets/chunk-qe3u763a.js +1 -0
  598. platformforge/portal_assets/favicon.ico +0 -0
  599. platformforge/portal_assets/index.html +13 -0
  600. platformforge/portal_assets/main-QDFVZK3T.js +5 -0
  601. platformforge/portal_assets/styles-MQEBDFDX.css +1 -0
  602. platformforge/product/__init__.py +10 -0
  603. platformforge/product/backstage.py +49 -0
  604. platformforge/product/catalog.py +74 -0
  605. platformforge/product/crossplane.py +341 -0
  606. platformforge/product/golden_paths/__init__.py +5 -0
  607. platformforge/product/golden_paths/engine.py +97 -0
  608. platformforge/product/golden_paths/library.yaml +198 -0
  609. platformforge/product/golden_paths/model.py +84 -0
  610. platformforge/product/maturity.py +107 -0
  611. platformforge/product/scorecards.py +84 -0
  612. platformforge/resources.py +36 -0
  613. platformforge/risk/__init__.py +5 -0
  614. platformforge/risk/engine.py +95 -0
  615. platformforge/risk/signals.py +49 -0
  616. platformforge/routing/__init__.py +13 -0
  617. platformforge/routing/decision.py +191 -0
  618. platformforge/routing/router.py +328 -0
  619. platformforge/rtk/__init__.py +9 -0
  620. platformforge/rtk/compact.py +127 -0
  621. platformforge/rtk/parsers.py +199 -0
  622. platformforge/rules/__init__.py +5 -0
  623. platformforge/rules/engine.py +345 -0
  624. platformforge/sandbox/__init__.py +7 -0
  625. platformforge/sandbox/env.py +151 -0
  626. platformforge/sandbox/review.py +92 -0
  627. platformforge/sdd/__init__.py +8 -0
  628. platformforge/sdd/lifecycle.py +239 -0
  629. platformforge/security/__init__.py +19 -0
  630. platformforge/security/cosign.py +67 -0
  631. platformforge/security/iam.py +112 -0
  632. platformforge/security/identity.py +84 -0
  633. platformforge/security/kyverno.py +106 -0
  634. platformforge/security/sbom.py +59 -0
  635. platformforge/security/scan.py +63 -0
  636. platformforge/security/slsa.py +86 -0
  637. platformforge/security/supply.py +62 -0
  638. platformforge/tokensave/__init__.py +20 -0
  639. platformforge/tokensave/budget.py +62 -0
  640. platformforge/tokensave/estimate.py +21 -0
  641. platformforge/tokensave/index.py +172 -0
  642. platformforge/tokensave/ledger.py +121 -0
  643. platformforge/tokensave/packs.py +285 -0
  644. platformforge/ui/__init__.py +28 -0
  645. platformforge/ui/app.py +465 -0
  646. platformforge/ui/home.py +113 -0
  647. platformforge/ui/i18n.py +143 -0
  648. platformforge/ui/kit.py +689 -0
  649. platformforge/ui/screen.py +628 -0
  650. platformforge/ui/tui.py +94 -0
  651. platformforge/ui/wizard.py +309 -0
  652. platformforge/workspace/__init__.py +13 -0
  653. platformforge/workspace/models.py +35 -0
  654. platformforge/workspace/service.py +69 -0
  655. platformforge-0.1.0.dist-info/METADATA +212 -0
  656. platformforge-0.1.0.dist-info/RECORD +659 -0
  657. platformforge-0.1.0.dist-info/WHEEL +4 -0
  658. platformforge-0.1.0.dist-info/entry_points.txt +3 -0
  659. platformforge-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,758 @@
1
+ """Cycle 4 — operations orchestrator.
2
+
3
+ Drives the §8 pipeline end to end with *no implicit skips*:
4
+
5
+ intent → plan → simulate → risk → policy → approval →
6
+ envelope → preconditions → execute (typed) → verify →
7
+ converge | rollback → ledger receipts
8
+
9
+ Every stage emits receipts into the OperationLedger; every refusal is a
10
+ `PF-OPS-*` object. Executors run only via injected host transports —
11
+ `dry_run` is the default and `--execute` semantics live in the CLI.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import Any
17
+
18
+ from platformforge.ops.actions import spec_for
19
+ from platformforge.ops.approval import Approval, BreakGlass, check_approval
20
+ from platformforge.ops.envelope import ExecutionEnvelope
21
+ from platformforge.ops.models import ChangeIntent, ChangePlan, PlanStep
22
+ from platformforge.ops.operation import ExecutionStep, LockTable, Operation, OperationLedger
23
+ from platformforge.ops.policy import Policy, evaluate
24
+ from platformforge.ops.preconditions import check_preconditions
25
+ from platformforge.ops.risk import assess, classify_reversibility
26
+ from platformforge.ops.rollback import RollbackPlan, build_rollback_plan, compensate_for, derive_rollback
27
+ from platformforge.ops.source_of_truth import resolve
28
+
29
+
30
+ def executor_for(action: str):
31
+ from platformforge.ops.executors.base import ObserveExecutor
32
+ from platformforge.ops.executors.git import GitExecutor
33
+ from platformforge.ops.executors.gitops import ArgoCDExecutor, KubernetesExecutor
34
+ from platformforge.ops.executors.terraform import TerraformExecutor, TofuExecutor
35
+ mapping = {"git": GitExecutor, "terraform": TerraformExecutor,
36
+ "tofu": TofuExecutor, "argocd": ArgoCDExecutor,
37
+ "kubernetes": KubernetesExecutor, "observe": ObserveExecutor}
38
+ spec = spec_for(action)
39
+ cls = mapping.get(spec.executor if spec else "")
40
+ return cls() if cls else None
41
+
42
+
43
+ def propose(intent: ChangeIntent,
44
+ resources: list[dict[str, Any]] | None = None,
45
+ sot_context: dict[str, Any] | None = None
46
+ ) -> dict[str, Any]:
47
+ """§8 stage 1-3: validate intent, resolve source of truth."""
48
+ violations = intent.validate()
49
+ if violations:
50
+ return {"ok": False, "refusals": violations}
51
+ resolutions = resolve(resources or [], sot_context)
52
+ unresolved = [r.to_dict() for r in resolutions if not r.resolved]
53
+ return {"ok": True, "intent": intent.to_dict(),
54
+ "resolutions": [r.to_dict() for r in resolutions],
55
+ "unresolved_sources": unresolved,
56
+ "requires_human_review": bool(unresolved)}
57
+
58
+
59
+ def plan(intent: ChangeIntent, *,
60
+ plan_id: str = "",
61
+ steps: list[PlanStep] | None = None,
62
+ expected_delta=None,
63
+ affected: list[str] | None = None) -> ChangePlan:
64
+ p = ChangePlan(plan_id=plan_id or f"plan-{intent.intent_id}",
65
+ intent_id=intent.intent_id,
66
+ steps=list(steps or []),
67
+ affected_resources=list(affected
68
+ or intent.target_resources),
69
+ expected_delta=expected_delta
70
+ or __import__("platformforge.ops.models",
71
+ fromlist=["ExpectedDelta"]).ExpectedDelta())
72
+ return p
73
+
74
+
75
+ def assess_risk(plan: ChangePlan, context: dict[str, Any] | None = None,
76
+ rollback_status: str | None = None
77
+ ) -> dict[str, Any]:
78
+ """Per-step risk assessments + plan ceiling (max class wins).
79
+ Cycle 4.1: the material-built rollback status feeds both
80
+ rollback_confidence and the reversibility class (worst-of)."""
81
+ from platformforge.ops.risk import RISK_RANK, reversibility_for_status, rollback_confidence
82
+ ctx = context or {}
83
+ per_step = []
84
+ worst = "R0"
85
+ rb_conf = rollback_confidence(rollback_status)
86
+ for s in plan.steps:
87
+ rev = classify_reversibility(s.action, s.params, ctx)
88
+ rev = reversibility_for_status(rollback_status, rev)
89
+ spec = spec_for(s.action)
90
+ step_status = rollback_status
91
+ if spec and spec.rollback_strategy in ("manual-only",
92
+ "impossible", "unknown"):
93
+ step_status = spec.rollback_strategy if \
94
+ spec.rollback_strategy != "unknown" else "unresolved"
95
+ a = assess(s.action, s.params,
96
+ {**ctx, "reversibility": rev,
97
+ "rollback_confidence":
98
+ rollback_confidence(step_status)})
99
+ per_step.append({"step_id": s.step_id, "action": s.action,
100
+ "rollback_status": step_status or "unknown",
101
+ **a.to_dict()})
102
+ if RISK_RANK[a.risk_class] > RISK_RANK[worst]:
103
+ worst = a.risk_class
104
+ out = {"plan_id": plan.plan_id, "risk_class": worst,
105
+ "steps": per_step,
106
+ "rollback_confidence": rb_conf}
107
+ if rollback_status:
108
+ out["rollback_status"] = rollback_status
109
+ return out
110
+
111
+
112
+ def decide(plan: ChangePlan, policies: list[Policy],
113
+ inp: dict[str, Any] | None = None,
114
+ exceptions=None, at: str | None = None):
115
+ base_inp = {"plan_id": plan.plan_id, "plan_hash": plan.hash(),
116
+ "intent_id": plan.intent_id,
117
+ "environment": (inp or {}).get("environment", "unknown"),
118
+ **(inp or {})}
119
+ return evaluate(base_inp, policies, exceptions=exceptions, at=at)
120
+
121
+
122
+ def mint_envelope(plan: ChangePlan, *, execution_id: str,
123
+ decisions: list, approvals: list[Approval],
124
+ risk: dict[str, Any], rollback: RollbackPlan | None,
125
+ preconditions: dict[str, Any] | None = None,
126
+ verification: dict[str, Any] | None = None,
127
+ expires_at: str = ""
128
+ ) -> ExecutionEnvelope | dict[str, Any]:
129
+ """Envelopes mint only when every decision allows/requires-satisfied
130
+ — never self-minted."""
131
+ if not decisions:
132
+ return {"refusal": "PF-OPS-ENVELOPE-UNGOVERNED",
133
+ "unlock": "run policy evaluation first"}
134
+ for d in decisions:
135
+ eff = d.effective
136
+ if eff in ("deny", "would_deny", "unresolved"):
137
+ return {"refusal": "PF-OPS-POLICY-BLOCK",
138
+ "unlock": f"policy {d.policy_id} → {eff}; resolve "
139
+ "before minting an envelope"}
140
+ # Cycle 4.1 §E — a mutating plan with NO declared delta refuses to
141
+ # mint. `unknown_dimensions` counts as an honest declaration; a
142
+ # completely empty delta does not.
143
+ if delta_refusal(plan) is not None:
144
+ return delta_refusal(plan)
145
+ actions = [{"step_id": s.step_id, "action": s.action,
146
+ "params": s.params} for s in plan.steps]
147
+ env = ExecutionEnvelope(
148
+ execution_id=execution_id, intent_id=plan.intent_id,
149
+ change_plan_hash=plan.hash(),
150
+ executor=_dominant_executor(plan),
151
+ actions=actions, scope=list(plan.affected_resources),
152
+ preconditions=preconditions or {},
153
+ policy_decisions=[d.to_dict() for d in decisions],
154
+ approvals=[a.approval_id for a in approvals],
155
+ risk=risk, expected_delta=plan.expected_delta.to_dict(),
156
+ verification=verification or {},
157
+ rollback=(rollback or derive_rollback(actions)).to_dict(),
158
+ expires_at=expires_at)
159
+ violations = env.validate()
160
+ if violations:
161
+ return {"refusal": "PF-OPS-ENVELOPE-INVALID",
162
+ "unlock": "envelope validation failed",
163
+ "violations": violations}
164
+ return env
165
+
166
+
167
+ def delta_refusal(plan: ChangePlan) -> dict[str, Any] | None:
168
+ """Cycle 4.1 — mutating steps require an ExpectedDelta declaration
169
+ (adds/removes/changes OR explicit unknown_dimensions). Read-only
170
+ plans (validate/show/observe/diff) may omit it."""
171
+ ed = plan.expected_delta
172
+ declared = bool(getattr(ed, "adds", None)
173
+ or getattr(ed, "removes", None)
174
+ or getattr(ed, "changes", None)
175
+ or getattr(ed, "unknown_dimensions", None))
176
+ if declared:
177
+ return None
178
+ mutating = [s.action for s in plan.steps
179
+ if (spec_for(s.action) or _MISSING).mutating]
180
+ if mutating:
181
+ return {"refusal": "PF-OPS-NO-DELTA",
182
+ "unlock": "declare expected_delta (adds/removes/changes "
183
+ "or explicit unknown_dimensions) — mutating "
184
+ "plans cannot mint without an expected "
185
+ "outcome",
186
+ "mutating_steps": mutating}
187
+ return None
188
+
189
+
190
+ class _MissingSpec:
191
+ mutating = True # unknown action → treat as mutating (conservative)
192
+
193
+
194
+ _MISSING = _MissingSpec()
195
+
196
+
197
+ def _dominant_executor(plan: ChangePlan) -> str:
198
+ for s in plan.steps:
199
+ spec = spec_for(s.action)
200
+ if spec:
201
+ return spec.executor
202
+ return ""
203
+
204
+
205
+ _REQUIRE_DECISIONS = {
206
+ "require-dual-human": "dual-human",
207
+ "require-security-review": "security-review",
208
+ "require-owner": "resource-owner",
209
+ "require-platform-owner": "platform-owner",
210
+ }
211
+
212
+
213
+ def required_approval_type(env: ExecutionEnvelope,
214
+ environment: str = "") -> str:
215
+ """§43/§54 — `require-*` policy decisions and prod R4+ risk escalate
216
+ the approval type an envelope demands (break-glass still bypasses
217
+ with its own audit trail)."""
218
+ for d in env.policy_decisions or []:
219
+ eff = d.get("decision", "")
220
+ if eff in _REQUIRE_DECISIONS:
221
+ return _REQUIRE_DECISIONS[eff]
222
+ risk_class = (env.risk or {}).get("risk_class", "")
223
+ if environment == "prod" and risk_class in ("R4", "R5"):
224
+ return "dual-human"
225
+ if risk_class == "R5":
226
+ return "dual-human"
227
+ return ""
228
+
229
+
230
+ def execute(op: Operation, env: ExecutionEnvelope, *,
231
+ approvals: list[Approval],
232
+ transports: dict[str, Any] | None = None,
233
+ ledger: OperationLedger | None = None,
234
+ locks: LockTable | None = None,
235
+ dry_run: bool = True,
236
+ break_glass: BreakGlass | None = None,
237
+ preconditions: dict[str, Any] | None = None,
238
+ material_store=None,
239
+ at: str | None = None) -> dict[str, Any]:
240
+ """§8 execution stages: approval → preconditions → locks → steps.
241
+ `dry_run=True` is the default; callers must opt out explicitly."""
242
+ ledger = ledger or OperationLedger()
243
+ locks = locks or LockTable()
244
+ transports = transports or {}
245
+
246
+ # 1. hash-bound approval re-check (TOCTOU)
247
+ # §61 — parameter bounds check against the *actual* step params:
248
+ # approving replicas 3→5 must not authorize 3→50.
249
+ merged_params: dict[str, Any] = {}
250
+ for a in env.actions:
251
+ merged_params.update(a.get("params") or {})
252
+ chk = check_approval(approvals, subject_hash=env.hash(),
253
+ scope=env.scope, params=merged_params,
254
+ required_type=required_approval_type(
255
+ env, (preconditions or {}).get(
256
+ "environment", "")),
257
+ break_glass=break_glass,
258
+ current_plan_hash=env.hash(), at=at)
259
+ if not chk.ok:
260
+ ledger.append("approval.denied", op.operation_id,
261
+ data={"refusal": chk.refusal})
262
+ return {"ok": False, "stage": "approval",
263
+ "refusal": chk.refusal}
264
+
265
+ # 2. preconditions — including TOCTOU resource-state comparison
266
+ # (§57–58) and owner drift when the caller supplies them.
267
+ pc = check_preconditions(
268
+ observation=(preconditions or {}).get("observation"),
269
+ max_observation_age_s=(preconditions or {}).get(
270
+ "max_observation_age_s", 900),
271
+ expected_resource_state=(preconditions or {}).get(
272
+ "expected_resource_state"),
273
+ current_resource_state=(preconditions or {}).get(
274
+ "current_resource_state"),
275
+ plan_hash=env.change_plan_hash,
276
+ current_plan_hash=(preconditions or {}).get(
277
+ "current_plan_hash", env.change_plan_hash),
278
+ owner=(preconditions or {}).get("owner", ""),
279
+ current_owner=(preconditions or {}).get("current_owner", ""),
280
+ policy_decision=(preconditions or {}).get("policy_decision"),
281
+ approval_valid=True,
282
+ maintenance_window=(preconditions or {}).get("maintenance_window"),
283
+ freeze_active=(preconditions or {}).get("freeze_active", False),
284
+ break_glass=chk.used_break_glass, at=at)
285
+ if not pc["ok"]:
286
+ ledger.append("precondition.failed", op.operation_id,
287
+ data=pc["refusal"])
288
+ return {"ok": False, "stage": "preconditions",
289
+ "refusal": pc["refusal"], "checks": pc["checks"]}
290
+
291
+ # 3. locks
292
+ lk = locks.acquire(op.resources or env.scope, op.operation_id, ledger)
293
+ if "refusal" in lk:
294
+ return {"ok": False, "stage": "locking", "refusal": lk}
295
+
296
+ # 4. FSM — walk the canonical chain to executing; resume-safe: a step
297
+ # whose completion is already legal-from-state is skipped.
298
+ for st in ("planned", "simulated", "policy-reviewed",
299
+ "awaiting-approval", "approved", "executing"):
300
+ if op.state == st:
301
+ continue
302
+ r = op.transition(st, ledger)
303
+ if "refusal" in r:
304
+ locks.release(op.operation_id, ledger)
305
+ return {"ok": False, "stage": f"transition→{st}",
306
+ "refusal": r, "state": op.state}
307
+
308
+ # 5. DAG execution — waves; mutating steps need transports.
309
+ # Cycle 4.1 §30: every mutating step captures RollbackMaterial
310
+ # (pre-state BEFORE the step, execution result AFTER) into the
311
+ # content-addressed store — never reused forward params.
312
+ from platformforge.ops.material import MaterialStore, capture_material
313
+ mstore = material_store or MaterialStore()
314
+ pre_state_ctx = (preconditions or {}).get("pre_state") or {}
315
+ sot_ctx = (preconditions or {}).get("sot") or {}
316
+ materials: dict[str, Any] = {}
317
+ plan_steps = {s["step_id"]: s for s in env.actions}
318
+ results: list[dict[str, Any]] = []
319
+ idem_seen: set[str] = set()
320
+ failed: list[str] = []
321
+ for wave in _waves(env):
322
+ for sid in wave:
323
+ a = plan_steps[sid]
324
+ spec = spec_for(a["action"])
325
+ ex = executor_for(a["action"])
326
+ step = ExecutionStep(step_id=sid, action=a["action"],
327
+ params=a.get("params", {}))
328
+ key = step.effect_key(env.change_plan_hash)
329
+ if key in idem_seen:
330
+ ledger.append("step.skipped", op.operation_id,
331
+ data={"step_id": sid,
332
+ "reason": "idempotent-duplicate"})
333
+ continue
334
+ idem_seen.add(key)
335
+ mat = None
336
+ if spec and spec.mutating:
337
+ mat = capture_material(
338
+ a["action"], sid, op.operation_id,
339
+ a.get("params", {}),
340
+ spec.pre_state_requirements,
341
+ pre_state=_capture_pre_state(
342
+ a["action"], a.get("params", {}),
343
+ pre_state_ctx.get(sid)),
344
+ source_of_truth=sot_ctx.get(sid) or
345
+ sot_ctx.get("__default__"),
346
+ provenance="observed" if pre_state_ctx.get(sid)
347
+ else "declared")
348
+ ledger.append("step.started", op.operation_id,
349
+ data={"step_id": sid, "action": a["action"],
350
+ "dry_run": dry_run,
351
+ "material": mat.hash() if mat else None})
352
+ if spec is None or ex is None:
353
+ r = {"ok": False, "refusal": {
354
+ "refusal": "PF-OPS-UNKNOWN-ACTION",
355
+ "unlock": f"{a['action']} not in vocabulary"}}
356
+ else:
357
+ t = transports.get(spec.executor)
358
+ rec = ex.run_step(sid, a["action"], a.get("params", {}),
359
+ transport=t, dry_run=dry_run)
360
+ r = rec.to_dict()
361
+ if mat is not None:
362
+ # post-result capture (§12/§31) + persist immutable
363
+ mat.execution_result.update(
364
+ {k: v for k, v in (r.get("outputs") or {}).items()
365
+ if k in spec.post_result_requirements
366
+ or k.endswith("commit") or k in ("pr", "output")})
367
+ for k in spec.post_result_requirements:
368
+ if k in r and k not in mat.execution_result:
369
+ mat.execution_result[k] = r[k]
370
+ mstore.put(mat)
371
+ r["material_hash"] = mat.hash()
372
+ materials[sid] = mat
373
+ results.append(r)
374
+ if r.get("ok"):
375
+ ledger.append("step.completed", op.operation_id,
376
+ data={"step_id": sid,
377
+ "receipt": r.get("receipt_hash")})
378
+ else:
379
+ failed.append(sid)
380
+ ledger.append("step.failed", op.operation_id,
381
+ data={"step_id": sid,
382
+ "refusal": r.get("refusal"),
383
+ "error": r.get("error", "")[:400]})
384
+ # stop the DAG — a failed dep blocks downstream
385
+ blocked = [s for w in _waves(env) for s in w
386
+ if s not in {x["step_id"] for x in results}]
387
+ for b in blocked:
388
+ ledger.append("step.skipped", op.operation_id,
389
+ data={"step_id": b,
390
+ "reason": f"dep-failed:{sid}"})
391
+ op.transition("failed", ledger)
392
+ locks.release(op.operation_id, ledger)
393
+ return {"ok": False, "stage": "execute",
394
+ "failed_step": sid, "results": results,
395
+ "state": op.state}
396
+
397
+ op.transition("verifying", ledger)
398
+ locks.release(op.operation_id, ledger)
399
+ # §30 — rebuild the rollback plan against captured materials so the
400
+ # post-execution plan is honest about executability.
401
+ fwd_steps = [{"step_id": a["step_id"], "action": a["action"],
402
+ "params": a.get("params", {})} for a in env.actions]
403
+ rb = build_rollback_plan(
404
+ fwd_steps, materials=materials,
405
+ context={"environment": (preconditions or {}).get(
406
+ "environment", "unknown"),
407
+ "automatic_allowed": (preconditions or {}).get(
408
+ "automatic_rollback", False)})
409
+ return {"ok": True, "stage": "execute", "dry_run": dry_run,
410
+ "results": results, "state": op.state,
411
+ "ledger_tip": ledger.tip,
412
+ "materials": {sid: m.to_dict() for sid, m
413
+ in materials.items()},
414
+ "material_hashes": {sid: m.hash() for sid, m
415
+ in materials.items()},
416
+ "rollback_plan": rb.to_dict()}
417
+
418
+
419
+ def _capture_pre_state(action: str, params: dict[str, Any],
420
+ ctx_state: dict[str, Any] | None
421
+ ) -> dict[str, Any]:
422
+ """§8/§31 — assemble the pre-change snapshot for one step.
423
+
424
+ Sources, weakest→strongest:
425
+ 1. params the action itself declares (`current_replicas`,
426
+ `resource_version`, `to_revision`…)
427
+ 2. caller-supplied `pre_state` ctx (observation-sourced values
428
+ per step_id) — wins on conflict, provenance observed.
429
+ """
430
+ pre: dict[str, Any] = {}
431
+ if action == "kubernetes.scale":
432
+ if params.get("current_replicas") is not None:
433
+ pre["replicas"] = params["current_replicas"]
434
+ if params.get("resource_version"):
435
+ pre["resource_version"] = params["resource_version"]
436
+ elif action == "kubernetes.annotate":
437
+ if params.get("previous_annotations") is not None:
438
+ pre["annotations"] = dict(params["previous_annotations"])
439
+ if params.get("resource_version"):
440
+ pre["resource_version"] = params["resource_version"]
441
+ elif action == "kubernetes.rollout_restart":
442
+ if params.get("current_revision") is not None:
443
+ pre["revision"] = params["current_revision"]
444
+ elif action == "argocd.sync":
445
+ for k in ("history_id", "previous_revision", "repo",
446
+ "git_revert_commit"):
447
+ if params.get(k) is not None:
448
+ pre[k] = params[k]
449
+ elif action in ("terraform.apply_saved_plan",
450
+ "tofu.apply_saved_plan"):
451
+ for k in ("source_ref", "workspace", "state_serial",
452
+ "resource_addresses", "plan_hash"):
453
+ if params.get(k) is not None:
454
+ pre[k] = params[k]
455
+ elif action in ("git.apply_patch", "git.commit", "git.open_pr"):
456
+ for k in ("pre_change_commit", "repo"):
457
+ if params.get(k) is not None:
458
+ pre[k] = params[k]
459
+ elif action == "git.delete_branch":
460
+ if params.get("branch_sha") is not None:
461
+ pre["branch_sha"] = params["branch_sha"]
462
+ if ctx_state:
463
+ for k, v in ctx_state.items(): # observed wins (§7)
464
+ if v is not None:
465
+ pre[k] = v
466
+ return pre
467
+
468
+
469
+ def _waves(env: ExecutionEnvelope) -> list[list[str]]:
470
+ """Waves from step deps stored as params.__depends_on__ or envelope
471
+ order — simple: use declared order unless depends_on is present."""
472
+ deps = {a["step_id"]: set((a.get("params") or {}).get(
473
+ "__depends_on__", [])) for a in env.actions}
474
+ done: set[str] = set()
475
+ out: list[list[str]] = []
476
+ total = len(env.actions)
477
+ while len(done) < total:
478
+ w = [s for s, d in deps.items() if d <= done and s not in done]
479
+ if not w:
480
+ w = [s for s in deps if s not in done] # malformed → all
481
+ out.append(w)
482
+ done.update(w)
483
+ return out
484
+
485
+
486
+ def finalize_verify(op: Operation, ledger: OperationLedger,
487
+ verification_result: dict[str, Any]) -> dict[str, Any]:
488
+ conv = verification_result.get("convergence", "unknown")
489
+ target = {"converged": "converged", "partially-converged":
490
+ "partially-converged", "not-converged": "failed",
491
+ "regressed": "rollback-planned",
492
+ "unknown": "unresolved"}.get(conv, "unresolved")
493
+ if op.state == "verifying":
494
+ r = op.transition(target, ledger, data={"convergence": conv})
495
+ if "refusal" in r:
496
+ return r
497
+ ledger.append("verified", op.operation_id, data=verification_result)
498
+ return {"operation_id": op.operation_id, "state": op.state,
499
+ "convergence": conv}
500
+
501
+
502
+ def execute_rollback(op: Operation, env: ExecutionEnvelope, *,
503
+ transports: dict[str, Any] | None = None,
504
+ ledger: OperationLedger | None = None,
505
+ locks: LockTable | None = None,
506
+ completed_results: list[dict[str, Any]] | None = None,
507
+ materials: dict[str, Any] | None = None,
508
+ rollback_plan: dict[str, Any] | None = None,
509
+ trigger: dict[str, Any] | None = None,
510
+ post_rollback_state: dict[str, Any] | None = None,
511
+ dry_run: bool = True,
512
+ at: str | None = None) -> dict[str, Any]:
513
+ """§40–43/§119–127/§149–160 — execute a rollback built from
514
+ MATERIAL, under the same locks and idempotency as the forward pass.
515
+
516
+ - `trigger` (§98): typed cause {type, verification_id,
517
+ observed_delta, slo_evidence, at}. Never a bare string.
518
+ - `rollback_plan` (v2): built from captured materials. Only
519
+ status=executable runs typed actions; manual-only/impossible/
520
+ unresolved/requires-replan refuse honestly.
521
+ - Idempotent (§156): an already rolled-back op returns the prior
522
+ receipt instead of re-executing; same locks as forward (§158).
523
+ - A failed rollback step lands in `failed` with stage=rollback —
524
+ never `rolled-back` (§149–153); no automatic second rollback.
525
+ - `post_rollback_state` (§41–43): caller-supplied observation used
526
+ to verify restored|partially-restored|regressed|unknown.
527
+ """
528
+ ledger = ledger or OperationLedger()
529
+ locks = locks or LockTable()
530
+ trigger = dict(trigger or {"type": "manual"})
531
+ rb = dict(rollback_plan or env.rollback or {})
532
+ strategy = rb.get("strategy", "unknown")
533
+ status = rb.get("status", "unresolved")
534
+
535
+ # §156 — duplicate rollback request → idempotent receipt
536
+ if op.state == "rolled-back":
537
+ return {"ok": True, "state": op.state,
538
+ "idempotent": "already-rolled-back"}
539
+
540
+ # §36 — only executable plans execute; everything else is an
541
+ # honest refusal, never an invented action
542
+ if status != "executable":
543
+ return {"ok": False, "state": op.state,
544
+ "refusal": {"refusal": "PF-OPS-ROLLBACK-NOT-EXECUTABLE",
545
+ "unlock": f"rollback status is "
546
+ f"'{status}' — manual remediation "
547
+ "or a new governed plan required"},
548
+ "strategy": strategy, "status": status,
549
+ "replans": rb.get("replans", []),
550
+ "limitations": rb.get("limitations", [])}
551
+
552
+ rb_actions = list(rb.get("actions", []))
553
+ mats = dict(materials or {})
554
+ if not rb_actions and completed_results:
555
+ comp = compensate_for([{"step_id": r.get("step_id"),
556
+ "action": r.get("action"),
557
+ "params": r.get("outputs", {}).get(
558
+ "params", {}),
559
+ "status": "completed" if r.get("ok")
560
+ else "failed"}
561
+ for r in completed_results],
562
+ materials=mats)
563
+ rb_actions = comp["compensating_actions"]
564
+ if comp["manual_steps"]:
565
+ rb.setdefault("limitations", []).append(
566
+ f"manual compensation needed for {comp['manual_steps']}")
567
+ if not rb_actions:
568
+ return {"ok": False, "state": op.state,
569
+ "refusal": {"refusal": "PF-OPS-ROLLBACK-EMPTY",
570
+ "unlock": "no rollback actions derivable — "
571
+ "manual remediation required"}}
572
+
573
+ # §158 — rollback holds the same resource locks as the forward op
574
+ lk = locks.acquire(op.resources or env.scope, op.operation_id,
575
+ ledger)
576
+ if "refusal" in lk:
577
+ return {"ok": False, "state": op.state, "stage": "locking",
578
+ "refusal": lk}
579
+
580
+ # §159 — rollback preconditions: the resource must still match the
581
+ # failed-forward state; external drift → human review, never blind
582
+ # revert of someone else's change.
583
+ precondition_refusals = _rollback_preconditions(
584
+ mats, rb.get("steps", []))
585
+ if precondition_refusals:
586
+ locks.release(op.operation_id, ledger)
587
+ return {"ok": False, "state": op.state,
588
+ "stage": "rollback-preconditions",
589
+ "refusal": {"refusal": "PF-OPS-ROLLBACK-PRECONDITION",
590
+ "unlock": "resource no longer matches "
591
+ "failed-forward state — human "
592
+ "review required (§159)",
593
+ "details": precondition_refusals}}
594
+
595
+ # legal walk into rolling-back
596
+ for st in ("rollback-planned", "rolling-back"):
597
+ if op.state == st:
598
+ continue
599
+ if st == "rollback-planned" and op.state in \
600
+ ("executing", "rolling-back"):
601
+ continue # executing may jump straight to rolling-back
602
+ r = op.transition(st, ledger, data={"strategy": strategy})
603
+ if "refusal" in r:
604
+ locks.release(op.operation_id, ledger)
605
+ return {"ok": False, "state": op.state, "refusal": r}
606
+
607
+ ledger.append("rollback.started", op.operation_id,
608
+ data={"strategy": strategy, "status": status,
609
+ "trigger": trigger,
610
+ "material_hashes": rb.get("material_hashes", []),
611
+ "actions": len(rb_actions)})
612
+ results: list[dict[str, Any]] = []
613
+ transports = transports or {}
614
+ for i, a in enumerate(rb_actions):
615
+ sid = f"rb-{i}-{a.get('action', '?')}"
616
+ spec = spec_for(a.get("action", ""))
617
+ ex = executor_for(a.get("action", ""))
618
+ ledger.append("step.started", op.operation_id,
619
+ data={"step_id": sid, "action": a.get("action"),
620
+ "rollback": True, "dry_run": dry_run})
621
+ if spec is None or ex is None:
622
+ r = {"ok": False, "refusal": {
623
+ "refusal": "PF-OPS-UNKNOWN-ACTION",
624
+ "unlock": f"{a.get('action')} not in vocabulary"}}
625
+ else:
626
+ rec = ex.run_step(sid, a["action"], a.get("params", {}),
627
+ transport=transports.get(spec.executor),
628
+ dry_run=dry_run)
629
+ r = rec.to_dict()
630
+ results.append(r)
631
+ if r.get("ok"):
632
+ ledger.append("step.completed", op.operation_id,
633
+ data={"step_id": sid,
634
+ "receipt": r.get("receipt_hash"),
635
+ "rollback": True})
636
+ else:
637
+ ledger.append("step.failed", op.operation_id,
638
+ data={"step_id": sid,
639
+ "refusal": r.get("refusal"),
640
+ "rollback": True})
641
+ # §149–153 — failed rollback → failed + stage detail +
642
+ # human escalation; NO automatic rollback-of-rollback
643
+ op.transition("failed", ledger,
644
+ data={"stage": "rollback",
645
+ "escalation": "human-required"})
646
+ locks.release(op.operation_id, ledger)
647
+ return {"ok": False, "state": op.state,
648
+ "stage": "rollback",
649
+ "rollback_failed": True,
650
+ "escalation": "human-required",
651
+ "failed_step": sid, "results": results}
652
+
653
+ # §41–43 — rollback verification: command success ≠ restored.
654
+ ver = verify_rollback(mats, post_rollback_state or {})
655
+ ledger.append("rollback.completed", op.operation_id,
656
+ data={"strategy": strategy,
657
+ "actions": len(results),
658
+ "rollback_verification": ver["convergence"]})
659
+ op.transition("rolled-back", ledger,
660
+ data={"rollback_verification": ver["convergence"]})
661
+ locks.release(op.operation_id, ledger)
662
+ # §40 — rollback receipt: trigger + material + strategy + result +
663
+ # verification, hash-linked to the forward operation
664
+ receipt = {
665
+ "operation_id": op.operation_id,
666
+ "stage": "rollback",
667
+ "result": ver["convergence"],
668
+ "trigger": trigger,
669
+ "strategy": strategy,
670
+ "material_hashes": rb.get("material_hashes", []),
671
+ "rollback_plan_ref": env.hash(),
672
+ "actions": len(results),
673
+ "rollback_verification": ver,
674
+ "comparisons": ver.get("comparisons", []),
675
+ }
676
+ return {"ok": ver["convergence"] in ("restored", "not-applicable"),
677
+ "state": op.state, "results": results,
678
+ "strategy": strategy,
679
+ "rollback_verification": ver["convergence"],
680
+ "receipt": receipt}
681
+
682
+
683
+ def _rollback_preconditions(materials: dict[str, Any],
684
+ plan_steps: list[dict[str, Any]]
685
+ ) -> list[dict[str, Any]]:
686
+ """§159 — per-step precondition records. A material whose
687
+ `pre_state` carries `current_matches_forward=False` (caller flags
688
+ external drift between forward failure and rollback attempt)
689
+ requires human review."""
690
+ bad = []
691
+ step_map = {s.get("step_id"): s for s in plan_steps}
692
+ for sid, mat in materials.items():
693
+ m = mat.to_dict() if hasattr(mat, "to_dict") else dict(mat)
694
+ pre = m.get("pre_state", {})
695
+ if pre.get("current_matches_forward") is False:
696
+ bad.append({"step_id": sid,
697
+ "reason": "external-change-detected",
698
+ "action": step_map.get(sid, {}).get("action")})
699
+ return bad
700
+
701
+
702
+ def verify_rollback(materials: dict[str, Any],
703
+ post_state: dict[str, Any] | None = None
704
+ ) -> dict[str, Any]:
705
+ """§41–43 — compare captured pre-state with the post-rollback
706
+ observation. Restored only when every captured dimension matches.
707
+
708
+ `post_state`: {step_id: {dim: observed_value}} from a fresh
709
+ observation after rollback. Missing observations → unknown, never
710
+ restored-by-assumption.
711
+ """
712
+ post = post_state or {}
713
+ if not materials:
714
+ return {"convergence": "not-applicable",
715
+ "comparisons": [],
716
+ "note": "no materials captured — nothing to verify"}
717
+ if not post:
718
+ return {"convergence": "unknown",
719
+ "comparisons": [],
720
+ "note": "no post-rollback observation — cannot claim "
721
+ "restored (§43)"}
722
+ comps: list[dict[str, Any]] = []
723
+ restored = regressed = missing = 0
724
+ # concurrency/provenance tokens are captured for preconditions,
725
+ # not restorable values — a new write always bumps them
726
+ non_restorable = {"resource_version", "uid",
727
+ "current_matches_forward", "generation"}
728
+ for sid, mat in materials.items():
729
+ m = mat.to_dict() if hasattr(mat, "to_dict") else dict(mat)
730
+ want = {k: v for k, v in m.get("pre_state", {}).items()
731
+ if k not in non_restorable}
732
+ got = post.get(sid) or {}
733
+ for dim, expected in want.items():
734
+ observed = got.get(dim, "<unobserved>")
735
+ if observed == "<unobserved>":
736
+ outcome = "unobserved"
737
+ missing += 1
738
+ elif observed == expected:
739
+ outcome = "restored"
740
+ restored += 1
741
+ else:
742
+ outcome = "different"
743
+ regressed += 1
744
+ comps.append({"step_id": sid, "dimension": dim,
745
+ "expected": expected, "observed": observed,
746
+ "outcome": outcome})
747
+ total = restored + regressed + missing
748
+ if missing == total:
749
+ conv = "unknown"
750
+ elif regressed:
751
+ conv = "regressed" if restored == 0 else "partially-restored"
752
+ elif missing:
753
+ conv = "partially-restored"
754
+ else:
755
+ conv = "restored"
756
+ return {"convergence": conv, "comparisons": comps,
757
+ "restored": restored, "different": regressed,
758
+ "unobserved": missing}