fde-framework 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 (286) hide show
  1. fde/__init__.py +3 -0
  2. fde/architect.py +152 -0
  3. fde/cli.py +2107 -0
  4. fde/costing.py +292 -0
  5. fde/decide.py +297 -0
  6. fde/decompose.py +54 -0
  7. fde/deploy.py +371 -0
  8. fde/emit.py +1309 -0
  9. fde/evolution.py +265 -0
  10. fde/factlog.py +369 -0
  11. fde/framework/approaches/ansible-playbook.md +22 -0
  12. fde/framework/approaches/assisted-deterministic.md +37 -0
  13. fde/framework/approaches/audit-only.md +23 -0
  14. fde/framework/approaches/boundary-and-audit.md +18 -0
  15. fde/framework/approaches/cascade.md +32 -0
  16. fde/framework/approaches/classical-ml.md +21 -0
  17. fde/framework/approaches/compose.md +18 -0
  18. fde/framework/approaches/decision-log.md +21 -0
  19. fde/framework/approaches/deterministic-masking.md +18 -0
  20. fde/framework/approaches/deterministic.md +25 -0
  21. fde/framework/approaches/direct-call.md +10 -0
  22. fde/framework/approaches/episodic-store.md +17 -0
  23. fde/framework/approaches/explainability-record.md +14 -0
  24. fde/framework/approaches/field-match.md +15 -0
  25. fde/framework/approaches/finetune.md +29 -0
  26. fde/framework/approaches/fixed-sequence.md +19 -0
  27. fde/framework/approaches/gitops.md +18 -0
  28. fde/framework/approaches/governed-tools.md +18 -0
  29. fde/framework/approaches/graph-retrieval.md +22 -0
  30. fde/framework/approaches/judged.md +15 -0
  31. fde/framework/approaches/keyword-search.md +11 -0
  32. fde/framework/approaches/kubernetes-manifests.md +19 -0
  33. fde/framework/approaches/labelled-metrics.md +14 -0
  34. fde/framework/approaches/llm-extraction.md +26 -0
  35. fde/framework/approaches/llm-scrubbing.md +20 -0
  36. fde/framework/approaches/llm.md +23 -0
  37. fde/framework/approaches/local-embedding.md +28 -0
  38. fde/framework/approaches/managed-api.md +49 -0
  39. fde/framework/approaches/managed-embedding.md +22 -0
  40. fde/framework/approaches/manual-runbook.md +32 -0
  41. fde/framework/approaches/model-planner.md +19 -0
  42. fde/framework/approaches/ocr-pipeline.md +13 -0
  43. fde/framework/approaches/optimisation-reasoning.md +30 -0
  44. fde/framework/approaches/optimisation.md +19 -0
  45. fde/framework/approaches/passthrough.md +11 -0
  46. fde/framework/approaches/role-scoped-authority.md +19 -0
  47. fde/framework/approaches/segmentation.md +29 -0
  48. fde/framework/approaches/self-hosted.md +38 -0
  49. fde/framework/approaches/serverless-gpu.md +27 -0
  50. fde/framework/approaches/speech-transcription.md +21 -0
  51. fde/framework/approaches/structured-logs.md +11 -0
  52. fde/framework/approaches/systemd-unit.md +30 -0
  53. fde/framework/approaches/terraform-module.md +22 -0
  54. fde/framework/approaches/text-extraction.md +14 -0
  55. fde/framework/approaches/traced.md +24 -0
  56. fde/framework/approaches/vector-search.md +17 -0
  57. fde/framework/approaches/video-ingestion.md +16 -0
  58. fde/framework/approaches/windowed-ingestion.md +25 -0
  59. fde/framework/approaches/working-state.md +13 -0
  60. fde/framework/cases/churn-scoring.md +45 -0
  61. fde/framework/cases/route-planning.md +44 -0
  62. fde/framework/cases/structured-extraction.md +48 -0
  63. fde/framework/cases/studio-style.md +46 -0
  64. fde/framework/components/accountability.md +20 -0
  65. fde/framework/components/deployment.md +19 -0
  66. fde/framework/components/embedding.md +23 -0
  67. fde/framework/components/evaluation.md +13 -0
  68. fde/framework/components/governance.md +19 -0
  69. fde/framework/components/integration.md +13 -0
  70. fde/framework/components/memory.md +21 -0
  71. fde/framework/components/observability.md +13 -0
  72. fde/framework/components/perception.md +15 -0
  73. fde/framework/components/planning.md +15 -0
  74. fde/framework/components/provisioning.md +17 -0
  75. fde/framework/components/reasoning.md +20 -0
  76. fde/framework/components/redaction.md +18 -0
  77. fde/framework/components/representation.md +13 -0
  78. fde/framework/components/retrieval.md +13 -0
  79. fde/framework/components/serving.md +20 -0
  80. fde/framework/dimensions/accelerator.md +29 -0
  81. fde/framework/dimensions/access_model.md +25 -0
  82. fde/framework/dimensions/arrival_rate.md +19 -0
  83. fde/framework/dimensions/availability_target.md +23 -0
  84. fde/framework/dimensions/cheap_path_coverage.md +21 -0
  85. fde/framework/dimensions/confidence_calibrated.md +30 -0
  86. fde/framework/dimensions/container_competence.md +22 -0
  87. fde/framework/dimensions/corpus_size.md +13 -0
  88. fde/framework/dimensions/data_residency.md +36 -0
  89. fde/framework/dimensions/environment_lifetime.md +19 -0
  90. fde/framework/dimensions/existing_cluster.md +22 -0
  91. fde/framework/dimensions/existing_iac_tool.md +25 -0
  92. fde/framework/dimensions/external_systems.md +15 -0
  93. fde/framework/dimensions/hosting.md +45 -0
  94. fde/framework/dimensions/human_waiting.md +41 -0
  95. fde/framework/dimensions/input_format.md +29 -0
  96. fde/framework/dimensions/interpretability_required.md +24 -0
  97. fde/framework/dimensions/labelled_count.md +15 -0
  98. fde/framework/dimensions/latency_budget_ms.md +16 -0
  99. fde/framework/dimensions/licence_posture.md +28 -0
  100. fde/framework/dimensions/operates_after_handover.md +25 -0
  101. fde/framework/dimensions/output_shape.md +29 -0
  102. fde/framework/dimensions/provisioning_api.md +18 -0
  103. fde/framework/dimensions/query_pattern.md +25 -0
  104. fde/framework/dimensions/recall_span.md +22 -0
  105. fde/framework/dimensions/sensitivity_present.md +22 -0
  106. fde/framework/interfaces/Generator.md +5 -0
  107. fde/framework/interfaces/Guard.md +5 -0
  108. fde/framework/interfaces/Mapper.md +5 -0
  109. fde/framework/interfaces/ModelServer.md +5 -0
  110. fde/framework/interfaces/Parser.md +5 -0
  111. fde/framework/interfaces/Planner.md +5 -0
  112. fde/framework/interfaces/Retriever.md +5 -0
  113. fde/framework/interfaces/Scorer.md +5 -0
  114. fde/framework/interfaces/Store.md +5 -0
  115. fde/framework/interfaces/ToolBoundary.md +5 -0
  116. fde/framework/interfaces/Tracer.md +5 -0
  117. fde/framework/locales/eu-gdpr.md +51 -0
  118. fde/framework/locales/in-dpdp.md +46 -0
  119. fde/framework/patterns/ansible-playbook.md +9 -0
  120. fde/framework/patterns/assisted-deterministic.md +11 -0
  121. fde/framework/patterns/audit-only.md +9 -0
  122. fde/framework/patterns/boundary-and-audit.md +12 -0
  123. fde/framework/patterns/cascade-reasoning.md +10 -0
  124. fde/framework/patterns/cascade-representation.md +10 -0
  125. fde/framework/patterns/cascade-retrieval.md +10 -0
  126. fde/framework/patterns/classical-ml-reasoning.md +15 -0
  127. fde/framework/patterns/classical-ml.md +13 -0
  128. fde/framework/patterns/compose.md +9 -0
  129. fde/framework/patterns/decision-log.md +9 -0
  130. fde/framework/patterns/deterministic-masking.md +9 -0
  131. fde/framework/patterns/deterministic.md +12 -0
  132. fde/framework/patterns/direct-call.md +12 -0
  133. fde/framework/patterns/episodic-store.md +13 -0
  134. fde/framework/patterns/explainability-record.md +9 -0
  135. fde/framework/patterns/field-match.md +12 -0
  136. fde/framework/patterns/finetune-representation.md +14 -0
  137. fde/framework/patterns/finetune.md +12 -0
  138. fde/framework/patterns/fixed-sequence.md +12 -0
  139. fde/framework/patterns/gitops.md +9 -0
  140. fde/framework/patterns/governed-tools.md +13 -0
  141. fde/framework/patterns/graph-retrieval.md +14 -0
  142. fde/framework/patterns/judged.md +14 -0
  143. fde/framework/patterns/keyword-search.md +12 -0
  144. fde/framework/patterns/kubernetes-manifests.md +9 -0
  145. fde/framework/patterns/labelled-metrics.md +13 -0
  146. fde/framework/patterns/llm-representation.md +17 -0
  147. fde/framework/patterns/llm-scrubbing.md +9 -0
  148. fde/framework/patterns/llm.md +12 -0
  149. fde/framework/patterns/local-embedding.md +9 -0
  150. fde/framework/patterns/managed-api.md +12 -0
  151. fde/framework/patterns/managed-embedding.md +9 -0
  152. fde/framework/patterns/manual-runbook.md +9 -0
  153. fde/framework/patterns/model-planner.md +13 -0
  154. fde/framework/patterns/ocr-pipeline.md +13 -0
  155. fde/framework/patterns/optimisation-reasoning.md +15 -0
  156. fde/framework/patterns/optimisation.md +13 -0
  157. fde/framework/patterns/passthrough.md +12 -0
  158. fde/framework/patterns/role-scoped-authority.md +9 -0
  159. fde/framework/patterns/segmentation.md +12 -0
  160. fde/framework/patterns/self-hosted.md +14 -0
  161. fde/framework/patterns/serverless-gpu.md +12 -0
  162. fde/framework/patterns/speech-transcription.md +10 -0
  163. fde/framework/patterns/structured-logs.md +12 -0
  164. fde/framework/patterns/systemd-unit.md +9 -0
  165. fde/framework/patterns/terraform-module.md +9 -0
  166. fde/framework/patterns/text-extraction.md +12 -0
  167. fde/framework/patterns/traced.md +13 -0
  168. fde/framework/patterns/vector-search.md +14 -0
  169. fde/framework/patterns/video-ingestion.md +9 -0
  170. fde/framework/patterns/windowed-ingestion.md +12 -0
  171. fde/framework/patterns/working-state.md +12 -0
  172. fde/framework/stacks/langgraph.md +9 -0
  173. fde/framework/stacks/local-judge.md +9 -0
  174. fde/framework/stacks/mcp.md +9 -0
  175. fde/framework/stacks/ollama.md +19 -0
  176. fde/framework/stacks/openai-judge.md +9 -0
  177. fde/framework/stacks/opentelemetry.md +9 -0
  178. fde/framework/stacks/ortools.md +9 -0
  179. fde/framework/stacks/pgvector.md +9 -0
  180. fde/framework/stacks/plain-python.md +9 -0
  181. fde/framework/stacks/qdrant.md +9 -0
  182. fde/framework/stacks/tesseract.md +9 -0
  183. fde/framework/stacks/vllm.md +9 -0
  184. fde/framework/stacks/whisper.md +15 -0
  185. fde/framework/stacks/xgboost.md +9 -0
  186. fde/framework/templates/accountability/decision-log.plain.py.j2 +64 -0
  187. fde/framework/templates/accountability/explainability-record.plain.py.j2 +90 -0
  188. fde/framework/templates/deployment/compose.plain.py.j2 +36 -0
  189. fde/framework/templates/deployment/kubernetes-manifests.plain.py.j2 +36 -0
  190. fde/framework/templates/deployment/systemd-unit.plain.py.j2 +36 -0
  191. fde/framework/templates/embedding/local-embedding.plain.py.j2 +71 -0
  192. fde/framework/templates/embedding/managed-embedding.plain.py.j2 +76 -0
  193. fde/framework/templates/evaluation/field-match.plain.py.j2 +146 -0
  194. fde/framework/templates/evaluation/judged.local-judge.py.j2 +100 -0
  195. fde/framework/templates/evaluation/judged.openai-judge.py.j2 +70 -0
  196. fde/framework/templates/evaluation/judged.plain.py.j2 +89 -0
  197. fde/framework/templates/evaluation/labelled-metrics.plain.py.j2 +64 -0
  198. fde/framework/templates/evaluation/labelled-metrics.xgboost.py.j2 +72 -0
  199. fde/framework/templates/governance/audit-only.plain.py.j2 +98 -0
  200. fde/framework/templates/governance/boundary-and-audit.plain.py.j2 +142 -0
  201. fde/framework/templates/governance/role-scoped-authority.plain.py.j2 +63 -0
  202. fde/framework/templates/integration/direct-call.plain.py.j2 +63 -0
  203. fde/framework/templates/integration/governed-tools.mcp.py.j2 +124 -0
  204. fde/framework/templates/integration/governed-tools.plain.py.j2 +153 -0
  205. fde/framework/templates/memory/episodic-store.pgvector.py.j2 +118 -0
  206. fde/framework/templates/memory/episodic-store.plain.py.j2 +147 -0
  207. fde/framework/templates/memory/working-state.plain.py.j2 +48 -0
  208. fde/framework/templates/observability/structured-logs.plain.py.j2 +61 -0
  209. fde/framework/templates/observability/traced.opentelemetry.py.j2 +65 -0
  210. fde/framework/templates/observability/traced.plain.py.j2 +132 -0
  211. fde/framework/templates/perception/ocr-pipeline.plain.py.j2 +71 -0
  212. fde/framework/templates/perception/ocr-pipeline.tesseract.py.j2 +80 -0
  213. fde/framework/templates/perception/passthrough.plain.py.j2 +41 -0
  214. fde/framework/templates/perception/speech-transcription.plain.py.j2 +37 -0
  215. fde/framework/templates/perception/speech-transcription.whisper.py.j2 +39 -0
  216. fde/framework/templates/perception/text-extraction.plain.py.j2 +88 -0
  217. fde/framework/templates/perception/video-ingestion.plain.py.j2 +35 -0
  218. fde/framework/templates/perception/windowed-ingestion.plain.py.j2 +69 -0
  219. fde/framework/templates/planning/fixed-sequence.plain.py.j2 +46 -0
  220. fde/framework/templates/planning/model-planner.langgraph.py.j2 +89 -0
  221. fde/framework/templates/planning/model-planner.plain.py.j2 +82 -0
  222. fde/framework/templates/planning/optimisation.ortools.py.j2 +90 -0
  223. fde/framework/templates/planning/optimisation.plain.py.j2 +71 -0
  224. fde/framework/templates/provisioning/ansible-playbook.plain.py.j2 +29 -0
  225. fde/framework/templates/provisioning/gitops.plain.py.j2 +29 -0
  226. fde/framework/templates/provisioning/manual-runbook.plain.py.j2 +29 -0
  227. fde/framework/templates/provisioning/terraform-module.plain.py.j2 +29 -0
  228. fde/framework/templates/reasoning/cascade.plain.py.j2 +86 -0
  229. fde/framework/templates/reasoning/classical-ml.plain.py.j2 +74 -0
  230. fde/framework/templates/reasoning/classical-ml.xgboost.py.j2 +104 -0
  231. fde/framework/templates/reasoning/finetune.plain.py.j2 +73 -0
  232. fde/framework/templates/reasoning/llm.plain.py.j2 +114 -0
  233. fde/framework/templates/reasoning/optimisation.ortools.py.j2 +90 -0
  234. fde/framework/templates/reasoning/optimisation.plain.py.j2 +58 -0
  235. fde/framework/templates/redaction/deterministic-masking.plain.py.j2 +44 -0
  236. fde/framework/templates/redaction/llm-scrubbing.plain.py.j2 +40 -0
  237. fde/framework/templates/representation/assisted.plain.py.j2 +86 -0
  238. fde/framework/templates/representation/cascade.plain.py.j2 +119 -0
  239. fde/framework/templates/representation/classical-ml.plain.py.j2 +74 -0
  240. fde/framework/templates/representation/classical-ml.xgboost.py.j2 +104 -0
  241. fde/framework/templates/representation/deterministic.plain.py.j2 +101 -0
  242. fde/framework/templates/representation/finetune.plain.py.j2 +68 -0
  243. fde/framework/templates/representation/llm.plain.py.j2 +94 -0
  244. fde/framework/templates/representation/segmentation.plain.py.j2 +68 -0
  245. fde/framework/templates/retrieval/cascade.plain.py.j2 +86 -0
  246. fde/framework/templates/retrieval/graph-retrieval.pgvector.py.j2 +88 -0
  247. fde/framework/templates/retrieval/graph-retrieval.plain.py.j2 +74 -0
  248. fde/framework/templates/retrieval/graph-retrieval.qdrant.py.j2 +71 -0
  249. fde/framework/templates/retrieval/keyword-search.plain.py.j2 +104 -0
  250. fde/framework/templates/retrieval/vector-search.pgvector.py.j2 +100 -0
  251. fde/framework/templates/retrieval/vector-search.plain.py.j2 +84 -0
  252. fde/framework/templates/retrieval/vector-search.qdrant.py.j2 +60 -0
  253. fde/framework/templates/serving/managed-api.plain.py.j2 +74 -0
  254. fde/framework/templates/serving/self-hosted.ollama.py.j2 +47 -0
  255. fde/framework/templates/serving/self-hosted.plain.py.j2 +117 -0
  256. fde/framework/templates/serving/self-hosted.vllm.py.j2 +77 -0
  257. fde/framework/templates/serving/serverless-gpu.plain.py.j2 +62 -0
  258. fde/gates.py +480 -0
  259. fde/graph.py +435 -0
  260. fde/implement.py +250 -0
  261. fde/intake/__init__.py +0 -0
  262. fde/intake/answers.py +154 -0
  263. fde/intake/documents.py +121 -0
  264. fde/intake/interview.py +218 -0
  265. fde/intake/llm_reader.py +336 -0
  266. fde/intake/prose.py +387 -0
  267. fde/intake/samples.py +369 -0
  268. fde/models/__init__.py +0 -0
  269. fde/models/base.py +86 -0
  270. fde/models/fact.py +37 -0
  271. fde/models/profile.py +159 -0
  272. fde/models/respondent.py +34 -0
  273. fde/models/schema.py +430 -0
  274. fde/moves.py +136 -0
  275. fde/ops.py +349 -0
  276. fde/predicate.py +106 -0
  277. fde/realization.py +109 -0
  278. fde/registry.py +162 -0
  279. fde/scan.py +479 -0
  280. fde/space.py +172 -0
  281. fde/workflow.py +233 -0
  282. fde_framework-0.1.0.dist-info/METADATA +449 -0
  283. fde_framework-0.1.0.dist-info/RECORD +286 -0
  284. fde_framework-0.1.0.dist-info/WHEEL +4 -0
  285. fde_framework-0.1.0.dist-info/entry_points.txt +2 -0
  286. fde_framework-0.1.0.dist-info/licenses/LICENSE +202 -0
