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,21 @@
1
+ """Token estimation — honest heuristic.
2
+
3
+ ~4 chars/token is the documented approximation for code+prose in English.
4
+ Estimates are ALWAYS labeled `estimated`, never `observed` — provider token
5
+ counts are only real when a host transcript supplies them.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ CHARS_PER_TOKEN = 4.0
11
+
12
+
13
+ def estimate_tokens(text: str | bytes) -> int:
14
+ if isinstance(text, bytes):
15
+ text = text.decode("utf-8", errors="replace")
16
+ return max(1, round(len(text) / CHARS_PER_TOKEN)) if text else 0
17
+
18
+
19
+ def estimate_obj(obj) -> int:
20
+ import json
21
+ return estimate_tokens(json.dumps(obj, default=str))
@@ -0,0 +1,172 @@
1
+ """Local search index — SQLite FTS5, incremental by content hash.
2
+
3
+ Indexes file content + extracted symbols. Unchanged files (same sha256) are
4
+ never re-indexed. No embeddings, no external service.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ import sqlite3
11
+ from collections.abc import Iterable
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from platformforge.core.hashing import sha256_file, sha256_text
16
+ from platformforge.core.redaction import redact_text_report, redaction_receipt
17
+
18
+ IGNORE_DIRS = {".git", ".venv", "node_modules", ".platformforge", "vendor",
19
+ "__pycache__", "dist", "build", ".terraform", ".idea", ".vscode"}
20
+ TEXT_EXT = {".py", ".tf", ".tfvars", ".yaml", ".yml", ".json", ".md", ".go",
21
+ ".ts", ".js", ".java", ".rb", ".rs", ".sh", ".toml", ".hcl",
22
+ ".sql", ".j2", ".tpl", ".rego", ".cel", ".xml", ".gradle",
23
+ ".properties", ".txt", ".cfg", ".ini", ".env.example"}
24
+ MAX_FILE_BYTES = 2_000_000
25
+
26
+ SYMBOL_PATTERNS = {
27
+ ".py": [re.compile(r"^\s*(?:class|def|async def)\s+([A-Za-z_][\w]*)", re.MULTILINE)],
28
+ ".go": [re.compile(r"^\s*func\s+(?:\([^)]*\)\s*)?([A-Za-z_][\w]*)", re.MULTILINE),
29
+ re.compile(r"^\s*type\s+([A-Za-z_][\w]*)", re.MULTILINE)],
30
+ ".java": [re.compile(r"\b(?:class|interface|enum|record)\s+([A-Za-z_][\w]*)")],
31
+ ".tf": [re.compile(r'\bresource\s+"([^"]+)"\s+"([^"]+)"'),
32
+ re.compile(r'\bmodule\s+"([^"]+)"'),
33
+ re.compile(r'\b(?:variable|output|data|provider)\s+"?([A-Za-z_][\w]*)"?')],
34
+ ".hcl": [re.compile(r'\bresource\s+"([^"]+)"\s+"([^"]+)"')],
35
+ ".yaml": [re.compile(r"^\s*name:\s*([A-Za-z0-9_.\-/]+)", re.MULTILINE)],
36
+ ".yml": [re.compile(r"^\s*name:\s*([A-Za-z0-9_.\-/]+)", re.MULTILINE)],
37
+ }
38
+
39
+ SCHEMA = """
40
+ CREATE TABLE IF NOT EXISTS files(
41
+ path TEXT PRIMARY KEY, sha256 TEXT, kind TEXT, bytes INTEGER, symbols TEXT);
42
+ CREATE TABLE IF NOT EXISTS facts_idx(
43
+ fact_id TEXT PRIMARY KEY, kind TEXT, location TEXT, doc TEXT);
44
+ CREATE VIRTUAL TABLE IF NOT EXISTS content USING fts5(
45
+ path UNINDEXED, body, tokenize='porter unicode61');
46
+ """
47
+
48
+
49
+ class SearchIndex:
50
+ def __init__(self, db_path: str | Path):
51
+ self.db_path = Path(db_path)
52
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
53
+ self.db = sqlite3.connect(str(self.db_path))
54
+ self.db.executescript(SCHEMA)
55
+ self.db.commit()
56
+
57
+ def close(self) -> None:
58
+ self.db.close()
59
+
60
+ def __enter__(self) -> SearchIndex: # noqa: PYI034 - typing.Self is 3.11+; no typing_extensions dep
61
+ return self
62
+
63
+ def __exit__(self, *exc: object) -> None:
64
+ self.close()
65
+
66
+ # ── indexing ──────────────────────────────────────────────
67
+ def _iter_files(self, root: Path) -> Iterable[Path]:
68
+ for p in sorted(root.rglob("*")):
69
+ if not p.is_file() or any(part in IGNORE_DIRS for part in p.parts):
70
+ continue
71
+ if p.suffix in TEXT_EXT and p.stat().st_size <= MAX_FILE_BYTES:
72
+ yield p
73
+
74
+ def index_workspace(self, root: str | Path) -> dict[str, int]:
75
+ """Incremental: only changed/new files are parsed and re-indexed."""
76
+ root = Path(root)
77
+ stats = {"indexed": 0, "reused": 0, "removed": 0}
78
+ seen = set()
79
+ for p in self._iter_files(root):
80
+ rel = str(p.relative_to(root))
81
+ seen.add(rel)
82
+ sha = sha256_file(p)
83
+ row = self.db.execute(
84
+ "SELECT sha256 FROM files WHERE path=?", (rel,)).fetchone()
85
+ if row and row[0] == sha:
86
+ stats["reused"] += 1
87
+ continue
88
+ body = p.read_text(errors="replace")
89
+ # §17 — secrets are never stored raw in the context engine;
90
+ # the index keeps redacted text + a receipt of what was masked.
91
+ body, redactions = redact_text_report(body)
92
+ if redactions:
93
+ stats.setdefault("redactions", {})[rel] = redaction_receipt(
94
+ rel, redactions)["redactions"]
95
+ symbols = sorted({m for pat in SYMBOL_PATTERNS.get(p.suffix, [])
96
+ for m in pat.findall(body)
97
+ for m in (m if isinstance(m, tuple) else (m,))})
98
+ self.db.execute(
99
+ "INSERT OR REPLACE INTO files VALUES(?,?,?,?,?)",
100
+ (rel, sha, p.suffix.lstrip("."), p.stat().st_size,
101
+ " ".join(symbols)))
102
+ self.db.execute("DELETE FROM content WHERE path=?", (rel,))
103
+ self.db.execute("INSERT INTO content(path, body) VALUES(?,?)",
104
+ (rel, body))
105
+ stats["indexed"] += 1
106
+ stale = {r[0] for r in self.db.execute(
107
+ "SELECT path FROM files")} - seen
108
+ for rel in stale:
109
+ self.db.execute("DELETE FROM files WHERE path=?", (rel,))
110
+ self.db.execute("DELETE FROM content WHERE path=?", (rel,))
111
+ stats["removed"] += 1
112
+ self.db.commit()
113
+ return stats
114
+
115
+ def index_fact(self, fact_id: str, kind: str, location: str, doc: str) -> None:
116
+ self.db.execute("INSERT OR REPLACE INTO facts_idx VALUES(?,?,?,?)",
117
+ (fact_id, kind, location, doc))
118
+ self.db.commit()
119
+
120
+ # ── queries ───────────────────────────────────────────────
121
+ def search(self, query: str, limit: int = 20) -> list[dict[str, Any]]:
122
+ """FTS5 literal/phrase search."""
123
+ try:
124
+ rows = self.db.execute(
125
+ "SELECT path, snippet(content,1,'«','»','…',12) FROM content "
126
+ "WHERE content MATCH ? ORDER BY rank LIMIT ?",
127
+ (query, limit)).fetchall()
128
+ except sqlite3.OperationalError:
129
+ return []
130
+ return [{"path": p, "snippet": s} for p, s in rows]
131
+
132
+ def search_regex(self, pattern: str, limit: int = 50) -> list[dict[str, Any]]:
133
+ rx = re.compile(pattern)
134
+ hits = []
135
+ for path, in self.db.execute("SELECT path FROM files"):
136
+ body_rows = self.db.execute(
137
+ "SELECT body FROM content WHERE path=?", (path,)).fetchall()
138
+ if body_rows and rx.search(body_rows[0][0] or ""):
139
+ hits.append({"path": path})
140
+ if len(hits) >= limit:
141
+ break
142
+ return hits
143
+
144
+ def symbol(self, name: str) -> list[dict[str, Any]]:
145
+ rows = self.db.execute(
146
+ "SELECT path, symbols FROM files WHERE symbols LIKE ?",
147
+ (f"%{name}%",)).fetchall()
148
+ return [{"path": p, "symbols": s.split() if s else []} for p, s in rows]
149
+
150
+ def by_path(self, glob: str) -> list[str]:
151
+ like = glob.replace("*", "%").replace("?", "_")
152
+ return [r[0] for r in self.db.execute(
153
+ "SELECT path FROM files WHERE path LIKE ?", (like,))]
154
+
155
+ def by_kind(self, kind: str) -> list[str]:
156
+ return [r[0] for r in self.db.execute(
157
+ "SELECT path FROM files WHERE kind=?", (kind,))]
158
+
159
+ def read(self, path: str) -> str | None:
160
+ row = self.db.execute(
161
+ "SELECT body FROM content WHERE path=?", (path,)).fetchone()
162
+ return row[0] if row else None
163
+
164
+ def stats(self) -> dict[str, int]:
165
+ files = self.db.execute("SELECT COUNT(*), COALESCE(SUM(bytes),0) "
166
+ "FROM files").fetchone()
167
+ return {"files": files[0], "bytes": files[1]}
168
+
169
+ def fingerprint(self) -> str:
170
+ return sha256_text(",".join(
171
+ f"{r[0]}:{r[1]}" for r in self.db.execute(
172
+ "SELECT path, sha256 FROM files ORDER BY path")))
@@ -0,0 +1,121 @@
1
+ """Token ledger — append-only JSONL accounting per run.
2
+
3
+ Records context requested/delivered/reused/skipped/compressed, provider tokens
4
+ when observed, estimates when estimated, cache hits/misses. Three honesty
5
+ states: observed | estimated | unknown.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import time
12
+ from dataclasses import asdict, dataclass, field
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+
17
+ @dataclass
18
+ class LedgerEntry:
19
+ """§29 — v2 fields: requested/candidate/selected/delivered/reused/
20
+ cached/compressed/skipped + essential/optional split. Provider-side
21
+ tokens stay None unless a host transcript observes them."""
22
+ operation: str
23
+ context_requested: int = 0 # estimated tokens requested
24
+ context_candidate: int = 0 # ranked candidates before budget
25
+ context_selected: int = 0 # selected after ranking
26
+ context_delivered: int = 0
27
+ context_reused: int = 0 # served from cache/content-address
28
+ context_cached: int = 0
29
+ context_skipped: int = 0
30
+ context_compressed: int = 0
31
+ essential_tokens: int = 0 # evidence that must not be dropped
32
+ optional_tokens: int = 0
33
+ input_tokens: int | None = None # observed (provider transcript)
34
+ output_tokens: int | None = None
35
+ reasoning_tokens: int | None = None
36
+ cost_usd: float | None = None
37
+ input_tokens_est: int | None = None # estimated
38
+ output_tokens_est: int | None = None
39
+ cache_hits: int = 0
40
+ cache_misses: int = 0
41
+ # §42 — deterministic vs model split. Deterministic bytes are the local
42
+ # working set (never billed as model spend); model_context_* counts only
43
+ # the payload a model/agent receives. Provider counts stay None when no
44
+ # transcript observed them (`unknown`).
45
+ deterministic_input_bytes: int = 0
46
+ deterministic_fact_count: int = 0
47
+ model_context_bytes: int = 0
48
+ model_context_tokens: int | None = None
49
+ model_output_tokens: int | None = None
50
+ cache_tokens: int = 0
51
+ latency_ms: float = 0.0
52
+ token_basis: str = "unknown" # closed vocab: observed|estimated|unknown
53
+ # an "observed" row must bind to the transcript that observed it —
54
+ # otherwise the count is unfalsifiable
55
+ transcript_ref: str | None = None
56
+ ts: float = field(default_factory=time.time)
57
+ extra: dict[str, Any] = field(default_factory=dict)
58
+
59
+ _TOKEN_BASES = ("observed", "estimated", "unknown")
60
+
61
+ def __post_init__(self) -> None:
62
+ if self.token_basis not in self._TOKEN_BASES:
63
+ raise ValueError(
64
+ f"token_basis must be one of {self._TOKEN_BASES}, "
65
+ f"got {self.token_basis!r}")
66
+ observed = any(v is not None for v in
67
+ (self.input_tokens, self.output_tokens,
68
+ self.reasoning_tokens))
69
+ if observed and self.token_basis != "observed":
70
+ raise ValueError(
71
+ "provider token counts require token_basis='observed'")
72
+ if self.token_basis == "observed" and observed \
73
+ and not self.transcript_ref:
74
+ raise ValueError(
75
+ "observed token counts require transcript_ref — "
76
+ "unbindable numbers are fabricated")
77
+
78
+ def to_dict(self) -> dict[str, Any]:
79
+ return asdict(self)
80
+
81
+
82
+ class TokenLedger:
83
+ def __init__(self, root: str | Path):
84
+ self.dir = Path(root) / ".platformforge" / "ledger"
85
+ self.dir.mkdir(parents=True, exist_ok=True)
86
+ self.path = self.dir / "tokens.jsonl"
87
+
88
+ def record(self, entry: LedgerEntry) -> None:
89
+ with open(self.path, "a") as fh:
90
+ fh.write(json.dumps(entry.to_dict(), default=str) + "\n")
91
+
92
+ def entries(self) -> list[dict[str, Any]]:
93
+ if not self.path.exists():
94
+ return []
95
+ return [json.loads(l) for l in self.path.read_text().splitlines() if l.strip()]
96
+
97
+ def report(self) -> dict[str, Any]:
98
+ ents = self.entries()
99
+ tot = lambda k: sum(e.get(k) or 0 for e in ents)
100
+ hits, misses = tot("cache_hits"), tot("cache_misses")
101
+ return {
102
+ "operations": len(ents),
103
+ "context_requested": tot("context_requested"),
104
+ "context_delivered": tot("context_delivered"),
105
+ "context_reused": tot("context_reused"),
106
+ "context_skipped": tot("context_skipped"),
107
+ "context_compressed": tot("context_compressed"),
108
+ "cache_hit_ratio": (hits / (hits + misses)) if (hits + misses) else None,
109
+ "input_tokens_observed": tot("input_tokens"),
110
+ "output_tokens_observed": tot("output_tokens"),
111
+ "input_tokens_estimated": tot("input_tokens_est"),
112
+ "output_tokens_estimated": tot("output_tokens_est"),
113
+ "deterministic_input_bytes": tot("deterministic_input_bytes"),
114
+ "deterministic_fact_count": tot("deterministic_fact_count"),
115
+ "model_context_bytes": tot("model_context_bytes"),
116
+ "cache_tokens": tot("cache_tokens"),
117
+ "basis_counts": {
118
+ b: sum(1 for e in ents if e.get("token_basis") == b)
119
+ for b in ("observed", "estimated", "unknown")
120
+ },
121
+ }
@@ -0,0 +1,285 @@
1
+ """Context packs v2 — graph-aware, explainable, budget-deciding (§22–§28).
2
+
3
+ Pipeline (§23): task → intent terms → candidate files → changed artifacts →
4
+ graph seeds/neighborhood → rules → facts → historical findings → risk
5
+ modifiers → rank → dedup → budget → pack.
6
+
7
+ Every item carries `score` + `reasons` (§25). Items are classified
8
+ essential / high-value / optional / background (§27). If essential alone
9
+ exceeds the budget the pack returns `budget_decision: refuse` — essential
10
+ evidence is never dropped silently.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import re
17
+ from typing import Any
18
+
19
+ from platformforge.tokensave.budget import Budget
20
+ from platformforge.tokensave.estimate import estimate_tokens
21
+ from platformforge.tokensave.index import SearchIndex
22
+ from platformforge.tokensave.ledger import LedgerEntry, TokenLedger
23
+
24
+ WORD_RE = re.compile(r"[A-Za-z0-9_.\-/]{3,}")
25
+
26
+ ESSENTIAL = "essential" # findings, facts, rules, unresolved — never dropped
27
+ HIGH_VALUE = "high-value" # changed files, graph distance ≤ 1, risk-flagged
28
+ OPTIONAL = "optional"
29
+ BACKGROUND = "background"
30
+
31
+ # §24 — explainable scoring weights. Each contributes a named reason.
32
+ W_CHANGED = 10.0
33
+ W_TASK_TERM = 2.0
34
+ W_SYMBOL = 3.0
35
+ W_PATH_TERM = 4.0
36
+ W_GRAPH_DIST = {1: 6.0, 2: 3.0}
37
+ W_RISK = {"critical": 8.0, "high": 5.0, "medium": 2.0}
38
+ W_RULE = 4.0
39
+
40
+
41
+ class ContextPackBuilder:
42
+ def __init__(self, index: SearchIndex, ledger: TokenLedger | None = None):
43
+ self.index = index
44
+ self.ledger = ledger
45
+
46
+ def _task_terms(self, task: str) -> list[str]:
47
+ return [t.lower() for t in WORD_RE.findall(task)]
48
+
49
+ def _rank_files(self, terms: list[str], changed: list[str],
50
+ graph_nodes: list[str] | None,
51
+ rule_ids: list[str] | None,
52
+ risk: str | None,
53
+ limit: int = 50) -> list[dict[str, Any]]:
54
+ """Ranked candidates with explainable reasons (§24–§25)."""
55
+ scores: dict[str, float] = {}
56
+ reasons: dict[str, list[str]] = {}
57
+
58
+ def bump(path: str, w: float, reason: str) -> None:
59
+ scores[path] = scores.get(path, 0.0) + w
60
+ reasons.setdefault(path, [])
61
+ if reason not in reasons[path]:
62
+ reasons[path].append(reason)
63
+
64
+ for path in changed:
65
+ bump(path, W_CHANGED, "changed-file")
66
+ for term in terms:
67
+ for hit in self.index.search(f'"{term}"', limit=20):
68
+ bump(hit["path"], W_TASK_TERM, f"task-term:{term}")
69
+ for hit in self.index.symbol(term):
70
+ bump(hit["path"], W_SYMBOL, f"symbol:{term}")
71
+ for path in self.index.by_path(f"*{term}*"):
72
+ bump(path, W_PATH_TERM, f"path:{term}")
73
+ for node in graph_nodes or []:
74
+ # graph seeds/neighbors: node label terms hit path ranking
75
+ depth = 1 if ":" not in node else int(node.split(":", 1)[1])
76
+ w = W_GRAPH_DIST.get(depth, 1.0)
77
+ label = node.split("/")[-1].split(":")[0]
78
+ for hit in self.index.search(f'"{label}"', limit=10):
79
+ bump(hit["path"], w, f"graph-distance:{depth}")
80
+ for path in self.index.by_path(f"*{label}*"):
81
+ bump(path, w, f"graph-distance:{depth}")
82
+ for rid in rule_ids or []:
83
+ for hit in self.index.search(f'"{rid}"', limit=5):
84
+ bump(hit["path"], W_RULE, f"rule:{rid}")
85
+ if risk:
86
+ for term in ("iam", "policy", "secret", "sg", "security"):
87
+ for hit in self.index.by_path(f"*{term}*"):
88
+ bump(hit, W_RISK.get(risk, 0.0), f"risk:{risk}")
89
+ ranked = sorted(scores.items(), key=lambda kv: (-kv[1], kv[0]))
90
+ return [{"path": p, "score": round(s, 2), "reasons": reasons[p]}
91
+ for p, s in ranked[:limit]]
92
+
93
+ def build(self, task: str, budget: Budget | None = None,
94
+ changed_files: list[str] | None = None,
95
+ facts: list[dict] | None = None,
96
+ findings: list[dict] | None = None,
97
+ rules: list[str] | None = None,
98
+ graph_neighborhood: list[dict] | None = None,
99
+ risk: str | None = None,
100
+ previous_pack_hash: str | None = None,
101
+ previous_files: dict[str, str] | None = None,
102
+ sources: list[str] | None = None,
103
+ allow_escalate: bool = False,
104
+ max_file_bytes: int = 20_000,
105
+ retain_content: bool = False) -> dict[str, Any]:
106
+ budget = budget or Budget()
107
+ changed = changed_files or []
108
+ node_terms = [
109
+ f"{n.get('node', '')}:{n.get('depth', '')}" if isinstance(n, dict)
110
+ else str(n) for n in (graph_neighborhood or [])]
111
+ ranked = self._rank_files(self._task_terms(task), changed,
112
+ node_terms, rules, risk)
113
+ seen_hash: set[str] = set()
114
+
115
+ files_out: list[dict[str, Any]] = []
116
+ symbols_out: list[str] = []
117
+ used_tokens = 0
118
+ skipped = 0
119
+ reused = 0
120
+ for cand in ranked:
121
+ path, score, c_reasons = cand["path"], cand["score"], cand["reasons"]
122
+ body = self.index.read(path)
123
+ if body is None:
124
+ continue
125
+ sha = self.index.db.execute(
126
+ "SELECT sha256 FROM files WHERE path=?", (path,)).fetchone()[0]
127
+ if sha in seen_hash:
128
+ skipped += 1
129
+ continue # dedup identical content
130
+ cls = HIGH_VALUE if ("changed-file" in c_reasons
131
+ or "graph-distance:1" in c_reasons) \
132
+ else OPTIONAL
133
+ truncated = body[:max_file_bytes]
134
+ tokens = estimate_tokens(truncated)
135
+ if budget.input_budget and used_tokens + tokens > budget.input_budget:
136
+ head = truncated[: len(truncated) // 4]
137
+ head_tokens = estimate_tokens(head)
138
+ if used_tokens + head_tokens <= budget.input_budget:
139
+ truncated, tokens = head + "\n…[truncated by budget]", head_tokens
140
+ elif cls == HIGH_VALUE and previous_pack_hash is None:
141
+ # high-value overflowing: degrade to optional head anyway
142
+ skipped += 1
143
+ continue
144
+ else:
145
+ skipped += 1
146
+ continue
147
+ seen_hash.add(sha)
148
+ # §26 — real delta accounting: identical sha as the previous
149
+ # pack means the file was reused, not re-read fresh.
150
+ if previous_files and previous_files.get(path) == sha:
151
+ reused += 1
152
+ used_tokens += tokens
153
+ entry = {"path": path, "score": score,
154
+ "reasons": c_reasons, "class": cls,
155
+ "tokens": tokens, "sha256": sha,
156
+ "delivered_bytes": len(truncated.encode()),
157
+ "delivered_chars": len(truncated)}
158
+ if retain_content:
159
+ # exact delivered payload — QPT measures what ships
160
+ entry["content"] = truncated
161
+ files_out.append(entry)
162
+ sym_row = self.index.db.execute(
163
+ "SELECT symbols FROM files WHERE path=?", (path,)).fetchone()
164
+ if sym_row and sym_row[0]:
165
+ symbols_out.extend(sym_row[0].split()[:8])
166
+
167
+ # §27–28 — essential evidence is facts/findings/rules/unresolved.
168
+ essential = {"facts": facts or [], "findings": findings or [],
169
+ "rules": rules or []}
170
+ essential_tokens = estimate_tokens(json.dumps(essential, default=str))
171
+ over_essential = bool(budget.input_budget
172
+ and essential_tokens > budget.input_budget)
173
+ if over_essential and allow_escalate:
174
+ decision = "escalate"
175
+ decision_reason = ("essential evidence exceeds input budget — "
176
+ "escalating beyond budget (caller allowed)")
177
+ elif over_essential:
178
+ decision = "refuse"
179
+ decision_reason = ("essential evidence exceeds input budget "
180
+ "— refusing rather than dropping evidence")
181
+ elif used_tokens > 0 and budget.input_budget \
182
+ and used_tokens + essential_tokens > budget.input_budget:
183
+ decision = "reduced_scope"
184
+ decision_reason = "optional items dropped to fit budget"
185
+ elif skipped:
186
+ decision = "reduced_scope"
187
+ decision_reason = "duplicates/overflow skipped"
188
+ else:
189
+ decision, decision_reason = "ok", "within budget"
190
+
191
+ pack = {
192
+ "task": {"description": task, "changed_files": changed},
193
+ "relevant_files": files_out,
194
+ "relevant_symbols": sorted(set(symbols_out))[:100],
195
+ "facts": facts or [],
196
+ "findings": findings or [],
197
+ "graph_neighborhood": graph_neighborhood or [],
198
+ "rules": rules or [],
199
+ "sources": sorted(set(sources or [])),
200
+ "security": {
201
+ # §21 contract field — redaction happens at index write
202
+ # (pre-FTS); packs never carry raw secrets downstream.
203
+ "redaction": "applied-at-index",
204
+ "secrets_in_pack": 0},
205
+ "budget": budget.to_dict(),
206
+ "budget_decision": decision,
207
+ "budget_reason": decision_reason,
208
+ "packed_files": len(files_out),
209
+ "packed_bytes": sum(f["delivered_bytes"] for f in files_out),
210
+ "est_input_tokens": used_tokens + essential_tokens,
211
+ "essential_tokens": essential_tokens,
212
+ "skipped_duplicates": skipped,
213
+ "delta_aware": previous_pack_hash is not None,
214
+ "reused_from_previous": reused,
215
+ }
216
+ if decision == "refuse":
217
+ pack["refusals"] = [{
218
+ "code": "PF-BUDGET-ESSENTIAL",
219
+ "reason": decision_reason,
220
+ "unlock": "raise --input-budget or narrow the task scope"}]
221
+ pack["relevant_files"] = []
222
+ else:
223
+ pack["refusals"] = []
224
+
225
+ if self.ledger:
226
+ self.ledger.record(LedgerEntry(
227
+ operation="context.pack",
228
+ context_requested=estimate_tokens(task) + sum(
229
+ estimate_tokens(self.index.read(c["path"]) or "")
230
+ for c in ranked),
231
+ context_candidate=sum(
232
+ estimate_tokens(self.index.read(c["path"]) or "")
233
+ for c in ranked),
234
+ context_selected=used_tokens,
235
+ context_delivered=used_tokens + essential_tokens,
236
+ context_skipped=skipped,
237
+ essential_tokens=essential_tokens,
238
+ optional_tokens=used_tokens,
239
+ input_tokens_est=used_tokens + essential_tokens,
240
+ token_basis="estimated",
241
+ extra={"budget_decision": decision},
242
+ ))
243
+ return pack
244
+
245
+ def delta_for_change(self, changed_files: list[str],
246
+ budget: Budget | None = None,
247
+ graph_neighborhood: list[dict] | None = None,
248
+ previous_pack_hash: str | None = None,
249
+ risk: str | None = None) -> dict[str, Any]:
250
+ """§26 — PR-style delta: diff → files → graph neighbors → rules →
251
+ pack. previous_pack_hash marks the delta (content-addressed dedup
252
+ already skips identical bodies)."""
253
+ return self.build(
254
+ task="review change", budget=budget, changed_files=changed_files,
255
+ graph_neighborhood=graph_neighborhood,
256
+ previous_pack_hash=previous_pack_hash, risk=risk)
257
+
258
+
259
+ def pack_to_text(pack: dict[str, Any], index: SearchIndex) -> str:
260
+ """Render a pack for an agent prompt — file bodies bounded by what the
261
+ pack actually delivered (`delivered_bytes`), plus the essential
262
+ facts/findings/rules the pack carries. Never re-reads beyond the
263
+ pack's own truncation."""
264
+ out = [f"# task: {pack['task']['description']}"]
265
+ for f in pack["relevant_files"]:
266
+ path = f["path"] if isinstance(f, dict) else f
267
+ reasons = ""
268
+ content = None
269
+ if isinstance(f, dict):
270
+ reasons = f" # {', '.join(f.get('reasons', []))}"
271
+ content = f.get("content")
272
+ out.append(f"\n## {path}{reasons}")
273
+ if content is None:
274
+ body = index.read(path) or ""
275
+ delivered = f.get("delivered_chars") if isinstance(f, dict) \
276
+ else None
277
+ content = body[:delivered] if delivered else body[:4000]
278
+ out.append(content)
279
+ essential = {"facts": pack.get("facts") or [],
280
+ "findings": pack.get("findings") or [],
281
+ "rules": pack.get("rules") or []}
282
+ if any(essential.values()):
283
+ out.append("\n## essential-evidence")
284
+ out.append(json.dumps(essential, default=str)[:8000])
285
+ return "\n".join(out)
@@ -0,0 +1,28 @@
1
+ """platform-forge vendored Forge UI Kit — stdlib-only interactive terminal layer.
2
+
3
+ Zero dependencies, cross-platform (msvcrt on Windows, termios on
4
+ POSIX). Every interaction has a headless equivalent so automation never
5
+ blocks on a TTY that isn't there.
6
+ """
7
+
8
+ from platformforge.ui.i18n import t
9
+ from platformforge.ui.kit import (
10
+ UIContext,
11
+ confirm,
12
+ dashboard,
13
+ multi_select,
14
+ prompt,
15
+ select,
16
+ status_table,
17
+ )
18
+
19
+ __all__ = [
20
+ "UIContext",
21
+ "confirm",
22
+ "dashboard",
23
+ "multi_select",
24
+ "prompt",
25
+ "select",
26
+ "status_table",
27
+ "t",
28
+ ]