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,157 @@
1
+ """Knowledge packs — operational knowledge, not a provenance registry
2
+ (cycle 2.1 §60–68).
3
+
4
+ A pack is a YAML document under ``knowledge/<domain>/<id>.yaml``:
5
+
6
+ ```yaml
7
+ schema: platformforge/knowledge/v1
8
+ id: kubernetes-api-deprecations
9
+ domain: kubernetes
10
+ applies_to: {versions: [kubernetes]}
11
+ sources: [kubernetes-docs]
12
+ claims:
13
+ - id: k8s-dep-v122
14
+ statement: "..."
15
+ versions: {kubernetes: ">=1.22"}
16
+ used_by:
17
+ rules: [PF-K8S-030]
18
+ analyzers: [k8s]
19
+ ```
20
+
21
+ Consumption is checked, not asserted: ``contract_check`` resolves every
22
+ ``sources`` id against the source registry, every ``used_by.rules`` against
23
+ the rule catalog, and every ``used_by.analyzers`` against the analyzer
24
+ registry — a pack nothing consumes is dead weight (§63/§136). Each pack
25
+ carries a ``content_hash`` (sha256 of the canonical claim set) so consumers
26
+ detect drift when content changes (§68).
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import hashlib
32
+ import json
33
+ from dataclasses import dataclass, field
34
+ from pathlib import Path
35
+ from typing import Any
36
+
37
+ import yaml
38
+
39
+ SCHEMA = "platformforge/knowledge/v1"
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class Claim:
44
+ claim_id: str
45
+ statement: str
46
+ versions: dict[str, str] = field(default_factory=dict)
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class Pack:
51
+ pack_id: str
52
+ domain: str
53
+ applies_to: dict[str, Any]
54
+ sources: tuple[str, ...]
55
+ claims: tuple[Claim, ...]
56
+ used_by: dict[str, list[str]]
57
+ path: str = ""
58
+
59
+ @property
60
+ def content_hash(self) -> str:
61
+ """§68 — sha256 over the canonical claim set; drift detectable."""
62
+ blob = json.dumps({
63
+ "id": self.pack_id,
64
+ "claims": [{"id": c.claim_id, "statement": c.statement,
65
+ "versions": c.versions} for c in self.claims],
66
+ }, sort_keys=True)
67
+ return hashlib.sha256(blob.encode()).hexdigest()[:16]
68
+
69
+ def to_dict(self) -> dict[str, Any]:
70
+ return {"id": self.pack_id, "domain": self.domain,
71
+ "path": self.path, "content_hash": self.content_hash,
72
+ "sources": list(self.sources),
73
+ "claims": [{"id": c.claim_id, "statement": c.statement,
74
+ "versions": c.versions} for c in self.claims],
75
+ "used_by": self.used_by}
76
+
77
+ @classmethod
78
+ def from_dict(cls, d: dict[str, Any], path: str = "") -> Pack:
79
+ if d.get("schema") != SCHEMA:
80
+ raise ValueError(f"{path or d.get('id')}: schema must be "
81
+ f"{SCHEMA}")
82
+ missing = [k for k in ("id", "domain", "sources", "claims")
83
+ if k not in d]
84
+ if missing:
85
+ raise ValueError(f"{d.get('id', path)}: missing {missing}")
86
+ claims = tuple(Claim(str(c["id"]), str(c["statement"]),
87
+ dict(c.get("versions") or {}))
88
+ for c in d["claims"])
89
+ return cls(pack_id=str(d["id"]), domain=str(d["domain"]),
90
+ applies_to=dict(d.get("applies_to") or {}),
91
+ sources=tuple(str(s) for s in d["sources"]),
92
+ claims=claims,
93
+ used_by=dict(d.get("used_by") or {}), path=path)
94
+
95
+
96
+ class PackRegistry:
97
+ def __init__(self, root: str | Path | None = None):
98
+ if root is None:
99
+ from platformforge.resources import data_path
100
+ root = data_path("knowledge")
101
+ self.root = Path(root)
102
+ self.packs: dict[str, Pack] = {}
103
+ if self.root.is_dir():
104
+ for f in sorted(self.root.rglob("*.yaml")):
105
+ if f.name == "sources.yaml":
106
+ continue
107
+ d = yaml.safe_load(f.read_text())
108
+ if not isinstance(d, dict) or d.get("schema") != SCHEMA:
109
+ continue
110
+ p = Pack.from_dict(d, path=str(f.relative_to(self.root)))
111
+ self.packs[p.pack_id] = p
112
+
113
+ @classmethod
114
+ def default(cls) -> PackRegistry:
115
+ return cls()
116
+
117
+ def for_rule(self, rule_id: str) -> list[Pack]:
118
+ return [p for p in self.packs.values()
119
+ if rule_id in (p.used_by.get("rules") or [])]
120
+
121
+ def for_analyzer(self, name: str) -> list[Pack]:
122
+ return [p for p in self.packs.values()
123
+ if name in (p.used_by.get("analyzers") or [])]
124
+
125
+ def contract_check(self) -> dict[str, Any]:
126
+ """Every pack consumed by something real: sources resolve against
127
+ the registry, rules against the catalog, analyzers against the lab
128
+ analyzer table."""
129
+ from platformforge.knowledge.registry import SourceRegistry
130
+ from platformforge.lab.runner import _ANALYZERS
131
+ from platformforge.resources import data_path
132
+ from platformforge.rules import load_catalog
133
+ sources = SourceRegistry.default().entries
134
+ rules = {r.rule_id
135
+ for r in load_catalog(data_path("rules", "catalog"))}
136
+ problems: list[dict[str, Any]] = []
137
+ consumed = 0
138
+ for p in sorted(self.packs.values(), key=lambda x: x.pack_id):
139
+ bad_src = [s for s in p.sources if s not in sources]
140
+ bad_rules = [r for r in p.used_by.get("rules", [])
141
+ if r not in rules]
142
+ bad_an = [a for a in p.used_by.get("analyzers", [])
143
+ if a not in _ANALYZERS]
144
+ if not p.used_by.get("rules") and not p.used_by.get("analyzers"):
145
+ problems.append({"pack": p.pack_id, "issue": "unconsumed"})
146
+ else:
147
+ consumed += 1
148
+ for kind, ids in (("unknown_source", bad_src),
149
+ ("unknown_rule", bad_rules),
150
+ ("unknown_analyzer", bad_an)):
151
+ for i in ids:
152
+ problems.append({"pack": p.pack_id, "issue": kind,
153
+ "ref": i})
154
+ if not p.claims:
155
+ problems.append({"pack": p.pack_id, "issue": "no_claims"})
156
+ return {"packs": len(self.packs), "consumed": consumed,
157
+ "problems": problems, "ok": not problems}
@@ -0,0 +1,249 @@
1
+ """Source registry + knowledge freshness.
2
+
3
+ Old documentation is never eternal truth. Every version-dependent claim cites
4
+ a source entry; freshness is computed, not assumed.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import time
10
+ from dataclasses import dataclass, field
11
+ from datetime import date, datetime, timezone
12
+
13
+ UTC = timezone.utc # datetime.UTC alias is 3.11+; keep 3.10 compatible
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ import yaml
18
+
19
+ from platformforge.models import FreshnessStatus
20
+
21
+ FRESH_DAYS = 120 # verified within ~4 months
22
+ STALE_DAYS = 365 # older than a year → stale by default
23
+
24
+ # Closed vocabulary for `source_authority` — asserted by the CI gate.
25
+ SOURCE_AUTHORITIES = ("official", "vendor-research", "first-party-sibling")
26
+
27
+
28
+ @dataclass
29
+ class SourceEntry:
30
+ id: str
31
+ source: str
32
+ source_authority: str = "unknown"
33
+ retrieved_at: str = ""
34
+ product: str = ""
35
+ version: str = ""
36
+ releases_tracked: list[str] = field(default_factory=list)
37
+ scope: str = ""
38
+ cloud: str = ""
39
+ deprecated: bool = False
40
+ superseded_by: str = ""
41
+ valid_from: str = ""
42
+ valid_until: str = ""
43
+ confidence: str = "medium"
44
+ notes: str = ""
45
+ aliases: list[str] = field(default_factory=list)
46
+
47
+ @classmethod
48
+ def from_dict(cls, d: dict[str, Any]) -> SourceEntry:
49
+ known = {f for f in cls.__dataclass_fields__}
50
+ return cls(**{k: v for k, v in d.items() if k in known})
51
+
52
+ def freshness(self, today: date | None = None) -> str:
53
+ """current|fresh|stale|deprecated|superseded|conflicted|unresolved."""
54
+ today = today or datetime.now(UTC).date()
55
+ if self.deprecated:
56
+ return FreshnessStatus.DEPRECATED
57
+ if self.superseded_by:
58
+ return FreshnessStatus.SUPERSEDED
59
+ if self.valid_until and str(self.valid_until) < today.isoformat():
60
+ return FreshnessStatus.STALE
61
+ if self.valid_from and str(self.valid_from) > today.isoformat():
62
+ return FreshnessStatus.UNRESOLVED
63
+ if not self.retrieved_at:
64
+ return FreshnessStatus.UNRESOLVED
65
+ try:
66
+ days = (today - date.fromisoformat(str(self.retrieved_at)[:10])).days
67
+ except ValueError:
68
+ return FreshnessStatus.UNRESOLVED
69
+ if days <= FRESH_DAYS:
70
+ return FreshnessStatus.CURRENT if days <= 30 else FreshnessStatus.FRESH
71
+ return FreshnessStatus.STALE if days > STALE_DAYS else FreshnessStatus.FRESH
72
+
73
+
74
+ class SourceRegistry:
75
+ def __init__(self, path: str | Path):
76
+ self.path = Path(path)
77
+ doc = yaml.safe_load(self.path.read_text())
78
+ self.schema = doc.get("schema", "")
79
+ self.entries: dict[str, SourceEntry] = {}
80
+ self.duplicate_ids: list[str] = []
81
+ for e in doc.get("sources", []):
82
+ if e["id"] in self.entries:
83
+ self.duplicate_ids.append(e["id"])
84
+ continue
85
+ self.entries[e["id"]] = SourceEntry.from_dict(e)
86
+
87
+ @classmethod
88
+ def default(cls) -> SourceRegistry:
89
+ from platformforge.resources import data_path
90
+ return cls(data_path("knowledge", "sources.yaml"))
91
+
92
+ def get(self, source_id: str) -> SourceEntry | None:
93
+ return self.entries.get(source_id)
94
+
95
+ def resolve(self, ref: str) -> SourceEntry | None:
96
+ """Canonical id, else explicit `aliases:` declared on an entry.
97
+ No inference — a ref that is neither is unresolvable."""
98
+ if ref in self.entries:
99
+ return self.entries[ref]
100
+ for e in self.entries.values():
101
+ if ref in e.aliases:
102
+ return e
103
+ return None
104
+
105
+ def check(self, today: date | None = None) -> list[dict[str, Any]]:
106
+ """Freshness report for every registered source."""
107
+ return [
108
+ {"id": e.id, "product": e.product, "version": e.version,
109
+ "retrieved_at": e.retrieved_at, "status": e.freshness(today),
110
+ "superseded_by": e.superseded_by}
111
+ for e in sorted(self.entries.values(), key=lambda x: x.id)
112
+ ]
113
+
114
+ def unresolved(self, today: date | None = None) -> list[str]:
115
+ return [r["id"] for r in self.check(today) if r["status"] in
116
+ (FreshnessStatus.UNRESOLVED, FreshnessStatus.CONFLICTED)]
117
+
118
+
119
+ # §46 — knowledge entry contract: these fields are mandatory for an
120
+ # entry to count as knowledge-backed.
121
+ REQUIRED = ("id", "source", "source_authority", "retrieved_at",
122
+ "confidence")
123
+
124
+ def contract_check(self) -> dict[str, Any]:
125
+ """§46 — report entries missing contract fields, duplicate ids,
126
+ and alias collisions (an alias owned by two entries resolves to
127
+ whichever loads first — silent wrong-linkage)."""
128
+ bad = {}
129
+ for e in self.entries.values():
130
+ missing = [f for f in self.REQUIRED
131
+ if not getattr(e, f, None)]
132
+ if missing:
133
+ bad[e.id or "?"] = missing
134
+ alias_owner: dict[str, str] = {}
135
+ alias_collisions: dict[str, list[str]] = {}
136
+ for e in self.entries.values():
137
+ for a in e.aliases:
138
+ if a in alias_owner and alias_owner[a] != e.id:
139
+ alias_collisions.setdefault(a, []).append(e.id)
140
+ else:
141
+ alias_owner[a] = e.id
142
+ for a, owners in alias_collisions.items():
143
+ owners.insert(0, alias_owner[a])
144
+ ok = not bad and not self.duplicate_ids and not alias_collisions
145
+ return {"entries": len(self.entries), "invalid": bad,
146
+ "duplicate_ids": self.duplicate_ids,
147
+ "alias_collisions": alias_collisions,
148
+ "ok": ok}
149
+
150
+ def link_rules(self, *catalog_dirs: str | Path) -> dict[str, Any]:
151
+ """§44/§47 + cycle 2.1 — rule→source linkage by exact canonical id.
152
+
153
+ Every `sources:` entry on a rule must be a registered id (or an
154
+ entry's explicit `aliases:` member). No domain-suffix inference —
155
+ `k8s.io` must never silently match `gateway-api.sigs.k8s.io`.
156
+ Unresolvable refs are reported `unlinked`, not silently trusted.
157
+ """
158
+ from platformforge.rules import load_catalog
159
+ linked, unlinked, bad_refs = {}, [], []
160
+ for r in load_catalog(*catalog_dirs):
161
+ hits = [{"source": s,
162
+ "entry": (self.resolve(s) or SourceEntry(
163
+ id="", source="")).id or None}
164
+ for s in r.sources]
165
+ if hits and all(h["entry"] for h in hits):
166
+ linked[r.rule_id] = [h["entry"] for h in hits]
167
+ else:
168
+ unlinked.append({"rule_id": r.rule_id, "sources": hits})
169
+ dangling = sorted(k for k in getattr(r, "source_refs", {})
170
+ if k not in r.sources)
171
+ if dangling:
172
+ bad_refs.append({"rule_id": r.rule_id,
173
+ "source_refs": dangling})
174
+ return {"linked": linked, "unlinked": unlinked, "bad_refs": bad_refs,
175
+ "coverage": len(linked) / max(len(linked) + len(unlinked), 1)}
176
+
177
+ def drift_report(self, *catalog_dirs: str | Path,
178
+ today: date | None = None) -> dict[str, Any]:
179
+ """§47 — knowledge drift: new major versions tracked but not
180
+ reflected, deprecated/superseded sources still cited, unavailable
181
+ sources, and rules whose backing sources went stale.
182
+
183
+ Never a bare "ok": every signal carries the entry/rule it
184
+ concerns and the reason."""
185
+ today = today or datetime.now(UTC).date()
186
+ link = self.link_rules(*catalog_dirs)
187
+ statuses = {r["id"]: r for r in self.check(today)}
188
+
189
+ signals: list[dict[str, Any]] = []
190
+ for e in sorted(self.entries.values(), key=lambda x: x.id):
191
+ st = statuses.get(e.id, {}).get("status", "unresolved")
192
+ if st in (FreshnessStatus.DEPRECATED,):
193
+ signals.append({"kind": "deprecated-source",
194
+ "source": e.id,
195
+ "reason": "entry marked deprecated"})
196
+ if st in (FreshnessStatus.SUPERSEDED,):
197
+ signals.append({"kind": "superseded-source",
198
+ "source": e.id,
199
+ "reason": f"superseded_by={e.superseded_by}"})
200
+ if st in (FreshnessStatus.STALE, FreshnessStatus.UNRESOLVED):
201
+ signals.append({"kind": "stale-source", "source": e.id,
202
+ "reason": f"freshness={st}"})
203
+ if not e.source:
204
+ signals.append({"kind": "unavailable-source",
205
+ "source": e.id,
206
+ "reason": "no source locator"})
207
+ # new major version: releases_tracked majors newer than the
208
+ # entry's declared version → knowledge drift signal
209
+ def _maj(v: str) -> int | None:
210
+ import re
211
+ m = re.match(r"\s*v?(\d+)", str(v))
212
+ return int(m.group(1)) if m else None
213
+ cur = _maj(e.version)
214
+ newer = sorted({_maj(r) for r in e.releases_tracked
215
+ if _maj(r) is not None and cur is not None
216
+ and _maj(r) > cur})
217
+ for mj in newer:
218
+ signals.append({
219
+ "kind": "new-major-version", "source": e.id,
220
+ "reason": f"entry pinned to {e.version} but v{mj} "
221
+ "is tracked — re-derive version-gated claims"})
222
+
223
+ # rules citing stale/deprecated/superseded sources
224
+ stale_ids = {s["source"] for s in signals
225
+ if s["kind"] in ("deprecated-source",
226
+ "superseded-source", "stale-source")}
227
+ stale_rules = [
228
+ {"rule_id": rid, "sources": [s for s in srcs
229
+ if s in stale_ids]}
230
+ for rid, srcs in link["linked"].items()
231
+ if any(s in stale_ids for s in srcs)]
232
+ return {"linkage": link,
233
+ "signals": signals,
234
+ "stale_rule_refs": stale_rules,
235
+ "unresolved": [u["rule_id"] for u in link["unlinked"]],
236
+ "counts": {"signals": len(signals),
237
+ "stale_rules": len(stale_rules),
238
+ "unlinked": len(link["unlinked"])}}
239
+
240
+
241
+ def knowledge_age_days(retrieved_at: str, today: date | None = None) -> int | None:
242
+ try:
243
+ return ((today or datetime.now(UTC).date()) - date.fromisoformat(str(retrieved_at)[:10])).days
244
+ except ValueError:
245
+ return None
246
+
247
+
248
+ def now_iso() -> str:
249
+ return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
@@ -0,0 +1,127 @@
1
+ """Knowledge economy (§208–215) — load only the relevant packs.
2
+
3
+ Selector inputs: task, domain, version, risk (§209). A pack declares
4
+ applicability (`applies_to`); PackApplicability is the verdict (§210).
5
+ Pack budget caps how many packs a task loads — never the whole registry
6
+ (§211). Stale packs are flagged `review_needed`, not silently trusted
7
+ (§212). Search tiers are lexical/graph — no embeddings required (§213–214).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ from dataclasses import asdict, dataclass
14
+ from typing import Any
15
+
16
+
17
+ @dataclass
18
+ class PackApplicability:
19
+ """§210 — why a pack does or doesn't apply to a task."""
20
+ pack_id: str
21
+ applicable: bool
22
+ reason: str
23
+ freshness: str = "unknown" # current|stale|unresolved
24
+ score: int = 0
25
+
26
+ def to_dict(self) -> dict[str, Any]:
27
+ return asdict(self)
28
+
29
+
30
+ def select_packs(packs: list[Any], *, domain: str = "",
31
+ versions: dict[str, str] | None = None,
32
+ risk: str = "low", max_packs: int = 8,
33
+ freshness_of=None) -> dict[str, Any]:
34
+ """§209–212 — select applicable packs within budget.
35
+
36
+ `packs` are knowledge.packs.Pack-like objects exposing
37
+ `pack_id/domain/applies_to/claims`. `freshness_of` maps a pack's
38
+ source ids to a freshness state (defaults: unknown → review_needed
39
+ for high risk).
40
+ """
41
+ versions = versions or {}
42
+ verdicts: list[PackApplicability] = []
43
+ for p in packs:
44
+ score, reasons = 0, []
45
+ if domain and getattr(p, "domain", "") == domain:
46
+ score += 2
47
+ reasons.append("domain match")
48
+ applies = getattr(p, "applies_to", {}) or {}
49
+ ok_versions = True
50
+ for prod, want in (applies.get("versions") or {}).items():
51
+ # applies_to.versions lists products the pack covers; the
52
+ # task's detected version for that product decides fit.
53
+ want = str(want)
54
+ have = versions.get(prod, "")
55
+ if prod in versions and _version_ok(have, want):
56
+ score += 2
57
+ reasons.append(f"{prod} {have} matches {want}")
58
+ elif prod not in versions:
59
+ pass # pack covers more than needed
60
+ if domain and getattr(p, "domain", "") and \
61
+ p.domain != domain:
62
+ ok_versions = False
63
+ reasons.append("domain mismatch")
64
+ verdicts.append(PackApplicability(
65
+ pack_id=getattr(p, "pack_id", "?"),
66
+ applicable=ok_versions and score > 0,
67
+ reason="; ".join(reasons) or "no applicability signal",
68
+ score=score))
69
+
70
+ applicable = sorted((v for v in verdicts if v.applicable),
71
+ key=lambda v: -v.score)
72
+ selected: list[PackApplicability] = []
73
+ for v in applicable[:max_packs]:
74
+ fr = freshness_of(v.pack_id) if freshness_of else "unknown"
75
+ v.freshness = fr
76
+ if fr in ("stale", "unresolved") and risk in ("high", "critical"):
77
+ v.reason += "; review_needed (stale pack under high risk)"
78
+ selected.append(v)
79
+ return {"selected": [v.to_dict() for v in selected],
80
+ "skipped": [v.to_dict() for v in verdicts
81
+ if not v.applicable],
82
+ "budget": max_packs,
83
+ "overflow": max(0, len(applicable) - max_packs)}
84
+
85
+
86
+ def _version_ok(have: str, want: str) -> bool:
87
+ """Minimal semver-ish range check (>=X.Y, >X.Y, ==X.Y, bare prefix)."""
88
+ m = re.match(r"^\s*(>=|>|<=|<|==)?\s*([\d.]+)", want)
89
+ if not m:
90
+ return have == want
91
+ op, target = m.groups()
92
+ h = [int(x) for x in have.split(".") if x.isdigit()]
93
+ t = [int(x) for x in target.split(".") if x.isdigit()]
94
+ n = max(len(h), len(t))
95
+ h += [0] * (n - len(h)); t += [0] * (n - len(t))
96
+ if op in (None, "=="):
97
+ return h == t
98
+ if op == ">=":
99
+ return h >= t
100
+ if op == ">":
101
+ return h > t
102
+ if op == "<=":
103
+ return h <= t
104
+ return h < t
105
+
106
+
107
+ def search_knowledge(packs: list[Any], query: str,
108
+ *, tier2_graph=None) -> dict[str, Any]:
109
+ """§213–214 — tiered search, no embeddings.
110
+
111
+ Tier 0: exact id/ref match. Tier 1: lexical substring/FTS over pack
112
+ id+domain+claims. Tier 2: caller-supplied graph expansion hook."""
113
+ q = query.lower()
114
+ t0 = [p for p in packs if getattr(p, "pack_id", "") == query]
115
+ if t0:
116
+ return {"tier": 0, "packs": t0}
117
+ t1 = [p for p in packs if q in getattr(p, "pack_id", "").lower()
118
+ or q in getattr(p, "domain", "").lower()
119
+ or any(q in c.statement.lower()
120
+ for c in getattr(p, "claims", ()))]
121
+ if t1:
122
+ return {"tier": 1, "packs": t1}
123
+ if tier2_graph is not None:
124
+ expanded = tier2_graph(query)
125
+ return {"tier": 2, "packs": expanded}
126
+ return {"tier": 2, "packs": [], "note": "no lexical match; "
127
+ "tier-2 graph expansion unavailable"}
@@ -0,0 +1,5 @@
1
+ """Forge Lab — bundled scenarios for evals, demos, regression detection."""
2
+
3
+ from platformforge.lab.runner import SCENARIOS_DIR, list_scenarios, run
4
+
5
+ __all__ = ["SCENARIOS_DIR", "list_scenarios", "run"]
@@ -0,0 +1,71 @@
1
+ """Chaos lab (§93) — deterministic fault injection *on the graph*.
2
+
3
+ Offline by construction: a fault removes or degrades nodes in a built
4
+ graph, then blast_radius computes the impacted set. Results are declared
5
+ simulations (`environment: simulation`), never live fault injection.
6
+
7
+ Production targets are refused unless the caller passes allow_prod — the
8
+ boundary is explicit, not implicit.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ import yaml
17
+
18
+ from platformforge.graph.model import Graph
19
+ from platformforge.graph.query import blast_radius
20
+
21
+ FAULTS = ("pod_kill", "latency_injection", "dependency_unavailable",
22
+ "dns_failure", "network_loss", "node_unavailable")
23
+
24
+ # faults that degrade edges (dependencies stay, SLO risk rises)
25
+ _DEGRADE = {"latency_injection", "network_loss"}
26
+
27
+
28
+ def chaos(g: Graph, fault: str, target: str,
29
+ allow_prod: bool = False) -> dict[str, Any]:
30
+ if fault not in FAULTS:
31
+ return {"refusal": "platform.chaos.unknown_fault",
32
+ "known": list(FAULTS)}
33
+ node = g.nodes.get(target)
34
+ if node is None:
35
+ return {"refusal": "platform.chaos.unknown_node", "node": target}
36
+ if node.attrs.get("env") == "production" and not allow_prod:
37
+ return {"refusal": "platform.chaos.production_default",
38
+ "node": target,
39
+ "unlock": "re-run with allow_prod=True in a controlled env"}
40
+
41
+ blast = blast_radius(g, target)
42
+ degraded = sorted(blast["nodes"])
43
+ impact = {"fault": fault, "target": target,
44
+ "environment": "simulation",
45
+ "mode": ("degraded" if fault in _DEGRADE else "removed"),
46
+ "impacted_total": blast["impacted_total"],
47
+ "impacted_by_class": blast["by_class"],
48
+ "degraded_nodes": degraded,
49
+ "unresolved": [
50
+ ("post-fault recovery behavior is not observable from "
51
+ "static artifacts")]}
52
+ return {"chaos": "platformforge.chaos/v1", **impact}
53
+
54
+
55
+ def run_scenario(path: str | Path,
56
+ allow_prod: bool = False) -> dict[str, Any]:
57
+ """chaos.yaml: {fault, target, facts} → simulation against fixture graph."""
58
+ d = Path(path)
59
+ spec = yaml.safe_load((d / "chaos.yaml").read_text())
60
+ facts_doc = yaml.safe_load((d / spec.get("facts", "facts.json"))
61
+ .read_text())
62
+ from platformforge.graph import GraphBuilder
63
+ g = GraphBuilder().from_facts(
64
+ facts_doc.get("facts", facts_doc if isinstance(facts_doc, list)
65
+ else [])).graph
66
+ out = chaos(g, spec["fault"], spec["target"], allow_prod=allow_prod)
67
+ expected = spec.get("expected_impacted")
68
+ if expected is not None and "degraded_nodes" in out:
69
+ out["expected_match"] = \
70
+ sorted(expected) == out["degraded_nodes"]
71
+ return out