nullforge 0.2.0__tar.gz → 0.3.1__tar.gz

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 (231) hide show
  1. {nullforge-0.2.0 → nullforge-0.3.1}/.github/CONTRIBUTING.md +35 -2
  2. {nullforge-0.2.0 → nullforge-0.3.1}/.github/dependabot.yml +6 -3
  3. {nullforge-0.2.0 → nullforge-0.3.1}/.github/labeler.yaml +2 -0
  4. {nullforge-0.2.0 → nullforge-0.3.1}/.github/workflows/ci-code-quality.yaml +4 -12
  5. nullforge-0.3.1/.github/workflows/ci-docs.yaml +37 -0
  6. {nullforge-0.2.0 → nullforge-0.3.1}/.github/workflows/ci-tests.yaml +11 -6
  7. nullforge-0.3.1/.github/workflows/deploy-docs.yaml +62 -0
  8. {nullforge-0.2.0 → nullforge-0.3.1}/.github/workflows/release-please.yaml +37 -3
  9. nullforge-0.3.1/.markdownlint-cli2.jsonc +25 -0
  10. {nullforge-0.2.0 → nullforge-0.3.1}/.pre-commit-config.yaml +8 -3
  11. nullforge-0.3.1/.release-please-manifest.json +3 -0
  12. {nullforge-0.2.0 → nullforge-0.3.1}/CHANGELOG.md +32 -0
  13. nullforge-0.3.1/PKG-INFO +48 -0
  14. nullforge-0.3.1/README.md +29 -0
  15. nullforge-0.3.1/docs/concepts/architecture.md +61 -0
  16. nullforge-0.3.1/docs/concepts/inventories.md +152 -0
  17. nullforge-0.3.1/docs/concepts/molds.md +52 -0
  18. nullforge-0.3.1/docs/concepts/runes.md +35 -0
  19. nullforge-0.3.1/docs/contributing/conventions.md +64 -0
  20. nullforge-0.3.1/docs/contributing/development.md +26 -0
  21. nullforge-0.3.1/docs/features/base.md +71 -0
  22. nullforge-0.3.1/docs/features/containers.md +51 -0
  23. nullforge-0.3.1/docs/features/dns.md +57 -0
  24. nullforge-0.3.1/docs/features/haproxy.md +32 -0
  25. nullforge-0.3.1/docs/features/index.md +27 -0
  26. nullforge-0.3.1/docs/features/monitoring.md +60 -0
  27. nullforge-0.3.1/docs/features/netsec.md +95 -0
  28. nullforge-0.3.1/docs/features/profiles.md +55 -0
  29. nullforge-0.3.1/docs/features/telemt.md +61 -0
  30. nullforge-0.3.1/docs/features/tor.md +29 -0
  31. nullforge-0.3.1/docs/features/users.md +54 -0
  32. nullforge-0.3.1/docs/features/warp.md +53 -0
  33. nullforge-0.3.1/docs/features/xray.md +30 -0
  34. nullforge-0.3.1/docs/features/zerotrust.md +40 -0
  35. nullforge-0.3.1/docs/getting-started/cli.md +85 -0
  36. nullforge-0.3.1/docs/getting-started/installation.md +40 -0
  37. nullforge-0.3.1/docs/getting-started/quickstart.md +75 -0
  38. nullforge-0.3.1/docs/guides/custom-runes.md +79 -0
  39. nullforge-0.3.1/docs/guides/version-pinning.md +57 -0
  40. nullforge-0.3.1/docs/index.md +55 -0
  41. nullforge-0.3.1/nullforge/__init__.py +1 -0
  42. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/inventories/example.py +3 -3
  43. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/runes/containers.py +81 -8
  44. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/runes/profiles.py +11 -9
  45. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/runes/xray.py +12 -5
  46. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/smithy/cloudflare.py +3 -2
  47. nullforge-0.3.1/nullforge/smithy/http.py +186 -0
  48. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/smithy/install.py +2 -3
  49. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/smithy/monitoring/nezha/deploy.py +2 -2
  50. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/smithy/packages.py +1 -0
  51. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/smithy/versions.py +3 -3
  52. nullforge-0.3.1/pyproject.toml +141 -0
  53. nullforge-0.3.1/tests/__init__.py +0 -0
  54. nullforge-0.3.1/tests/cli/__init__.py +0 -0
  55. nullforge-0.3.1/tests/cli/components/__init__.py +0 -0
  56. nullforge-0.3.1/tests/cli/components/completion/__init__.py +0 -0
  57. nullforge-0.3.1/tests/cli/components/completion/test_controller.py +77 -0
  58. nullforge-0.3.1/tests/cli/components/completion/test_powershell.py +97 -0
  59. nullforge-0.3.1/tests/cli/components/foundry/__init__.py +0 -0
  60. nullforge-0.3.1/tests/cli/components/foundry/test_controller.py +141 -0
  61. nullforge-0.3.1/tests/cli/conftest.py +20 -0
  62. nullforge-0.3.1/tests/cli/core/__init__.py +0 -0
  63. nullforge-0.3.1/tests/cli/core/test_application.py +120 -0
  64. nullforge-0.3.1/tests/cli/test_types.py +127 -0
  65. nullforge-0.3.1/tests/conftest.py +50 -0
  66. nullforge-0.3.1/tests/test_foundry.py +56 -0
  67. nullforge-0.3.1/tests/test_foundry_cast.py +60 -0
  68. nullforge-0.3.1/tests/test_foundry_pyinfra.py +94 -0
  69. nullforge-0.3.1/tests/test_models_dns.py +42 -0
  70. nullforge-0.3.1/tests/test_molds_dns.py +50 -0
  71. nullforge-0.3.1/tests/test_molds_monitoring.py +78 -0
  72. nullforge-0.3.1/tests/test_molds_netsec.py +44 -0
  73. nullforge-0.3.1/tests/test_molds_telemt.py +65 -0
  74. nullforge-0.3.1/tests/test_molds_user.py +21 -0
  75. nullforge-0.3.1/tests/test_molds_utils.py +177 -0
  76. nullforge-0.3.1/tests/test_runes_dns.py +19 -0
  77. nullforge-0.3.1/tests/test_runes_meta.py +163 -0
  78. nullforge-0.3.1/tests/test_runes_misc.py +33 -0
  79. nullforge-0.3.1/tests/test_runes_netsec.py +204 -0
  80. nullforge-0.3.1/tests/test_runes_profiles.py +114 -0
  81. nullforge-0.3.1/tests/test_runes_telemt.py +105 -0
  82. nullforge-0.3.1/tests/test_runes_users.py +62 -0
  83. nullforge-0.3.1/tests/test_smithy_arch.py +53 -0
  84. nullforge-0.3.1/tests/test_smithy_github.py +153 -0
  85. nullforge-0.3.1/tests/test_smithy_http.py +312 -0
  86. nullforge-0.3.1/tests/test_smithy_install.py +119 -0
  87. nullforge-0.3.1/tests/test_smithy_network.py +128 -0
  88. nullforge-0.3.1/tests/test_smithy_nezha.py +44 -0
  89. nullforge-0.3.1/tests/test_smithy_nezha_dashboard.py +84 -0
  90. nullforge-0.3.1/tests/test_smithy_packages.py +44 -0
  91. nullforge-0.3.1/tests/test_smithy_service.py +61 -0
  92. nullforge-0.3.1/tests/test_smithy_sni.py +224 -0
  93. nullforge-0.3.1/tests/test_smithy_swap.py +54 -0
  94. nullforge-0.3.1/tests/test_smithy_system.py +101 -0
  95. nullforge-0.3.1/tests/test_smithy_versions.py +178 -0
  96. nullforge-0.3.1/tests/test_templates.py +19 -0
  97. nullforge-0.3.1/tests/test_templates_telemt.py +108 -0
  98. nullforge-0.3.1/uv.lock +1234 -0
  99. nullforge-0.3.1/zensical.toml +110 -0
  100. nullforge-0.2.0/.release-please-manifest.json +0 -3
  101. nullforge-0.2.0/PKG-INFO +0 -68
  102. nullforge-0.2.0/README.md +0 -52
  103. nullforge-0.2.0/nullforge/__init__.py +0 -1
  104. nullforge-0.2.0/nullforge/smithy/http.py +0 -85
  105. nullforge-0.2.0/pyproject.toml +0 -45
  106. nullforge-0.2.0/uv.lock +0 -662
  107. {nullforge-0.2.0 → nullforge-0.3.1}/.gitattributes +0 -0
  108. {nullforge-0.2.0 → nullforge-0.3.1}/.github/CODEOWNERS +0 -0
  109. {nullforge-0.2.0 → nullforge-0.3.1}/.github/ISSUE_TEMPLATE/BUG-REPORT.yaml +0 -0
  110. {nullforge-0.2.0 → nullforge-0.3.1}/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yaml +0 -0
  111. {nullforge-0.2.0 → nullforge-0.3.1}/.github/ISSUE_TEMPLATE/config.yaml +0 -0
  112. {nullforge-0.2.0 → nullforge-0.3.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  113. {nullforge-0.2.0 → nullforge-0.3.1}/.github/labels.yaml +0 -0
  114. {nullforge-0.2.0 → nullforge-0.3.1}/.github/release.yaml +0 -0
  115. {nullforge-0.2.0 → nullforge-0.3.1}/.github/workflows/check-commit-message-pr.yaml +0 -0
  116. {nullforge-0.2.0 → nullforge-0.3.1}/.github/workflows/label-prs.yaml +0 -0
  117. {nullforge-0.2.0 → nullforge-0.3.1}/.github/workflows/labels.yaml +0 -0
  118. {nullforge-0.2.0 → nullforge-0.3.1}/.gitignore +0 -0
  119. {nullforge-0.2.0 → nullforge-0.3.1}/CODE_OF_CONDUCT.md +0 -0
  120. {nullforge-0.2.0 → nullforge-0.3.1}/LICENSE +0 -0
  121. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/cli/__init__.py +0 -0
  122. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/cli/__main__.py +0 -0
  123. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/cli/app.py +0 -0
  124. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/cli/components/__init__.py +0 -0
  125. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/cli/components/completion/__init__.py +0 -0
  126. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/cli/components/completion/component.py +0 -0
  127. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/cli/components/completion/controller.py +0 -0
  128. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/cli/components/completion/errors.py +0 -0
  129. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/cli/components/completion/powershell.py +0 -0
  130. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/cli/components/foundry/__init__.py +0 -0
  131. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/cli/components/foundry/component.py +0 -0
  132. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/cli/components/foundry/controller.py +0 -0
  133. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/cli/components/foundry/errors.py +0 -0
  134. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/cli/components/runes/__init__.py +0 -0
  135. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/cli/components/runes/component.py +0 -0
  136. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/cli/components/runes/controller.py +0 -0
  137. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/cli/core/__init__.py +0 -0
  138. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/cli/core/application.py +0 -0
  139. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/cli/core/component.py +0 -0
  140. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/cli/core/controller.py +0 -0
  141. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/cli/core/errors.py +0 -0
  142. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/cli/core/types.py +0 -0
  143. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/cli/display.py +0 -0
  144. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/cli/types.py +0 -0
  145. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/foundry/README.md +0 -0
  146. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/foundry/__init__.py +0 -0
  147. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/foundry/_pyinfra.py +0 -0
  148. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/foundry/cast.py +0 -0
  149. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/foundry/full_cast.py +0 -0
  150. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/inventories/README.md +0 -0
  151. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/models/__init__.py +0 -0
  152. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/models/containers.py +0 -0
  153. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/models/dns.py +0 -0
  154. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/models/monitoring/__init__.py +0 -0
  155. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/models/monitoring/base.py +0 -0
  156. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/models/monitoring/nezha.py +0 -0
  157. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/models/netsec.py +0 -0
  158. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/models/profiles.py +0 -0
  159. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/models/system.py +0 -0
  160. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/models/users.py +0 -0
  161. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/models/warp.py +0 -0
  162. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/models/zerotrust.py +0 -0
  163. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/molds/__init__.py +0 -0
  164. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/molds/base_mold.py +0 -0
  165. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/molds/containers.py +0 -0
  166. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/molds/defaults.py +0 -0
  167. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/molds/dns.py +0 -0
  168. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/molds/features.py +1 -1
  169. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/molds/haproxy.py +0 -0
  170. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/molds/monitoring/__init__.py +0 -0
  171. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/molds/monitoring/nezha.py +0 -0
  172. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/molds/netsec.py +0 -0
  173. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/molds/profiles.py +0 -0
  174. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/molds/system.py +0 -0
  175. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/molds/telemt.py +0 -0
  176. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/molds/tor.py +0 -0
  177. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/molds/user.py +0 -0
  178. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/molds/utils.py +0 -0
  179. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/molds/warp.py +0 -0
  180. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/molds/xray.py +0 -0
  181. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/molds/zerotrust.py +0 -0
  182. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/runes/__init__.py +0 -0
  183. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/runes/base.py +0 -0
  184. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/runes/dns.py +0 -0
  185. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/runes/haproxy.py +0 -0
  186. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/runes/monitoring.py +0 -0
  187. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/runes/netsec.py +0 -0
  188. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/runes/prepare.py +0 -0
  189. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/runes/telemt.py +0 -0
  190. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/runes/tor.py +0 -0
  191. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/runes/users.py +0 -0
  192. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/runes/warp.py +0 -0
  193. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/runes/zerotrust.py +0 -0
  194. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/smithy/__init__.py +0 -0
  195. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/smithy/admin.py +0 -0
  196. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/smithy/arch.py +0 -0
  197. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/smithy/blocky.py +0 -0
  198. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/smithy/github.py +0 -0
  199. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/smithy/monitoring/__init__.py +0 -0
  200. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/smithy/monitoring/nezha/__init__.py +0 -0
  201. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/smithy/monitoring/nezha/agent.py +0 -0
  202. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/smithy/monitoring/nezha/dashboard.py +0 -0
  203. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/smithy/network.py +0 -0
  204. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/smithy/service.py +0 -0
  205. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/smithy/sni.py +0 -0
  206. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/smithy/swap.py +0 -0
  207. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/smithy/system.py +0 -0
  208. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/templates/__init__.py +0 -0
  209. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/templates/cloudflared/tunnel.yml.j2 +0 -0
  210. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/templates/dns/blocky.yaml.j2 +0 -0
  211. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/templates/dns/dns.yaml.j2 +0 -0
  212. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/templates/dns/resolv.conf.j2 +0 -0
  213. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/templates/dns/resolved.conf.j2 +0 -0
  214. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/templates/etc/default/zramswap.j2 +0 -0
  215. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/templates/nvim/nvim_patch.lua.j2 +0 -0
  216. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/templates/profiles/direnv.toml +0 -0
  217. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/templates/profiles/starship.toml +0 -0
  218. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/templates/profiles/tmux.conf +0 -0
  219. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/templates/profiles/zshrc.j2 +0 -0
  220. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/templates/scripts/telemt-synfix.sh +0 -0
  221. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/templates/scripts/teleproxy-warp.sh +0 -0
  222. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/templates/scripts/warp-v6-policy.sh +0 -0
  223. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/templates/scripts/zt-tunnel-warp.sh +0 -0
  224. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/templates/systemd/blocky.service.j2 +0 -0
  225. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/templates/systemd/cloudflare-tunnel.service.j2 +0 -0
  226. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/templates/systemd/cloudflare-warp.service.j2 +0 -0
  227. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/templates/systemd/dns-internal.service.j2 +0 -0
  228. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/templates/systemd/telemt.service.j2 +0 -0
  229. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/templates/telemt/telemt.toml.j2 +0 -0
  230. {nullforge-0.2.0 → nullforge-0.3.1}/nullforge/templates/tor/torrc.j2 +0 -0
  231. {nullforge-0.2.0 → nullforge-0.3.1}/release-please-config.json +0 -0
@@ -85,6 +85,40 @@ If any hooks fail, please fix the issues before committing. You can manually run
85
85
  prek run --all-files
86
86
  ```
87
87
 
88
+ ### Tasks (via poe)
89
+
90
+ Every routine check, build and test step is a [poe](https://poethepoet.natn.io/) task.
91
+ Tasks run through poe's `uv` executor, so they resolve their own environment - no `uv run --group ...` prefix needed.
92
+ The bare name is the variant CI runs; a colon suffix selects a mode (`:fix` for auto-fix, `:dev` for the local loop):
93
+
94
+ ```bash
95
+ uv sync # create the environment
96
+ uv run poe check # format + lint + typecheck + lint-imports + deptry (what CI runs)
97
+ uv run poe format # ruff format --check
98
+ uv run poe format:fix # ruff format
99
+ uv run poe lint # ruff check
100
+ uv run poe lint:fix # ruff check --fix
101
+ uv run poe lint:md # markdownlint over all Markdown (via prek)
102
+ uv run poe typecheck # ty check
103
+ uv run poe lint-imports # import-linter layer contracts
104
+ uv run poe deptry # dependency hygiene
105
+ uv run poe tests # pytest with coverage
106
+ uv run poe docs # strict docs site build (what docs CI runs)
107
+ uv run poe docs:dev # serve the docs site with live reload
108
+ ```
109
+
110
+ Run `uv run poe check` and `uv run poe tests` before pushing - they are the same checks the pull request runs.
111
+ Markdown is not in `poe check` - the prek hook lints it on every commit and in CI; `uv run poe lint:md` runs it on demand.
112
+ `poe check` does not build the documentation site either, so run `uv run poe docs` when the change touches `docs/`.
113
+
114
+ ### Tests
115
+
116
+ `uv run poe tests` runs the suite; CI runs it on Python 3.13, 3.14 and 3.15.
117
+ `tests/conftest.py` patches the pyinfra context so rune and operation calls are no-ops - molds, smithy helpers, runes, and templates unit-test without a real target.
118
+ End-to-end correctness is still validated by deploying to a real host.
119
+
120
+ The suite also enforces the [conventions](https://wlix13.github.io/NullForge/contributing/conventions/).
121
+
88
122
  ## Commits
89
123
 
90
124
  All commits are expected to follow the conventional commits specification.
@@ -113,8 +147,7 @@ message — please replace that with something descriptive. Examples:
113
147
  - `Improve CI/CD caching and test matrix`
114
148
  - `Fix swap sizing on small hosts`
115
149
 
116
- A good title reads cleanly in the PR list and the project history. The individual
117
- **commits** inside the PR still follow Conventional Commits (see [Commits](#commits));
150
+ The individual **commits** inside the PR still follow Conventional Commits (see [Commits](#commits));
118
151
  because PRs are merged with a merge/rebase strategy, those commit messages — not the
119
152
  title — drive the release changelog, so the title itself does not need a `type:` prefix.
120
153
 
@@ -3,7 +3,8 @@ updates:
3
3
  - package-ecosystem: "github-actions"
4
4
  directory: "/"
5
5
  schedule:
6
- interval: "weekly"
6
+ interval: "cron"
7
+ cronjob: "0 6 1,15 * *" # twice a month, closest dependabot gets to every two weeks
7
8
  commit-message:
8
9
  prefix: "chore"
9
10
  include: "scope"
@@ -17,7 +18,8 @@ updates:
17
18
  - package-ecosystem: "uv"
18
19
  directory: "/"
19
20
  schedule:
20
- interval: "weekly"
21
+ interval: "cron"
22
+ cronjob: "0 6 1,15 * *" # twice a month, closest dependabot gets to every two weeks
21
23
  commit-message:
22
24
  prefix: "chore"
23
25
  include: "scope"
@@ -31,7 +33,8 @@ updates:
31
33
  - package-ecosystem: "pre-commit"
32
34
  directory: "/"
33
35
  schedule:
34
- interval: "weekly"
36
+ interval: "cron"
37
+ cronjob: "0 6 1,15 * *" # twice a month, closest dependabot gets to every two weeks
35
38
  commit-message:
36
39
  prefix: "chore"
37
40
  include: "scope"
@@ -19,9 +19,11 @@ labels:
19
19
  files:
20
20
  - '^README\.md$'
21
21
  - "^docs/"
22
+ - '^zensical\.toml$'
22
23
  - label: "ci-cd"
23
24
  files:
24
25
  - '^\.github/'
26
+ - '^\.markdownlint-cli2\.jsonc$'
25
27
  - label: "tests"
26
28
  files:
27
29
  - "^tests/"
@@ -25,21 +25,13 @@ jobs:
25
25
  with:
26
26
  persist-credentials: false
27
27
 
28
- - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
28
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
29
29
  with:
30
30
  enable-cache: true
31
31
  cache-dependency-glob: uv.lock
32
32
 
33
- - run: uv sync --all-groups
34
-
35
- - name: Check formatting
36
- run: uv run ruff format --check .
37
-
38
- - name: Lint code
39
- run: uv run ruff check .
40
-
41
- - name: Type check
42
- run: uv run ty check
33
+ - name: Run quality checks
34
+ run: uv run poe check
43
35
 
44
36
  hooks:
45
37
  name: 🪝 prek hooks
@@ -50,7 +42,7 @@ jobs:
50
42
  with:
51
43
  persist-credentials: false
52
44
 
53
- - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
45
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
54
46
  with:
55
47
  enable-cache: true
56
48
  cache-dependency-glob: uv.lock
@@ -0,0 +1,37 @@
1
+ name: 📚 Docs build check
2
+
3
+ on:
4
+ pull_request:
5
+ branches:
6
+ - main
7
+ paths:
8
+ - "docs/**"
9
+ - "zensical.toml"
10
+ - "pyproject.toml"
11
+ - "uv.lock"
12
+ - ".github/workflows/ci-docs.yaml"
13
+
14
+ permissions:
15
+ contents: read
16
+
17
+ concurrency:
18
+ group: ${{ github.workflow }}-${{ github.ref }}
19
+ cancel-in-progress: true
20
+
21
+ jobs:
22
+ build:
23
+ name: 📚 Build documentation
24
+ runs-on: ubuntu-latest
25
+ timeout-minutes: 10
26
+ steps:
27
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
28
+ with:
29
+ persist-credentials: false
30
+
31
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
32
+ with:
33
+ enable-cache: true
34
+ cache-dependency-glob: uv.lock
35
+
36
+ - name: Build documentation
37
+ run: uv run poe docs
@@ -17,22 +17,27 @@ concurrency:
17
17
 
18
18
  jobs:
19
19
  tests:
20
- name: 🧪 Tests
20
+ name: 🧪 Tests (py${{ matrix.python-version }})
21
21
  runs-on: ubuntu-latest
22
22
  timeout-minutes: 15
23
+ strategy:
24
+ fail-fast: false
25
+ matrix:
26
+ python-version: ["3.13", "3.14", "3.15"]
23
27
  steps:
24
28
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
25
29
  with:
26
30
  persist-credentials: false
27
31
 
28
- - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
32
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
29
33
  with:
30
34
  enable-cache: true
31
35
  cache-dependency-glob: uv.lock
32
- python-version: "3.13"
36
+ cache-suffix: py${{ matrix.python-version }}
37
+ python-version: ${{ matrix.python-version }}
33
38
 
34
- - name: Run tests with coverage
35
- run: uv run --group tests pytest
39
+ - name: Run tests
40
+ run: uv run poe tests
36
41
 
37
42
  build:
38
43
  name: 📦 Package build check
@@ -43,7 +48,7 @@ jobs:
43
48
  with:
44
49
  persist-credentials: false
45
50
 
46
- - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
51
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
47
52
  with:
48
53
  enable-cache: true
49
54
  cache-dependency-glob: uv.lock
@@ -0,0 +1,62 @@
1
+ name: 📚 Deploy documentation
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ paths:
8
+ - "docs/**"
9
+ - "zensical.toml"
10
+ - "pyproject.toml"
11
+ - "uv.lock"
12
+ - ".github/workflows/deploy-docs.yaml"
13
+ workflow_dispatch:
14
+
15
+ concurrency:
16
+ group: pages
17
+ cancel-in-progress: false
18
+
19
+ permissions:
20
+ contents: read
21
+
22
+ jobs:
23
+ build:
24
+ name: 📚 Build documentation
25
+ runs-on: ubuntu-latest
26
+ timeout-minutes: 10
27
+ permissions:
28
+ contents: read
29
+ pages: read
30
+ steps:
31
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
32
+ with:
33
+ persist-credentials: false
34
+
35
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
36
+ with:
37
+ enable-cache: true
38
+ cache-dependency-glob: uv.lock
39
+
40
+ - name: Build documentation
41
+ run: uv run poe docs
42
+
43
+ - uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
44
+
45
+ - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
46
+ with:
47
+ path: site
48
+
49
+ deploy:
50
+ name: 🚀 Deploy to GitHub Pages
51
+ needs: build
52
+ runs-on: ubuntu-latest
53
+ timeout-minutes: 10
54
+ permissions:
55
+ pages: write
56
+ id-token: write
57
+ environment:
58
+ name: github-pages
59
+ url: ${{ steps.deployment.outputs.page_url }}
60
+ steps:
61
+ - id: deployment
62
+ uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
@@ -52,7 +52,7 @@ jobs:
52
52
  with:
53
53
  persist-credentials: false
54
54
 
55
- - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
55
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
56
56
  with:
57
57
  enable-cache: false
58
58
 
@@ -95,7 +95,7 @@ jobs:
95
95
  path: dist/
96
96
 
97
97
  - name: Generate artifact attestations
98
- uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
98
+ uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
99
99
  with:
100
100
  subject-path: dist/*
101
101
 
@@ -111,7 +111,9 @@ jobs:
111
111
  permissions:
112
112
  id-token: write
113
113
  steps:
114
- - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
114
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
115
+ with:
116
+ enable-cache: false
115
117
 
116
118
  - name: Download distribution artifacts
117
119
  uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
@@ -122,6 +124,38 @@ jobs:
122
124
  - name: Publish to PyPI
123
125
  run: uv publish --trusted-publishing always
124
126
 
127
+ publish-photon:
128
+ name: 📤 Publish to photon-manifest
129
+ needs: [release-please, build, attest-artifacts]
130
+ if: needs.release-please.outputs.release_created == 'true'
131
+ runs-on: ubuntu-latest
132
+ timeout-minutes: 10
133
+ environment:
134
+ name: photon
135
+ url: https://py.wlix13.dev/simple/nullforge/
136
+ permissions:
137
+ contents: read
138
+ steps:
139
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
140
+ with:
141
+ enable-cache: false
142
+
143
+ - name: Download distribution artifacts
144
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
145
+ with:
146
+ name: python-package-distributions
147
+ path: dist/
148
+
149
+ - name: Publish to photon-manifest
150
+ env:
151
+ UV_PUBLISH_TOKEN: ${{ secrets.PHOTON_PUBLISH_TOKEN }}
152
+ run: |
153
+ uv auth login https://py.wlix13.dev/simple/ --token "$UV_PUBLISH_TOKEN"
154
+ uv publish \
155
+ --publish-url https://py.wlix13.dev/legacy/ \
156
+ --check-url https://py.wlix13.dev/simple/ \
157
+ --no-attestations
158
+
125
159
  finalize-release:
126
160
  name: 🚀 Attach assets and publish
127
161
  needs: [release-please, build, attest-artifacts, publish-pypi]
@@ -0,0 +1,25 @@
1
+ {
2
+ "gitignore": true,
3
+
4
+ // Generated, boilerplate and vendored - not authored to this style.
5
+ "ignores": [
6
+ "CHANGELOG.md",
7
+ ".github/PULL_REQUEST_TEMPLATE.md",
8
+ ".github/ISSUE_TEMPLATE/**",
9
+ ],
10
+
11
+ "config": {
12
+ "default": true,
13
+
14
+ // Prose is one sentence per line; tables pad cells but not the delimiter row.
15
+ "MD013": false,
16
+ "MD060": false,
17
+
18
+ "MD046": false,
19
+
20
+ // mkdocs admonition blocks
21
+ "MD033": {
22
+ "allowed_elements": ["div"],
23
+ },
24
+ },
25
+ }
@@ -5,7 +5,7 @@ default_install_hook_types:
5
5
  - post-rewrite
6
6
  repos:
7
7
  - repo: https://github.com/tox-dev/pyproject-fmt
8
- rev: v2.25.3
8
+ rev: v2.29.4
9
9
  hooks:
10
10
  - id: pyproject-fmt
11
11
 
@@ -19,12 +19,12 @@ repos:
19
19
  - id: end-of-file-fixer
20
20
 
21
21
  - repo: https://github.com/astral-sh/uv-pre-commit
22
- rev: 0.11.32
22
+ rev: 0.12.14
23
23
  hooks:
24
24
  - id: uv-lock
25
25
 
26
26
  - repo: https://github.com/astral-sh/ruff-pre-commit
27
- rev: v0.16.0
27
+ rev: v0.16.7
28
28
  hooks:
29
29
  - id: ruff-check
30
30
  args: [--fix]
@@ -40,3 +40,8 @@ repos:
40
40
  - id: python-no-log-warn
41
41
  - id: python-use-type-annotations
42
42
  - id: text-unicode-replacement-char
43
+
44
+ - repo: https://github.com/DavidAnson/markdownlint-cli2
45
+ rev: v0.23.2
46
+ hooks:
47
+ - id: markdownlint-cli2
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.3.1"
3
+ }
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.1](https://github.com/wlix13/NullForge/compare/v0.3.0...v0.3.1) (2026-09-18)
4
+
5
+
6
+ ### Features
7
+
8
+ * **runes:** enhance Podman support with rootless prerequisites and auto-updater ([9c02ab7](https://github.com/wlix13/NullForge/commit/9c02ab7de2196149c500ac6aa3ddd457896af31b))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **docs:** cut repeated and internal detail from docs and contributing guide ([21741ef](https://github.com/wlix13/NullForge/commit/21741efe1a785fc5c3fe3b22b58aaefff43f8e70))
14
+ * **inventories:** keep example presets out of pyinfra group discovery ([73578d6](https://github.com/wlix13/NullForge/commit/73578d65f7ceef892491905cc1c670c2e92f7a04))
15
+
16
+ ## [0.3.0](https://github.com/wlix13/NullForge/compare/v0.2.0...v0.3.0) (2026-08-28)
17
+
18
+
19
+ ### ⚠ BREAKING CHANGES
20
+
21
+ * **molds:** deploy warp before other features
22
+ * **smithy:** fall back to WARP interface for filtered downloads
23
+
24
+ ### Features
25
+
26
+ * **molds:** deploy warp before other features ([a97cbfe](https://github.com/wlix13/NullForge/commit/a97cbfe9020bf0708acec4adf370cfa7113ea964))
27
+ * **smithy:** fall back to WARP interface for filtered downloads ([25f0b28](https://github.com/wlix13/NullForge/commit/25f0b28cb7d0efb89cf7710aa922d52a9651c985))
28
+
29
+
30
+ ### Documentation
31
+
32
+ * describe WARP download fallback ([8b57441](https://github.com/wlix13/NullForge/commit/8b57441c8ecc234db4f9131e5ab9a603e3384b4c))
33
+ * **site:** add documentation ([d6b8575](https://github.com/wlix13/NullForge/commit/d6b857541d8ed35a5fe8fa3a32cdef95d8d8bf8c))
34
+
3
35
  ## [0.2.0](https://github.com/wlix13/NullForge/compare/v0.1.0...v0.2.0) (2026-07-25)
4
36
 
5
37
 
@@ -0,0 +1,48 @@
1
+ Metadata-Version: 2.5
2
+ Name: nullforge
3
+ Version: 0.3.1
4
+ Summary: Forge the server's baseline from null
5
+ Author-email: wlix13 <github@wlix13.dev>
6
+ Maintainer-email: wlix13 <github@wlix13.dev>
7
+ License-File: LICENSE
8
+ Classifier: Programming Language :: Python :: 3 :: Only
9
+ Classifier: Programming Language :: Python :: 3.13
10
+ Classifier: Programming Language :: Python :: 3.14
11
+ Classifier: Programming Language :: Python :: 3.15
12
+ Requires-Python: >=3.13
13
+ Requires-Dist: click
14
+ Requires-Dist: pydantic
15
+ Requires-Dist: pyinfra
16
+ Requires-Dist: rich
17
+ Requires-Dist: rich-click
18
+ Description-Content-Type: text/markdown
19
+
20
+ # NullForge
21
+
22
+ [![PyPI](https://img.shields.io/pypi/v/nullforge?logo=pypi&logoColor=white)](https://pypi.org/project/nullforge/)
23
+ ![Python](https://img.shields.io/badge/python-3.13%20%7C%203.14%20%7C%203.15-blue?logo=python&logoColor=white)
24
+ ![Build](https://img.shields.io/github/actions/workflow/status/wlix13/NullForge/ci-tests.yaml?label=build&logo=github)
25
+ ![Lint](https://img.shields.io/github/actions/workflow/status/wlix13/NullForge/ci-code-quality.yaml?label=lint&logo=github)
26
+ ![License](https://img.shields.io/badge/license-MIT-green)
27
+ ![uv](https://img.shields.io/badge/package%20manager-uv-blueviolet?logo=astral)
28
+ ![Ruff](https://img.shields.io/badge/linter-ruff-orange?logo=ruff)
29
+ [![Docs](https://img.shields.io/badge/docs-GitHub%20Pages-0075ca?logo=materialformkdocs&logoColor=white)](https://wlix13.github.io/NullForge/)
30
+
31
+ Forge the server's baseline from null - an infrastructure-as-code framework built on [pyinfra](https://pyinfra.com), themed around a blacksmith's forge: **inventories** define hosts, **molds** shape the configuration, **runes** are idempotent operation sets, the **foundry** casts them onto targets, and the **smithy** holds cross-distro helpers.
32
+
33
+ > [!WARNING]
34
+ > **NullForge is in active development.** Until the `v1.0.0` release, the CLI, mold schemas and deploy behaviour may change at any time - breaking changes can land in **any** release, including patch versions. Pin an exact version (e.g. `nullforge==0.2.0`) and check the [release notes](https://github.com/wlix13/NullForge/releases) before upgrading.
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ uv tool install nullforge
40
+ ```
41
+
42
+ ## Documentation
43
+
44
+ Everything else - getting started, concepts, the full feature reference, and guides - lives at **[wlix13.github.io/NullForge](https://wlix13.github.io/NullForge/)**.
45
+
46
+ ## Contributing
47
+
48
+ See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for the full setup, commit conventions and pull request flow.
@@ -0,0 +1,29 @@
1
+ # NullForge
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/nullforge?logo=pypi&logoColor=white)](https://pypi.org/project/nullforge/)
4
+ ![Python](https://img.shields.io/badge/python-3.13%20%7C%203.14%20%7C%203.15-blue?logo=python&logoColor=white)
5
+ ![Build](https://img.shields.io/github/actions/workflow/status/wlix13/NullForge/ci-tests.yaml?label=build&logo=github)
6
+ ![Lint](https://img.shields.io/github/actions/workflow/status/wlix13/NullForge/ci-code-quality.yaml?label=lint&logo=github)
7
+ ![License](https://img.shields.io/badge/license-MIT-green)
8
+ ![uv](https://img.shields.io/badge/package%20manager-uv-blueviolet?logo=astral)
9
+ ![Ruff](https://img.shields.io/badge/linter-ruff-orange?logo=ruff)
10
+ [![Docs](https://img.shields.io/badge/docs-GitHub%20Pages-0075ca?logo=materialformkdocs&logoColor=white)](https://wlix13.github.io/NullForge/)
11
+
12
+ Forge the server's baseline from null - an infrastructure-as-code framework built on [pyinfra](https://pyinfra.com), themed around a blacksmith's forge: **inventories** define hosts, **molds** shape the configuration, **runes** are idempotent operation sets, the **foundry** casts them onto targets, and the **smithy** holds cross-distro helpers.
13
+
14
+ > [!WARNING]
15
+ > **NullForge is in active development.** Until the `v1.0.0` release, the CLI, mold schemas and deploy behaviour may change at any time - breaking changes can land in **any** release, including patch versions. Pin an exact version (e.g. `nullforge==0.2.0`) and check the [release notes](https://github.com/wlix13/NullForge/releases) before upgrading.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ uv tool install nullforge
21
+ ```
22
+
23
+ ## Documentation
24
+
25
+ Everything else - getting started, concepts, the full feature reference, and guides - lives at **[wlix13.github.io/NullForge](https://wlix13.github.io/NullForge/)**.
26
+
27
+ ## Contributing
28
+
29
+ See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for the full setup, commit conventions and pull request flow.
@@ -0,0 +1,61 @@
1
+ # Architecture
2
+
3
+ NullForge is a thin, opinionated layer over pyinfra.
4
+ Every layer has one job, and imports only flow downward.
5
+
6
+ ## Data flow
7
+
8
+ ```mermaid
9
+ flowchart TB
10
+ subgraph plan [Control node]
11
+ CLI[cli - nullforge cast] --> FY["foundry - full_cast.py / cast.py"]
12
+ INV[inventories] --> FY
13
+ FY --> RN["runes/*.py"]
14
+ RN --> SM[smithy]
15
+ RN --> TP["templates (Jinja2)"]
16
+ MD["molds (Pydantic)"] --> INV
17
+ MD --> RN
18
+ ML["models (domain types)"] --> MD
19
+ end
20
+ RN -->|pyinfra operations over SSH| HOSTS[(Target hosts)]
21
+ ```
22
+
23
+ 1. **Inventories** define hosts and attach `system` and `features` data, built by [merging layers](inventories.md) over the defaults.
24
+ 2. The **foundry** is the deploy entry point.
25
+ `full_cast.py` coerces inventory data through the molds, always includes `prepare` then `base`, and then includes the rune of every active feature.
26
+ `cast.py` is the selective variant used by `nullforge cast -r ...`.
27
+ 3. **Runes** are self-contained pyinfra operation sets - one file per concern.
28
+ They read validated configuration from `host.data` and emit idempotent operations.
29
+ 4. **Molds** are Pydantic schemas for all configuration; [`FeaturesMold`](molds.md) composes the per-feature sub-molds.
30
+ 5. **Models** hold pure domain types and constants consumed by molds.
31
+ 6. The **smithy** provides cross-distro abstractions: package-name mapping (apt/dnf), release-binary installs with checksum verification, version pinning, networking facts, swap, service users.
32
+ 7. **Templates** are Jinja2 files for systemd units, service configs, and shell profiles.
33
+
34
+ ## Layer contracts
35
+
36
+ The layering is enforced by import contracts:
37
+
38
+ | Contract | Meaning |
39
+ | --- | --- |
40
+ | Deploy spine | `cli` -> `foundry` -> `runes` -> `smithy` -> `molds`/`templates` -> `models`; a layer may import only layers below it |
41
+ | Models are pure | `models` imports no other NullForge package |
42
+ | Templates are a leaf | `templates` imports no other NullForge package, `models` included - its spine position only says which layers may import *it* |
43
+ | Molds describe, never provision | `molds` cannot import runes, smithy, or templates |
44
+ | Runes are independent | no rune imports another rune |
45
+
46
+ Why rune independence matters - and how runes coordinate without it - is covered in [Runes](runes.md#independence).
47
+
48
+ ## Execution model
49
+
50
+ pyinfra runs in two phases:
51
+
52
+ 1. **Plan** - fact gathering and operation collection on the control node.
53
+ Python-level branching (`if host.get_fact(...)`) happens here.
54
+ 2. **Execute** - the collected operations run against each host, in a deterministic order shared by all hosts.
55
+
56
+ The [conventions](../contributing/conventions.md) around `host.loop` and change detection exist to keep that shared ordering stable.
57
+
58
+ ## The CLI wrapper
59
+
60
+ `nullforge cast` is a thin planner around pyinfra: it resolves the [cast stages](../getting-started/cli.md#stages) and hands them to pyinfra.
61
+ Everything else is stock pyinfra, which is why unknown CLI options are proxied through verbatim.