fde/deploy.py ADDED
@@ -0,0 +1,371 @@
1
+ """The deployment artefacts, chosen rather than defaulted.
2
+
3
+ What gets written follows the decision. A service-unit deployment gets no
4
+ Dockerfile, a cluster deployment gets manifests and no Terraform, and an
5
+ Ansible shop gets a playbook whatever the topology.
6
+
7
+ Two things are written regardless of the choice.
8
+
9
+ **A README naming the decisions.** Somebody opening deploy/ six months later
10
+ should not have to infer why it looks like this from what is in it.
11
+
12
+ **TEARDOWN.md.** An FDE who cannot cleanly undo a demo has a problem, and only
13
+ one provisioning option knows what it created. Where the tool has no destroy,
14
+ the manual steps are written out rather than left implied.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from pathlib import Path
20
+
21
+ # Buildable as emitted; pin before production. A moving tag means two builds
22
+ # of the same commit can differ, which turns a reproducibility question into
23
+ # an archaeology one -- but an unresolvable placeholder digest means nothing
24
+ # builds at all, which is worse. The Dockerfile carries the instruction.
25
+ PYTHON_BASE = "python:3.12-slim"
26
+
27
+
28
+ def write_deploy(architecture, out: Path) -> None:
29
+ deploy = out / "deploy"
30
+ deploy.mkdir(parents=True, exist_ok=True)
31
+
32
+ substrate = _approach(architecture, "deployment")
33
+ provisioner = _approach(architecture, "provisioning")
34
+ air_gapped = architecture.topology == "air-gapped"
35
+
36
+ if substrate == "systemd-unit":
37
+ _systemd(deploy)
38
+ elif substrate == "compose":
39
+ _container(out, deploy, air_gapped)
40
+ _compose(deploy)
41
+ elif substrate == "kubernetes-manifests":
42
+ _container(out, deploy, air_gapped)
43
+ _manifests(deploy)
44
+ elif substrate:
45
+ _unemitted(deploy, "deployment", substrate)
46
+
47
+ if provisioner == "terraform-module":
48
+ _terraform(deploy, air_gapped)
49
+ elif provisioner == "ansible-playbook":
50
+ _ansible(deploy, substrate)
51
+ elif provisioner == "gitops":
52
+ _gitops(deploy)
53
+ elif provisioner == "manual-runbook":
54
+ _manual_runbook(deploy)
55
+ elif provisioner:
56
+ _unemitted(deploy, "provisioning", provisioner)
57
+
58
+ _readme(deploy, substrate, provisioner, architecture.topology)
59
+ _teardown(deploy, substrate, provisioner)
60
+
61
+
62
+ def _unemitted(deploy: Path, component: str, approach: str) -> None:
63
+ """An approach the registry knows and this emitter does not.
64
+
65
+ Written down rather than skipped: the registry can grow a deployment
66
+ approach faster than this module grows a branch for it, and an empty
67
+ deploy directory reads as a finished one.
68
+ """
69
+ (deploy / f"UNEMITTED-{component}.md").write_text(
70
+ f"# {approach}: decided, not emitted\n\n"
71
+ f"The registry decided {approach!r} for {component}, and this version "
72
+ f"of the emitter has no assets for it. The decision stands -- write "
73
+ f"the assets by hand, and consider contributing the emitter branch.\n"
74
+ )
75
+
76
+
77
+ # --- substrate -----------------------------------------------------------
78
+
79
+
80
+ def _systemd(deploy: Path) -> None:
81
+ (deploy / "systemd").mkdir(exist_ok=True)
82
+ (deploy / "systemd" / "app.service").write_text(
83
+ "# Rung zero, and frequently the right answer rather than the lesser one.\n"
84
+ "# Understood by anyone who has administered a Linux box, restarts on\n"
85
+ "# failure, starts on boot, and adds nothing anybody has to learn.\n"
86
+ "[Unit]\n"
87
+ "Description=Generated application\n"
88
+ "After=network-online.target\n"
89
+ "Wants=network-online.target\n\n"
90
+ "[Service]\n"
91
+ "Type=simple\n"
92
+ "User=app\n"
93
+ "WorkingDirectory=/opt/app\n"
94
+ "ExecStart=/opt/app/.venv/bin/python -m app.pipeline\n"
95
+ "Restart=on-failure\n"
96
+ "RestartSec=5\n"
97
+ "# Least privilege costs nothing here and is awkward to add later.\n"
98
+ "NoNewPrivileges=true\n"
99
+ "PrivateTmp=true\n"
100
+ "ProtectSystem=strict\n"
101
+ "ReadWritePaths=/var/lib/app\n\n"
102
+ "[Install]\n"
103
+ "WantedBy=multi-user.target\n"
104
+ )
105
+
106
+
107
+ def _container(out: Path, deploy: Path, air_gapped: bool) -> None:
108
+ registry = (
109
+ "# Air-gapped: this base must already be in the internal registry.\n"
110
+ "# Nothing here reaches a public one, by construction.\n"
111
+ if air_gapped else ""
112
+ )
113
+ (out / "Dockerfile").write_text(
114
+ "# Buildable as emitted. Before production, pin by digest\n"
115
+ "# (FROM python@sha256:...): a moving tag means two builds of the same\n"
116
+ "# commit can differ, which turns a reproducibility question into an\n"
117
+ "# archaeology one.\n"
118
+ f"{registry}"
119
+ f"FROM {PYTHON_BASE}\n\n"
120
+ "WORKDIR /opt/app\n"
121
+ "COPY pyproject.toml ./\n"
122
+ "RUN pip install --no-cache-dir -e .\n"
123
+ "COPY app ./app\n"
124
+ "COPY evals ./evals\n\n"
125
+ "# Not root. The default is root and the default is wrong.\n"
126
+ "RUN useradd --system --uid 10001 app && chown -R app /opt/app\n"
127
+ "USER app\n\n"
128
+ 'CMD ["python", "-m", "app.pipeline"]\n'
129
+ )
130
+ # At the context root: compose builds with context .., so a
131
+ # .dockerignore inside deploy/ is a file Docker never reads.
132
+ (out / ".dockerignore").write_text(".venv\n__pycache__\n*.pyc\n.git\n")
133
+
134
+
135
+ def _compose(deploy: Path) -> None:
136
+ (deploy / "compose.yaml").write_text(
137
+ "# One host, declared. Its limit is honest: while this host restarts,\n"
138
+ "# the service is down. If that is unacceptable, the next rung is the\n"
139
+ "# answer rather than a workaround here.\n"
140
+ "services:\n"
141
+ " app:\n"
142
+ " build: ..\n"
143
+ " restart: unless-stopped\n"
144
+ " read_only: true\n"
145
+ " tmpfs: [/tmp]\n"
146
+ " cap_drop: [ALL]\n"
147
+ " security_opt: [no-new-privileges:true]\n"
148
+ " healthcheck:\n"
149
+ ' test: ["CMD", "python", "-c", "import app.pipeline"]\n'
150
+ " interval: 30s\n"
151
+ )
152
+
153
+
154
+ def _manifests(deploy: Path) -> None:
155
+ (deploy / "manifests").mkdir(exist_ok=True)
156
+ (deploy / "manifests" / "deployment.yaml").write_text(
157
+ "# Applied to a cluster that already exists. Cheap because somebody\n"
158
+ "# else patches it -- which is the entire argument for a platform.\n"
159
+ "apiVersion: apps/v1\n"
160
+ "kind: Deployment\n"
161
+ "metadata:\n"
162
+ " name: app\n"
163
+ "spec:\n"
164
+ " replicas: 2\n"
165
+ " selector:\n"
166
+ " matchLabels: {app: app}\n"
167
+ " template:\n"
168
+ " metadata:\n"
169
+ " labels: {app: app}\n"
170
+ " spec:\n"
171
+ " securityContext:\n"
172
+ " runAsNonRoot: true\n"
173
+ " runAsUser: 10001\n"
174
+ " containers:\n"
175
+ " - name: app\n"
176
+ " image: app@sha256:REPLACE_WITH_DIGEST\n"
177
+ " # Both set. A limit without a request is a pod the scheduler\n"
178
+ " # cannot place sensibly; a request without a limit is a\n"
179
+ " # neighbour nobody can protect.\n"
180
+ " resources:\n"
181
+ " requests: {cpu: 500m, memory: 512Mi}\n"
182
+ " limits: {cpu: '2', memory: 2Gi}\n"
183
+ " securityContext:\n"
184
+ " allowPrivilegeEscalation: false\n"
185
+ " readOnlyRootFilesystem: true\n"
186
+ " capabilities: {drop: [ALL]}\n"
187
+ )
188
+
189
+
190
+ # --- provisioning --------------------------------------------------------
191
+
192
+
193
+ def _terraform(deploy: Path, air_gapped: bool) -> None:
194
+ directory = deploy / "terraform"
195
+ directory.mkdir(exist_ok=True)
196
+ mirror = (
197
+ "\n# Air-gapped: providers come from a filesystem mirror, because there\n"
198
+ "# is no registry to reach. Run `terraform providers mirror ./vendor`\n"
199
+ "# somewhere with network access and carry the result in.\n"
200
+ 'provider_installation {\n filesystem_mirror { path = "./vendor" }\n}\n'
201
+ if air_gapped else ""
202
+ )
203
+ (directory / "main.tf").write_text(
204
+ "# Chosen because this environment has to be destroyed cleanly, which is\n"
205
+ "# the one thing a convergence tool cannot do.\n"
206
+ "terraform {\n"
207
+ ' required_version = ">= 1.5"\n'
208
+ "}\n"
209
+ f"{mirror}\n"
210
+ "variable \"environment\" {\n"
211
+ " type = string\n"
212
+ " description = \"Name of this environment. Used in every resource name\"\n"
213
+ "}\n\n"
214
+ "# Resources go here. Keep them in one module per environment so that\n"
215
+ "# `terraform destroy` takes exactly one environment away.\n"
216
+ )
217
+ if air_gapped:
218
+ (directory / "vendor").mkdir(exist_ok=True)
219
+ (directory / "vendor" / ".gitkeep").write_text("")
220
+
221
+
222
+ def _ansible(deploy: Path, substrate: str | None = None) -> None:
223
+ directory = deploy / "ansible"
224
+ directory.mkdir(exist_ok=True)
225
+ # The unit-file tasks exist only when the substrate emitted a unit file:
226
+ # a playbook copying deploy/systemd/ beside a compose substrate fails on
227
+ # its first task, against a file this emitter never wrote.
228
+ unit_tasks = (
229
+ " - name: Install the service unit\n"
230
+ " ansible.builtin.copy:\n"
231
+ " src: ../systemd/app.service\n"
232
+ " dest: /etc/systemd/system/app.service\n"
233
+ " notify: restart app\n"
234
+ " handlers:\n"
235
+ " - name: restart app\n"
236
+ " ansible.builtin.systemd:\n"
237
+ " name: app\n"
238
+ " state: restarted\n"
239
+ " daemon_reload: true\n"
240
+ " enabled: true\n"
241
+ if substrate == "systemd-unit" else
242
+ " # The substrate deploys through its own mechanism; this playbook\n"
243
+ " # only stages the application onto the host.\n"
244
+ )
245
+ (directory / "site.yml").write_text(
246
+ "# Chosen because this is what the team already operates. They maintain\n"
247
+ "# it after the engagement ends, and the tool they cannot maintain is\n"
248
+ "# the one that rots.\n"
249
+ "- name: Deploy the application\n"
250
+ " hosts: app\n"
251
+ " become: true\n"
252
+ " tasks:\n"
253
+ " - name: Create the service account\n"
254
+ " ansible.builtin.user:\n"
255
+ " name: app\n"
256
+ " system: true\n"
257
+ " - name: Install the application\n"
258
+ " ansible.builtin.copy:\n"
259
+ " src: ../../app\n"
260
+ " dest: /opt/app/\n"
261
+ " owner: app\n"
262
+ f"{unit_tasks}"
263
+ )
264
+ (directory / "inventory.ini").write_text(
265
+ "# Hosts that already exist. Nothing here creates a machine, because\n"
266
+ "# somebody already did.\n"
267
+ "[app]\n"
268
+ "# app-01.internal\n"
269
+ )
270
+
271
+
272
+ def _manual_runbook(deploy: Path) -> None:
273
+ (deploy / "runbook.md").write_text(
274
+ "# Provisioning runbook\n\n"
275
+ "Chosen because nothing here can be provisioned through an API --\n"
276
+ "somebody files a ticket, somebody racks a machine -- so the honest\n"
277
+ "artefact is the list of steps a person follows, written down once\n"
278
+ "instead of re-derived per environment.\n\n"
279
+ "Fill in each step as it is learned. A runbook nobody updates is a\n"
280
+ "runbook that lies.\n\n"
281
+ "## Request\n\n1. _who to ask, and for what_\n\n"
282
+ "## Verify\n\n1. _what proves the environment is usable_\n\n"
283
+ "## Hand back\n\n1. _how this environment is returned or destroyed_\n"
284
+ )
285
+
286
+
287
+ def _gitops(deploy: Path) -> None:
288
+ (deploy / "gitops.md").write_text(
289
+ "# Reconciled onto an existing cluster\n\n"
290
+ "The infrastructure is somebody else's problem. Adding a provisioner "
291
+ "here would provision nothing and give whoever takes this over a second "
292
+ "thing to maintain.\n\n"
293
+ "What this buys instead is that the deployed state is reviewable and "
294
+ "revertable by whoever already reviews changes -- which is usually the "
295
+ "property people wanted from a provisioning tool in the first place.\n\n"
296
+ "Point the cluster's reconciler at `deploy/manifests`.\n"
297
+ )
298
+
299
+
300
+ # --- always --------------------------------------------------------------
301
+
302
+
303
+ def _readme(deploy: Path, substrate: str | None, provisioner: str | None,
304
+ topology: str) -> None:
305
+ (deploy / "README.md").write_text(
306
+ f"# Deployment\n\n"
307
+ f"Topology: **{topology}** \n"
308
+ f"Substrate: **{substrate or 'not decided'}** \n"
309
+ f"Provisioning: **{provisioner or 'not decided'}**\n\n"
310
+ f"Neither of these is a default. The substrate is a ladder and this is "
311
+ f"the rung the profile earned; the provisioner follows what the team "
312
+ f"already operates, whether there is an API to call, and whether this "
313
+ f"environment has to be destroyed cleanly.\n\n"
314
+ f"The reasoning for each is in `ARCHITECTURE.md`, alongside what was "
315
+ f"rejected and why.\n"
316
+ )
317
+
318
+
319
+ def _teardown(deploy: Path, substrate: str | None, provisioner: str | None) -> None:
320
+ """Written whatever the tools -- both of them.
321
+
322
+ The substrate and the provisioner each leave things behind, and a
323
+ teardown that covers only one is how a demo's service unit outlives the
324
+ engagement. An earlier version branched on provisioner *or* substrate:
325
+ choosing Terraform -- the one tool that can destroy what it made --
326
+ was exactly what suppressed the substrate's manual steps.
327
+ """
328
+ substrate_steps = {
329
+ "systemd-unit": "sudo systemctl disable --now app\n"
330
+ "sudo rm /etc/systemd/system/app.service\n"
331
+ "sudo systemctl daemon-reload\n"
332
+ "sudo rm -rf /opt/app /var/lib/app\n"
333
+ "sudo userdel app\n",
334
+ "compose": "cd deploy && docker compose down --volumes\n"
335
+ "docker image rm $(docker compose config --images)\n",
336
+ "kubernetes-manifests": "kubectl delete -f deploy/manifests\n"
337
+ "# check for retained PersistentVolumes\n"
338
+ "kubectl get pv | grep app\n",
339
+ }.get(substrate or "", "# nothing was deployed\n")
340
+
341
+ sections = [
342
+ "## The application\n\n"
343
+ "The substrate has no concept of un-doing, so these are manual and "
344
+ "written out rather than left implied.\n\n"
345
+ f"```bash\n{substrate_steps}```\n"
346
+ ]
347
+ if provisioner == "terraform-module":
348
+ sections.append(
349
+ "## The environment\n\n"
350
+ "```bash\ncd deploy/terraform\nterraform destroy\n```\n\n"
351
+ "This tool tracks what it created, so this takes it away cleanly. "
352
+ "Check that no state remains in a remote backend afterwards.\n"
353
+ )
354
+ else:
355
+ sections.append(
356
+ "## The environment\n\n"
357
+ "This provisioner has no destroy. Whatever it configured -- users, "
358
+ "packages, mounts -- is removed by hand, or by re-running the "
359
+ "provisioning against a clean target.\n"
360
+ )
361
+ sections.append(
362
+ "Then confirm nothing was left behind: data directories, secrets in "
363
+ "a vault, DNS entries, and anything created by hand during the "
364
+ "engagement.\n"
365
+ )
366
+ (deploy / "TEARDOWN.md").write_text("# Taking it away\n\n" + "\n".join(sections))
367
+
368
+
369
+ def _approach(architecture, component: str) -> str | None:
370
+ decision = architecture.decisions.get(component)
371
+ return decision.approach if decision else None