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,287 @@
1
+ """Economy cache (§35–55) — multilayer, content-addressed, dependency-
2
+ invalidated, receipted.
3
+
4
+ Layers (§36): artifact → fact → graph → finding → context → analysis →
5
+ decision. Each layer declares which dependencies it binds to; a change in
6
+ a dependency invalidates only the layers that depend on it (§40–42):
7
+
8
+ rule catalog changes → findings/analysis/decision miss,
9
+ artifacts + facts still hit (§292 north star)
10
+ knowledge changes → recommendations/context miss; facts survive
11
+ engine changes → the corresponding layer misses
12
+
13
+ States (§44): hit | miss | stale | invalid | partial-reuse.
14
+ Never cached (§48): credentials, secret values, approval tokens — a
15
+ payload flagged sensitive is refused at `put`, and `decision` entries
16
+ require evidence_hash + policy_version + risk_profile (§53). Human
17
+ approval is operation-specific and has no cache layer (§54). Live
18
+ conclusions carry `observed_at` + `freshness_s`; stale observations never
19
+ count as a fresh hit (§55).
20
+
21
+ TTL exists but is a fallback — dependency invalidation is primary (§49).
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import hashlib
27
+ import json
28
+ import time
29
+ from dataclasses import asdict, dataclass, field
30
+ from pathlib import Path
31
+ from typing import Any
32
+
33
+ SCHEMA = "platformforge/cache-entry/v1"
34
+
35
+ LAYERS = ("artifact", "fact", "graph", "finding", "context",
36
+ "analysis", "decision")
37
+
38
+ # §38/§43 — the dependency keys each layer binds to. A dep not listed
39
+ # for a layer does not invalidate it (selective invalidation, §40).
40
+ LAYER_DEPS: dict[str, tuple[str, ...]] = {
41
+ "artifact": ("runtime_version",),
42
+ "fact": ("artifact_hash", "extractor_version", "runtime_version"),
43
+ "graph": ("artifact_hash", "runtime_version", "config_hash"),
44
+ "finding": ("artifact_hash", "rule_catalog_hash", "knowledge_hash",
45
+ "policy_hash"),
46
+ "context": ("artifact_hash", "rule_catalog_hash", "knowledge_hash"),
47
+ "analysis": ("rule_catalog_hash", "knowledge_hash", "engine_version",
48
+ "policy_hash"),
49
+ "decision": ("evidence_hash", "policy_version", "risk_profile"),
50
+ }
51
+
52
+ DECISION_STATES = ("hit", "miss", "stale", "invalid", "partial-reuse")
53
+
54
+ # §48 — payload markers that are never persisted into cache.
55
+ SENSITIVE_MARKERS = ("credential", "secret_value", "approval_token",
56
+ "private_key", "session_token")
57
+
58
+
59
+ def content_hash(payload: Any) -> str:
60
+ """§37 — content addressing: sha256 of the canonical serialization."""
61
+ body = json.dumps(payload, sort_keys=True, separators=(",", ":"),
62
+ default=str).encode()
63
+ return hashlib.sha256(body).hexdigest()
64
+
65
+
66
+ def cache_key(layer: str, scope: str, deps: dict[str, str]) -> str:
67
+ """§39 — key = sha256(layer + scope + the deps this layer binds)."""
68
+ bound = {k: deps.get(k, "") for k in LAYER_DEPS[layer]}
69
+ body = json.dumps({"layer": layer, "scope": scope, "deps": bound},
70
+ sort_keys=True).encode()
71
+ return hashlib.sha256(body).hexdigest()
72
+
73
+
74
+ @dataclass
75
+ class CacheDependency:
76
+ """§43 — declared dependency set for an entry."""
77
+ deps: dict[str, str] = field(default_factory=dict)
78
+
79
+ def changed(self, current: dict[str, str], layer: str) -> list[str]:
80
+ """Return the bound deps whose value changed vs `current`."""
81
+ return [k for k in LAYER_DEPS[layer]
82
+ if k in self.deps and self.deps[k] != current.get(k, "")]
83
+
84
+
85
+ @dataclass
86
+ class CacheDecision:
87
+ """§44 — the auditable outcome of a lookup."""
88
+ state: str # DECISION_STATES
89
+ layer: str
90
+ key: str
91
+ reason: str = ""
92
+ changed_deps: list[str] = field(default_factory=list)
93
+ entry_age_s: float | None = None
94
+ reusable_layers: list[str] = field(default_factory=list)
95
+ invalidated_layers: list[str] = field(default_factory=list)
96
+
97
+ def to_dict(self) -> dict[str, Any]:
98
+ return asdict(self)
99
+
100
+
101
+ @dataclass
102
+ class CacheEntry:
103
+ schema: str = SCHEMA
104
+ layer: str = ""
105
+ key: str = ""
106
+ scope: str = ""
107
+ payload: Any = None
108
+ payload_hash: str = ""
109
+ deps: dict[str, str] = field(default_factory=dict)
110
+ created_at: float = field(default_factory=time.time)
111
+ ttl_s: float | None = None # §49 — fallback only
112
+ observed_at: float | None = None # §55 — live conclusions
113
+ freshness_s: float | None = None
114
+ live: bool = False
115
+
116
+ def expired(self, now: float | None = None) -> bool:
117
+ now = now if now is not None else time.time()
118
+ if self.ttl_s is not None and now - self.created_at > self.ttl_s:
119
+ return True
120
+ if self.live:
121
+ if self.observed_at is None or self.freshness_s is None:
122
+ return True # live entry without freshness: stale
123
+ return now - self.observed_at > self.freshness_s
124
+ return False
125
+
126
+
127
+ class CacheStore:
128
+ """`.platformforge/cache/<layer>/<key>.json` + per-layer counters."""
129
+
130
+ def __init__(self, root: str | Path):
131
+ self.dir = Path(root) / ".platformforge" / "cache"
132
+ for layer in LAYERS:
133
+ (self.dir / layer).mkdir(parents=True, exist_ok=True)
134
+ self._stats_path = self.dir / "stats.json"
135
+
136
+ # -- stats ----------------------------------------------------------
137
+ def _stats(self) -> dict[str, int]:
138
+ if self._stats_path.exists():
139
+ return json.loads(self._stats_path.read_text())
140
+ return {"hits": 0, "misses": 0, "stale": 0, "invalid": 0,
141
+ "invalidations": 0, "puts": 0, "bytes_reused": 0,
142
+ "refused": 0}
143
+
144
+ def _bump(self, **kw: int) -> None:
145
+ s = self._stats()
146
+ for k, v in kw.items():
147
+ s[k] = s.get(k, 0) + v
148
+ self._stats_path.write_text(json.dumps(s, indent=1, sort_keys=True))
149
+
150
+ # -- write ------------------------------------------------------------
151
+ def put(self, layer: str, scope: str, payload: Any,
152
+ deps: dict[str, str], *, ttl_s: float | None = None,
153
+ live: bool = False, observed_at: float | None = None,
154
+ freshness_s: float | None = None,
155
+ sensitive: bool = False) -> CacheDecision:
156
+ """§48/§53/§54/§55 guards enforced at write time."""
157
+ if layer not in LAYERS:
158
+ raise ValueError(f"unknown cache layer {layer!r}")
159
+ if sensitive:
160
+ self._bump(refused=1)
161
+ return CacheDecision(
162
+ "invalid", layer, "", reason=(
163
+ "refused: sensitive payload — credentials, secret "
164
+ "values and approval tokens are never cached (§48/§54)"))
165
+ if layer == "decision":
166
+ missing = [k for k in LAYER_DEPS["decision"]
167
+ if k not in deps]
168
+ if missing:
169
+ self._bump(refused=1)
170
+ return CacheDecision(
171
+ "invalid", layer, "", reason=(
172
+ f"decision cache requires {missing} "
173
+ "(§53: evidence_hash+policy_version+risk_profile)"))
174
+ key = cache_key(layer, scope, deps)
175
+ ent = CacheEntry(layer=layer, key=key, scope=scope,
176
+ payload=payload, payload_hash=content_hash(payload),
177
+ deps=dict(deps), ttl_s=ttl_s, live=live,
178
+ observed_at=observed_at, freshness_s=freshness_s)
179
+ (self.dir / layer / f"{key}.json").write_text(
180
+ json.dumps(asdict(ent), sort_keys=True, default=str))
181
+ self._bump(puts=1)
182
+ return CacheDecision("hit", layer, key, "stored")
183
+
184
+ # -- read -------------------------------------------------------------
185
+ def _load(self, layer: str, key: str) -> CacheEntry | None:
186
+ p = self.dir / layer / f"{key}.json"
187
+ if not p.exists():
188
+ return None
189
+ d = json.loads(p.read_text())
190
+ return CacheEntry(**{k: v for k, v in d.items()
191
+ if k in CacheEntry.__dataclass_fields__})
192
+
193
+ def get(self, layer: str, scope: str, current_deps: dict[str, str],
194
+ *, now: float | None = None
195
+ ) -> tuple[CacheDecision, Any | None]:
196
+ """Lookup: dependency-diff first, freshness second (§44, §55)."""
197
+ # the stored key encodes the OLD deps — recompute from stored deps
198
+ # is impossible, so lookup scans by scope+layer prefix: keys are
199
+ # content-derived, so we resolve the entry whose deps-match.
200
+ # For a 1:1 scope→key mapping we store under a scope index.
201
+ for cand in (self.dir / layer).glob("*.json"):
202
+ d = json.loads(cand.read_text())
203
+ if d.get("scope") != scope:
204
+ continue
205
+ ent = CacheEntry(**{k: v for k, v in d.items()
206
+ if k in CacheEntry.__dataclass_fields__})
207
+ dep = CacheDependency(ent.deps)
208
+ changed = dep.changed(current_deps, layer)
209
+ if changed:
210
+ self._bump(invalid=1)
211
+ return (CacheDecision(
212
+ "invalid", layer, ent.key,
213
+ f"deps changed: {changed}", changed), None)
214
+ if ent.expired(now):
215
+ self._bump(stale=1)
216
+ return (CacheDecision(
217
+ "stale", layer, ent.key,
218
+ "freshness/ttl expired", entry_age_s=(
219
+ (now or time.time()) - ent.created_at)), None)
220
+ self._bump(hits=1, bytes_reused=len(
221
+ json.dumps(ent.payload, default=str)))
222
+ return (CacheDecision("hit", layer, ent.key, "deps match"),
223
+ ent.payload)
224
+ self._bump(misses=1)
225
+ return (CacheDecision("miss", layer,
226
+ cache_key(layer, scope, current_deps),
227
+ "no entry for scope"), None)
228
+
229
+ def invalidate_dep(self, dep_key: str, new_value: str
230
+ ) -> dict[str, Any]:
231
+ """§40–42 — selective invalidation: drop every entry whose bound
232
+ deps include dep_key with a different value; keep the rest."""
233
+ removed: list[str] = []
234
+ kept: list[str] = []
235
+ for layer in LAYERS:
236
+ if dep_key not in LAYER_DEPS[layer]:
237
+ kept.append(layer)
238
+ continue
239
+ for cand in (self.dir / layer).glob("*.json"):
240
+ d = json.loads(cand.read_text())
241
+ if d.get("deps", {}).get(dep_key) not in (None, new_value):
242
+ cand.unlink()
243
+ removed.append(f"{layer}/{d.get('key', cand.stem)}")
244
+ self._bump(invalidations=len(removed))
245
+ return {"dep": dep_key, "removed": removed,
246
+ "unaffected_layers": [l for l in kept]}
247
+
248
+ # -- maintenance --------------------------------------------------------
249
+ def stats(self) -> dict[str, Any]:
250
+ """§51 — hits/misses/bytes reused/entries/invalidations."""
251
+ s = self._stats()
252
+ entries = sum(1 for layer in LAYERS
253
+ for _ in (self.dir / layer).glob("*.json"))
254
+ return {**s, "entries": entries,
255
+ "layers": {l: sum(1 for _ in (self.dir / l).glob("*.json"))
256
+ for l in LAYERS}}
257
+
258
+ def gc(self, *, now: float | None = None) -> dict[str, Any]:
259
+ """§50 — remove expired entries; dependency-invalid entries are
260
+ already removed by invalidate_dep."""
261
+ now = now if now is not None else time.time()
262
+ removed = []
263
+ for layer in LAYERS:
264
+ for cand in (self.dir / layer).glob("*.json"):
265
+ d = json.loads(cand.read_text())
266
+ ent = CacheEntry(**{k: v for k, v in d.items()
267
+ if k in CacheEntry.__dataclass_fields__})
268
+ if ent.expired(now):
269
+ cand.unlink()
270
+ removed.append(f"{layer}/{d.get('key', cand.stem)}")
271
+ return {"gc_removed": removed, "remaining": self.stats()["entries"]}
272
+
273
+ def reuse_plan(self, current_deps: dict[str, str], scopes: dict[str,
274
+ str], *, now: float | None = None) -> CacheDecision:
275
+ """§44 partial-reuse — per-layer lookup over a pipeline; returns
276
+ which layers still hit vs miss (§292 north star shape)."""
277
+ hit, missed = [], []
278
+ for layer, scope in scopes.items():
279
+ if layer not in LAYERS:
280
+ continue
281
+ dec, _ = self.get(layer, scope, current_deps, now=now)
282
+ (hit if dec.state == "hit" else missed).append(layer)
283
+ state = ("hit" if not missed else
284
+ "partial-reuse" if hit else "miss")
285
+ return CacheDecision(state, "pipeline", "",
286
+ f"reusable={hit} recompute={missed}",
287
+ reusable_layers=hit, invalidated_layers=missed)
@@ -0,0 +1,112 @@
1
+ """EconomyCheckpoint (§76–80) — run state persisted so a stopped run
2
+ resumes with its spend intact.
3
+
4
+ Invariants: resume never resets spend (§78/§290 north star); resume
5
+ revalidates artifact hashes, knowledge freshness, observation freshness,
6
+ policy version and operation state (§79); a profile cannot downgrade on
7
+ resume — `deep` never becomes `cheap` because the run paused (§80).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import time
14
+ from dataclasses import asdict, dataclass, field
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ SCHEMA = "platformforge/checkpoint/v1"
19
+
20
+ PROFILE_ORDER = {"offline": 0, "economy": 1, "balanced": 2,
21
+ "deep": 3, "strict": 4}
22
+
23
+
24
+ @dataclass
25
+ class EconomyCheckpoint:
26
+ """§77 — canonical fields."""
27
+ schema: str = SCHEMA
28
+ run_id: str = ""
29
+ spent_budget: dict[str, float] = field(default_factory=dict)
30
+ remaining_budget: dict[str, float] = field(default_factory=dict)
31
+ context_refs: list[str] = field(default_factory=list)
32
+ cache_refs: list[str] = field(default_factory=list)
33
+ agent_state: dict[str, Any] = field(default_factory=dict)
34
+ routing_profile: str = "balanced"
35
+ risk_profile: str = "low"
36
+ tool_spend: dict[str, float] = field(default_factory=dict)
37
+ provider_spend: dict[str, float] = field(default_factory=dict)
38
+ deps: dict[str, str] = field(default_factory=dict)
39
+ # artifact_hash / knowledge_hash / policy_version — revalidated §79
40
+ created_at: float = field(default_factory=time.time)
41
+
42
+ def to_dict(self) -> dict[str, Any]:
43
+ return asdict(self)
44
+
45
+
46
+ class CheckpointStore:
47
+ """`.platformforge/checkpoints/<run_id>.json`."""
48
+
49
+ def __init__(self, root: str | Path):
50
+ self.dir = Path(root) / ".platformforge" / "checkpoints"
51
+ self.dir.mkdir(parents=True, exist_ok=True)
52
+
53
+ def save(self, cp: EconomyCheckpoint) -> str:
54
+ path = self.dir / f"{cp.run_id}.json"
55
+ path.write_text(json.dumps(cp.to_dict(), sort_keys=True,
56
+ default=str, indent=1))
57
+ return str(path)
58
+
59
+ def load(self, run_id: str) -> EconomyCheckpoint | None:
60
+ p = self.dir / f"{run_id}.json"
61
+ if not p.exists():
62
+ return None
63
+ d = json.loads(p.read_text())
64
+ return EconomyCheckpoint(**{
65
+ k: v for k, v in d.items()
66
+ if k in EconomyCheckpoint.__dataclass_fields__})
67
+
68
+ def list(self) -> list[str]:
69
+ return sorted(p.stem for p in self.dir.glob("*.json"))
70
+
71
+ def resume(self, run_id: str, current_deps: dict[str, str],
72
+ requested_profile: str = ""
73
+ ) -> dict[str, Any]:
74
+ """§78–80 — resume with spend intact; revalidate the world;
75
+ refuse profile downgrade."""
76
+ cp = self.load(run_id)
77
+ if cp is None:
78
+ return {"refusal": "PF-CHECKPOINT-MISSING",
79
+ "reason": f"no checkpoint for {run_id}",
80
+ "unlock": "start a fresh run — spend unknown"}
81
+
82
+ # §79 — revalidation: every dep that drifted is reported, and a
83
+ # stale dep means the resume must re-verify, not silently reuse.
84
+ stale = {k: {"was": v, "now": current_deps.get(k, "")}
85
+ for k, v in cp.deps.items()
86
+ if current_deps.get(k, "") != v}
87
+ needs_revalidation = bool(stale)
88
+
89
+ # §80 — profile cannot downgrade on resume.
90
+ eff_profile = requested_profile or cp.routing_profile
91
+ if PROFILE_ORDER.get(eff_profile, -1) < \
92
+ PROFILE_ORDER.get(cp.routing_profile, 0):
93
+ return {"refusal": "PF-CHECKPOINT-DOWNGRADE",
94
+ "reason": f"cannot resume {cp.routing_profile!r} run "
95
+ f"as {eff_profile!r} — profile cannot "
96
+ "downgrade on resume (§80)",
97
+ "unlock": "resume at the stored profile or higher"}
98
+
99
+ return {"run_id": run_id, "resumed": True,
100
+ "profile": eff_profile,
101
+ "spent_budget": cp.spent_budget, # §78 — carries over
102
+ "remaining_budget": cp.remaining_budget,
103
+ "context_refs": cp.context_refs,
104
+ "cache_refs": cp.cache_refs,
105
+ "tool_spend": cp.tool_spend,
106
+ "provider_spend": cp.provider_spend,
107
+ "risk_profile": cp.risk_profile,
108
+ "needs_revalidation": needs_revalidation,
109
+ "stale_deps": stale,
110
+ "note": ("spend preserved; re-verify sections whose deps "
111
+ "changed" if needs_revalidation else
112
+ "spend preserved; all deps fresh")}
@@ -0,0 +1,142 @@
1
+ """Live collection economy v2 (§197–207) + fleet context funnel helpers
2
+ (§192–196).
3
+
4
+ A collection starts broad-and-cheap (inventory) and only goes deep on the
5
+ parts the graph/risk point at (§199). Every live call passes the evidence
6
+ gate first (§206–207): if local evidence can answer, the provider call
7
+ is refused — runtime truth is the only justification for spending a call.
8
+ Provider-call ROI is measured in facts gained and coverage gained per
9
+ call (§200), not money — "quality per call" claims need data (§201).
10
+ Cached observations are used while fresh (§202–203); budget exhaustion
11
+ yields partial coverage, honestly reported (§204).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from dataclasses import asdict, dataclass, field
17
+ from typing import Any
18
+
19
+ from platformforge.economy.cache import CacheStore
20
+
21
+ SCHEMA = "platformforge/collection-economy-plan/v1"
22
+
23
+
24
+ @dataclass
25
+ class CollectionEconomyPlan:
26
+ """§198 — broad → targeted, with declared caps."""
27
+ schema: str = SCHEMA
28
+ inventory_first: bool = True # §199
29
+ inventory_budget: dict[str, int] = field(
30
+ default_factory=lambda: {"api_calls": 20, "objects": 10000})
31
+ targeted_budget: dict[str, int] = field(
32
+ default_factory=lambda: {"api_calls": 80, "objects": 5000})
33
+ target_selectors: dict[str, Any] = field(default_factory=dict)
34
+ # e.g. {"graph_nodes": [...], "risk_min": "high"}
35
+ freshness_s: int = 3600 # §203 cached observations
36
+ evidence_gate: bool = True # §206 — always on by default
37
+
38
+ def to_dict(self) -> dict[str, Any]:
39
+ return asdict(self)
40
+
41
+
42
+ def evidence_gate(question_kind: str, local_evidence: dict[str, Any],
43
+ *, runtime_truth_needed: bool = False
44
+ ) -> dict[str, Any]:
45
+ """§206–207 — before any live call: can local evidence answer?
46
+
47
+ A live call is allowed only when runtime truth is needed AND local
48
+ evidence cannot answer. Otherwise the call is refused and the caller
49
+ is pointed at the local verbs."""
50
+ answers = local_evidence.get("answers", [])
51
+ coverage = local_evidence.get("coverage", 0.0)
52
+ if runtime_truth_needed and coverage < 0.5:
53
+ return {"decision": "live_call_justified",
54
+ "reason": "runtime truth needed and local coverage "
55
+ f"{coverage:.0%} is insufficient"}
56
+ if answers or coverage >= 0.5:
57
+ return {"decision": "refuse_live_call",
58
+ "reason": "local evidence can answer "
59
+ f"(coverage {coverage:.0%})",
60
+ "code": "PF-ECONOMY-LIVE-UNNECESSARY",
61
+ "use": "local artifacts/graph/facts instead"}
62
+ return {"decision": "live_call_justified",
63
+ "reason": "no local evidence covers the question"}
64
+
65
+
66
+ def provider_call_roi(calls: int, facts_gained: int,
67
+ coverage_gained: float) -> dict[str, Any]:
68
+ """§200–201 — measured ROI in facts/coverage per call. 'Quality per
69
+ call' conclusions require data; with no calls there is nothing."""
70
+ if calls == 0:
71
+ return {"state": "unresolved",
72
+ "reason": "no calls — ROI undefined, not zero"}
73
+ return {"calls": calls, "facts_gained": facts_gained,
74
+ "coverage_gained": round(coverage_gained, 4),
75
+ "facts_per_call": round(facts_gained / calls, 3),
76
+ "coverage_per_call": round(coverage_gained / calls, 5),
77
+ "basis": "measured"}
78
+
79
+
80
+ def coverage_on_exhaustion(collected: int, total: int
81
+ ) -> dict[str, Any]:
82
+ """§204 — budget exhausted → coverage is partial and says so."""
83
+ frac = collected / total if total else 0.0
84
+ return {"coverage": round(frac, 4), "state": "partial",
85
+ "code": "PF-ECONOMY-BUDGET-EXHAUSTED",
86
+ "reason": f"collection budget exhausted at {collected}/"
87
+ f"{total} objects — results are partial"}
88
+
89
+
90
+ # --- fleet (§192–196) ----------------------------------------------------
91
+
92
+ def fleet_snapshot_hash(snapshot: dict[str, Any]) -> str:
93
+ import hashlib
94
+ import json
95
+ return hashlib.sha256(json.dumps(snapshot, sort_keys=True,
96
+ default=str).encode()).hexdigest()
97
+
98
+
99
+ def _fleet_key(snapshot: dict[str, Any], query: str) -> str:
100
+ # the snapshot hash is part of the KEY (not just deps): a new
101
+ # snapshot is a different question — a miss, never a stale hit (§195)
102
+ return f"fleet:{query}:{fleet_snapshot_hash(snapshot)[:16]}"
103
+
104
+
105
+ def fleet_query_cache_get(store: CacheStore, snapshot: dict[str, Any],
106
+ query: str,
107
+ deps: dict[str, Any] | None = None
108
+ ) -> dict[str, Any]:
109
+ """§195 — cache fleet aggregates/graph queries/analytics by snapshot
110
+ hash; a new snapshot hash is a miss, never a stale hit."""
111
+ dec, payload = store.get("analysis", _fleet_key(snapshot, query),
112
+ deps or {})
113
+ return {"decision": dec.to_dict(),
114
+ "payload": payload}
115
+
116
+
117
+ def fleet_query_cache_put(store: CacheStore, snapshot: dict[str, Any],
118
+ query: str, result: Any,
119
+ deps: dict[str, Any] | None = None
120
+ ) -> dict[str, Any]:
121
+ dec = store.put("analysis", _fleet_key(snapshot, query), result,
122
+ deps or {})
123
+ return {"decision": dec.to_dict()}
124
+
125
+
126
+ def fleet_delta(snap_a: dict[str, Any], snap_b: dict[str, Any]
127
+ ) -> dict[str, Any]:
128
+ """§196 — snapshot delta so repeated fleet questions don't recompute.
129
+
130
+ Members are keyed `kind:id`; the delta lists added/removed/changed
131
+ references (member snapshots are hash references, not payloads)."""
132
+ a = snap_a.get("members", snap_a.get("observations", {})) or {}
133
+ b = snap_b.get("members", snap_b.get("observations", {})) or {}
134
+ ka, kb = set(a), set(b)
135
+ changed = sorted(k for k in ka & kb if a[k] != b[k])
136
+ return {"snapshot_a": fleet_snapshot_hash(snap_a)[:12],
137
+ "snapshot_b": fleet_snapshot_hash(snap_b)[:12],
138
+ "added": sorted(kb - ka), "removed": sorted(ka - kb),
139
+ "changed": changed,
140
+ "unchanged": len(ka & kb) - len(changed),
141
+ "note": "delta-first fleet reads — unchanged members keep "
142
+ "cached analysis"}