create-win-project 1.3.0 → 2.0.0

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 (289) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +58 -160
  3. package/checks/check-compatibility.js +95 -0
  4. package/{scripts/verify-generated.mjs → checks/check-generated-project.js} +28 -4
  5. package/{scripts/validate-content.mjs → checks/check-library.js} +17 -7
  6. package/checks/check-package.js +15 -0
  7. package/checks/classify-changes.js +29 -0
  8. package/checks/run-compatibility-shard.js +21 -0
  9. package/docs/README.md +20 -0
  10. package/docs/capabilities.md +13 -0
  11. package/docs/compatibility.md +13 -0
  12. package/docs/generated-project.md +12 -0
  13. package/docs/getting-started.md +26 -0
  14. package/docs/maintainers/architecture.md +161 -0
  15. package/docs/maintainers/ci-strategy.md +46 -0
  16. package/docs/{CONTENT_MODEL.md → maintainers/content-model.md} +3 -3
  17. package/docs/maintainers/contributing.md +154 -0
  18. package/docs/{DEPENDENCY_MAINTENANCE.md → maintainers/dependencies.md} +1 -1
  19. package/docs/migration-v2.md +13 -0
  20. package/docs/production-contract.md +24 -0
  21. package/index.js +2 -429
  22. package/{playbooks → library}/INDEX.md +5 -5
  23. package/library/compatibility-impact.json +14 -0
  24. package/{playbooks/capabilities/ci/github-actions.manifest.json → library/development-tools/ci/definition.json} +1 -0
  25. package/library/development-tools/devops/makefile/commands.md +11 -0
  26. package/library/development-tools/devops/makefile/definition.json +11 -0
  27. package/library/development-tools/devops/makefile/docker.md +9 -0
  28. package/library/development-tools/devops/makefile/validation.md +5 -0
  29. package/{playbooks/devops/pr-template.manifest.json → library/development-tools/devops/pr-template/definition.json} +1 -0
  30. package/library/features/sqlalchemy-alembic.md +17 -0
  31. package/library/optional-features/concerns/zod/errors.md +5 -0
  32. package/library/optional-features/concerns/zod/testing.md +5 -0
  33. package/library/optional-features/concerns/zod/validation.md +21 -0
  34. package/library/optional-features/styling/css-modules/definition.json +12 -0
  35. package/library/optional-features/styling/css-modules/responsive.md +5 -0
  36. package/library/optional-features/styling/css-modules/theme.md +7 -0
  37. package/{playbooks/platform/mobile.manifest.json → library/platforms/mobile/definition.json} +1 -0
  38. package/{playbooks/platform/web.manifest.json → library/platforms/web/definition.json} +1 -0
  39. package/{playbooks/stack/react-native.manifest.json → library/stacks/expo/definition.json} +2 -2
  40. package/library/stacks/fastapi/architecture.md +40 -0
  41. package/library/stacks/fastapi/definition.json +39 -0
  42. package/library/stacks/fastapi/runtime.md +25 -0
  43. package/library/stacks/fastapi/security.md +26 -0
  44. package/library/stacks/fastapi/structure.md +27 -0
  45. package/library/stacks/fastapi/testing.md +23 -0
  46. package/{playbooks/stack/nextjs.manifest.json → library/stacks/nextjs/definition.json} +2 -2
  47. package/{playbooks/stack/no-frontend.manifest.json → library/stacks/no-frontend/definition.json} +1 -1
  48. package/{playbooks/stack/react-vite.manifest.json → library/stacks/react-vite/definition.json} +2 -2
  49. package/{compatibility/profiles.json → library/tested-versions.json} +46 -2
  50. package/{playbooks/universal/accessibility.manifest.json → library/universal/accessibility/definition.json} +1 -1
  51. package/library/universal/coding-rules/definition.json +13 -0
  52. package/library/universal/coding-rules/hygiene.md +9 -0
  53. package/library/universal/coding-rules/naming.md +17 -0
  54. package/{playbooks/universal/error-handling.manifest.json → library/universal/error-handling/definition.json} +1 -0
  55. package/library/universal/git-conventions/branches.md +5 -0
  56. package/library/universal/git-conventions/commits.md +7 -0
  57. package/library/universal/git-conventions/definition.json +13 -0
  58. package/library/universal/git-conventions/workflow.md +5 -0
  59. package/{playbooks/universal/observability.manifest.json → library/universal/observability/definition.json} +1 -0
  60. package/{playbooks/universal/security.manifest.json → library/universal/security/definition.json} +1 -0
  61. package/library/universal/typescript/boundaries.md +13 -0
  62. package/library/universal/typescript/definition.json +12 -0
  63. package/library/universal/typescript/errors.md +5 -0
  64. package/library/universal/typescript/patterns.md +7 -0
  65. package/package.json +14 -15
  66. package/public/logo.svg +19 -0
  67. package/src/cli/arguments.js +63 -0
  68. package/src/cli/display.js +220 -0
  69. package/src/cli/main.js +203 -0
  70. package/src/cli/navigation.js +156 -0
  71. package/src/cli/questions.js +188 -0
  72. package/src/cli/system-check.js +158 -0
  73. package/src/engine/create-project.js +10 -0
  74. package/src/engine/install-dependencies.js +13 -0
  75. package/{lib/catalog.js → src/engine/load-library.js} +8 -8
  76. package/{lib/files.js → src/engine/project-files.js} +10 -1
  77. package/{lib/playbooks.js → src/engine/project-guidance.js} +26 -3
  78. package/{lib/application-shapes.js → src/engine/project-shapes.js} +4 -4
  79. package/{lib/template.js → src/engine/render-templates.js} +11 -0
  80. package/{lib/compatibility.js → src/engine/tested-versions.js} +21 -2
  81. package/src/engine/upgrade-report.js +20 -0
  82. package/src/engine/write-files.js +34 -0
  83. package/{lib/stacks/registry.js → src/stacks/available-stacks.js} +25 -1
  84. package/src/stacks/backends/README.md +5 -0
  85. package/src/stacks/backends/fastapi/ci.js +3 -0
  86. package/src/stacks/backends/fastapi/create-files.js +874 -0
  87. package/src/stacks/backends/fastapi/docker.js +75 -0
  88. package/src/stacks/backends/fastapi/environment.js +3 -0
  89. package/src/stacks/backends/fastapi/index.js +32 -0
  90. package/{lib/stacks → src/stacks/backends}/laravel/architecture.js +1 -0
  91. package/{lib/stacks → src/stacks/backends}/laravel/auth/oidc.js +1 -0
  92. package/{lib/stacks → src/stacks/backends}/laravel/auth/public.js +1 -0
  93. package/{lib/stacks → src/stacks/backends}/laravel/auth/sanctum.js +1 -0
  94. package/{lib/stacks → src/stacks/backends}/laravel/auth/session.js +1 -0
  95. package/src/stacks/backends/laravel/ci.js +3 -0
  96. package/{lib/stacks → src/stacks/backends}/laravel/composer.js +1 -1
  97. package/{lib/stacks/laravel/generate.js → src/stacks/backends/laravel/create-files.js} +1 -0
  98. package/src/stacks/backends/laravel/docker.js +14 -0
  99. package/src/stacks/backends/laravel/environment.js +5 -0
  100. package/{lib/stacks → src/stacks/backends}/laravel/index.js +11 -3
  101. package/{lib/stacks → src/stacks/backends}/laravel/ui/blade.js +1 -0
  102. package/{lib/stacks → src/stacks/backends}/laravel/ui/index.js +1 -0
  103. package/{lib/stacks → src/stacks/backends}/laravel/ui/inertia-react.js +4 -2
  104. package/{lib/stacks → src/stacks/backends}/laravel/ui/livewire.js +1 -0
  105. package/{lib/stacks → src/stacks/backends}/laravel/ui/shared.js +1 -0
  106. package/src/stacks/backends/none/index.js +23 -0
  107. package/src/stacks/backends/postgres/ci.js +3 -0
  108. package/src/stacks/backends/postgres/create-files.js +21 -0
  109. package/src/stacks/backends/postgres/docker.js +3 -0
  110. package/src/stacks/backends/postgres/environment.js +3 -0
  111. package/src/stacks/backends/postgres/index.js +30 -0
  112. package/src/stacks/backends/springboot/ci.js +3 -0
  113. package/src/stacks/backends/springboot/create-files.js +91 -0
  114. package/src/stacks/backends/springboot/docker.js +3 -0
  115. package/src/stacks/backends/springboot/environment.js +3 -0
  116. package/src/stacks/backends/springboot/index.js +31 -0
  117. package/src/stacks/backends/supabase/ci.js +3 -0
  118. package/src/stacks/backends/supabase/create-files.js +52 -0
  119. package/src/stacks/backends/supabase/docker.js +3 -0
  120. package/src/stacks/backends/supabase/environment.js +3 -0
  121. package/src/stacks/backends/supabase/index.js +31 -0
  122. package/src/stacks/backends/supabase/native.js +14 -0
  123. package/src/stacks/compose-files.js +259 -0
  124. package/{lib/generator.js → src/stacks/create-project.js} +206 -57
  125. package/src/stacks/frontends/README.md +4 -0
  126. package/src/stacks/frontends/nextjs/ci.js +3 -0
  127. package/src/stacks/frontends/nextjs/create-files.js +46 -0
  128. package/src/stacks/frontends/nextjs/dependencies.js +5 -0
  129. package/src/stacks/frontends/nextjs/docker.js +3 -0
  130. package/src/stacks/frontends/nextjs/environment.js +7 -0
  131. package/src/stacks/frontends/nextjs/index.js +35 -0
  132. package/src/stacks/frontends/react-native/ci.js +3 -0
  133. package/src/stacks/frontends/react-native/create-files.js +28 -0
  134. package/src/stacks/frontends/react-native/dependencies.js +5 -0
  135. package/src/stacks/frontends/react-native/docker.js +5 -0
  136. package/src/stacks/frontends/react-native/environment.js +7 -0
  137. package/src/stacks/frontends/react-native/index.js +30 -0
  138. package/src/stacks/frontends/react-vite/ci.js +3 -0
  139. package/src/stacks/frontends/react-vite/create-files.js +27 -0
  140. package/src/stacks/frontends/react-vite/dependencies.js +5 -0
  141. package/src/stacks/frontends/react-vite/docker.js +3 -0
  142. package/src/stacks/frontends/react-vite/environment.js +8 -0
  143. package/src/stacks/frontends/react-vite/index.js +32 -0
  144. package/src/stacks/shared/capability-packs.js +31 -0
  145. package/{lib → src}/stacks/shared/contributions.js +1 -1
  146. package/src/stacks/shared/environment.js +38 -0
  147. package/src/stacks/shared/javascript-package.js +117 -0
  148. package/src/stacks/shared/testing-files.js +16 -0
  149. package/templates/ci/fastapi.yml +62 -0
  150. package/templates/docker/compose/postgres.yml +1 -1
  151. package/templates/docker/compose/springboot.yml +3 -3
  152. package/templates/docker/compose/supabase.yml +1 -1
  153. package/templates/docker/compose-prod/fastapi.yml +52 -0
  154. package/templates/docker/compose-prod/springboot.yml +20 -2
  155. package/templates/docker/dockerfile/fastapi.dev.dockerfile +9 -0
  156. package/templates/docker/dockerfile/fastapi.prod.dockerfile +11 -0
  157. package/templates/docker/dockerfile/nextjs.prod.dockerfile +1 -0
  158. package/templates/docker/dockerfile/springboot.prod.dockerfile +4 -1
  159. package/templates/docker/dockerfile/vite.prod.dockerfile +2 -1
  160. package/templates/makefile/fastapi.mk +97 -0
  161. package/docs/ARCHITECTURE.md +0 -118
  162. package/lib/banner.js +0 -45
  163. package/lib/constants.js +0 -3
  164. package/lib/doctor.js +0 -45
  165. package/lib/interview.js +0 -77
  166. package/lib/laravel-scaffold.js +0 -3
  167. package/lib/scaffold.js +0 -467
  168. package/lib/stacks/index.js +0 -8
  169. package/playbooks/concerns/zod.md +0 -174
  170. package/playbooks/devops/makefile.manifest.json +0 -10
  171. package/playbooks/devops/makefile.md +0 -556
  172. package/playbooks/styling/css-modules-extensions.md +0 -267
  173. package/playbooks/styling/css-modules.manifest.json +0 -11
  174. package/playbooks/universal/coding-rules.manifest.json +0 -12
  175. package/playbooks/universal/coding-rules.md +0 -281
  176. package/playbooks/universal/git-conventions.manifest.json +0 -10
  177. package/playbooks/universal/git-conventions.md +0 -186
  178. package/playbooks/universal/typescript.manifest.json +0 -11
  179. package/playbooks/universal/typescript.md +0 -272
  180. package/scripts/compatibility-matrix.mjs +0 -48
  181. /package/{playbooks/capabilities → library/development-tools}/ci/github-actions.md +0 -0
  182. /package/{playbooks/devops → library/development-tools/devops/pr-template}/pr-template.md +0 -0
  183. /package/{playbooks/capabilities/docker/docker.manifest.json → library/development-tools/docker/definition.json} +0 -0
  184. /package/{playbooks/capabilities → library/development-tools}/docker/overview.md +0 -0
  185. /package/{playbooks/capabilities → library/features}/auth/oidc-resource-server.md +0 -0
  186. /package/{playbooks/capabilities → library/features}/auth/spring-session.md +0 -0
  187. /package/{playbooks/capabilities/flyway/flyway.manifest.json → library/features/flyway/definition.json} +0 -0
  188. /package/{playbooks/capabilities → library/features}/flyway/environments.md +0 -0
  189. /package/{playbooks/capabilities → library/features}/flyway/migrations.md +0 -0
  190. /package/{playbooks/capabilities → library/features}/flyway/testing.md +0 -0
  191. /package/{playbooks/capabilities → library/features}/laravel/database.md +0 -0
  192. /package/{playbooks/capabilities → library/features}/laravel/migrations.md +0 -0
  193. /package/{playbooks/capabilities → library/features}/laravel/observability.md +0 -0
  194. /package/{playbooks/capabilities → library/features}/laravel/oidc-resource-server.md +0 -0
  195. /package/{playbooks/capabilities → library/features}/laravel/queues.md +0 -0
  196. /package/{playbooks/capabilities → library/features}/laravel/sanctum-spa.md +0 -0
  197. /package/{playbooks/capabilities → library/features}/laravel/scheduler.md +0 -0
  198. /package/{playbooks/capabilities → library/features}/laravel/session-auth.md +0 -0
  199. /package/{playbooks/capabilities → library/features}/laravel/storage-uploads.md +0 -0
  200. /package/{playbooks/capabilities → library/features}/postgresql/architecture.md +0 -0
  201. /package/{playbooks/capabilities/postgresql/postgresql.manifest.json → library/features/postgresql/definition.json} +0 -0
  202. /package/{playbooks/capabilities → library/features}/postgresql/migrations.md +0 -0
  203. /package/{playbooks/capabilities → library/features}/postgresql/schema-design.md +0 -0
  204. /package/{playbooks/capabilities → library/features}/postgresql/security.md +0 -0
  205. /package/{playbooks/capabilities → library/features}/postgresql/testing.md +0 -0
  206. /package/{playbooks/capabilities → library/features}/prisma/architecture.md +0 -0
  207. /package/{playbooks/capabilities/prisma/prisma.manifest.json → library/features/prisma/definition.json} +0 -0
  208. /package/{playbooks/capabilities → library/features}/prisma/migrations.md +0 -0
  209. /package/{playbooks/capabilities → library/features}/prisma/runtime.md +0 -0
  210. /package/{playbooks/capabilities → library/features}/prisma/schema.md +0 -0
  211. /package/{playbooks/capabilities → library/features}/prisma/testing.md +0 -0
  212. /package/{playbooks/capabilities → library/features}/supabase/architecture.md +0 -0
  213. /package/{playbooks/capabilities → library/features}/supabase/authentication.md +0 -0
  214. /package/{playbooks/capabilities/supabase/supabase.manifest.json → library/features/supabase/definition.json} +0 -0
  215. /package/{playbooks/capabilities → library/features}/supabase/expo.md +0 -0
  216. /package/{playbooks/capabilities → library/features}/supabase/migrations.md +0 -0
  217. /package/{playbooks/capabilities → library/features}/supabase/nextjs.md +0 -0
  218. /package/{playbooks/capabilities → library/features}/supabase/rls.md +0 -0
  219. /package/{playbooks/capabilities → library/features}/supabase/testing.md +0 -0
  220. /package/{playbooks/capabilities → library/features}/supabase/vite.md +0 -0
  221. /package/{playbooks → library/optional-features}/concerns/axios.md +0 -0
  222. /package/{playbooks → library/optional-features}/concerns/next-safe-action.md +0 -0
  223. /package/{playbooks → library/optional-features}/concerns/next-themes.md +0 -0
  224. /package/{playbooks → library/optional-features}/concerns/nuqs.md +0 -0
  225. /package/{playbooks → library/optional-features}/concerns/t3-env.md +0 -0
  226. /package/{playbooks → library/optional-features}/concerns/tanstack-query.md +0 -0
  227. /package/{playbooks → library/optional-features}/concerns/zustand.md +0 -0
  228. /package/{playbooks/styling/native-styles.manifest.json → library/optional-features/styling/native-styles/definition.json} +0 -0
  229. /package/{playbooks/styling → library/optional-features/styling/native-styles}/native-styles.md +0 -0
  230. /package/{playbooks/styling/tailwind.manifest.json → library/optional-features/styling/tailwind/definition.json} +0 -0
  231. /package/{playbooks/styling → library/optional-features/styling/tailwind}/tailwind-extensions.md +0 -0
  232. /package/{playbooks/platform → library/platforms}/laravel-ui/blade/architecture.md +0 -0
  233. /package/{playbooks/platform → library/platforms}/laravel-ui/blade/runtime.md +0 -0
  234. /package/{playbooks/platform → library/platforms}/laravel-ui/blade/security.md +0 -0
  235. /package/{playbooks/platform → library/platforms}/laravel-ui/blade/structure.md +0 -0
  236. /package/{playbooks/platform → library/platforms}/laravel-ui/blade/testing.md +0 -0
  237. /package/{playbooks/stack/laravel-ui.manifest.json → library/platforms/laravel-ui/definition.json} +0 -0
  238. /package/{playbooks/platform → library/platforms}/laravel-ui/inertia-react/architecture.md +0 -0
  239. /package/{playbooks/platform → library/platforms}/laravel-ui/inertia-react/runtime.md +0 -0
  240. /package/{playbooks/platform → library/platforms}/laravel-ui/inertia-react/security.md +0 -0
  241. /package/{playbooks/platform → library/platforms}/laravel-ui/inertia-react/structure.md +0 -0
  242. /package/{playbooks/platform → library/platforms}/laravel-ui/inertia-react/testing.md +0 -0
  243. /package/{playbooks/platform → library/platforms}/laravel-ui/livewire/architecture.md +0 -0
  244. /package/{playbooks/platform → library/platforms}/laravel-ui/livewire/runtime.md +0 -0
  245. /package/{playbooks/platform → library/platforms}/laravel-ui/livewire/security.md +0 -0
  246. /package/{playbooks/platform → library/platforms}/laravel-ui/livewire/structure.md +0 -0
  247. /package/{playbooks/platform → library/platforms}/laravel-ui/livewire/testing.md +0 -0
  248. /package/{playbooks/platform → library/platforms/mobile}/mobile.md +0 -0
  249. /package/{playbooks/platform → library/platforms/web}/web.md +0 -0
  250. /package/{playbooks/stack → library/stacks}/expo/architecture.md +0 -0
  251. /package/{playbooks/stack → library/stacks}/expo/runtime.md +0 -0
  252. /package/{playbooks/stack → library/stacks}/expo/security.md +0 -0
  253. /package/{playbooks/stack → library/stacks}/expo/structure.md +0 -0
  254. /package/{playbooks/stack → library/stacks}/expo/testing.md +0 -0
  255. /package/{playbooks/stack → library/stacks}/laravel/architecture.md +0 -0
  256. /package/{playbooks/stack/laravel.manifest.json → library/stacks/laravel/definition.json} +0 -0
  257. /package/{playbooks/stack → library/stacks}/laravel/runtime.md +0 -0
  258. /package/{playbooks/stack → library/stacks}/laravel/security.md +0 -0
  259. /package/{playbooks/stack → library/stacks}/laravel/structure.md +0 -0
  260. /package/{playbooks/stack → library/stacks}/laravel/testing.md +0 -0
  261. /package/{playbooks/stack → library/stacks}/nextjs/architecture.md +0 -0
  262. /package/{playbooks/stack → library/stacks}/nextjs/runtime.md +0 -0
  263. /package/{playbooks/stack → library/stacks}/nextjs/security.md +0 -0
  264. /package/{playbooks/stack → library/stacks}/nextjs/structure.md +0 -0
  265. /package/{playbooks/stack → library/stacks}/nextjs/testing.md +0 -0
  266. /package/{playbooks/stack/none.manifest.json → library/stacks/none/definition.json} +0 -0
  267. /package/{playbooks/stack → library/stacks}/react-vite/architecture.md +0 -0
  268. /package/{playbooks/stack → library/stacks}/react-vite/runtime.md +0 -0
  269. /package/{playbooks/stack → library/stacks}/react-vite/security.md +0 -0
  270. /package/{playbooks/stack → library/stacks}/react-vite/structure.md +0 -0
  271. /package/{playbooks/stack → library/stacks}/react-vite/testing.md +0 -0
  272. /package/{playbooks/stack → library/stacks}/springboot/architecture.md +0 -0
  273. /package/{playbooks/stack/springboot.manifest.json → library/stacks/springboot/definition.json} +0 -0
  274. /package/{playbooks/stack → library/stacks}/springboot/runtime.md +0 -0
  275. /package/{playbooks/stack → library/stacks}/springboot/security.md +0 -0
  276. /package/{playbooks/stack → library/stacks}/springboot/structure.md +0 -0
  277. /package/{playbooks/stack → library/stacks}/springboot/testing.md +0 -0
  278. /package/{playbooks/universal → library/universal/accessibility}/accessibility.md +0 -0
  279. /package/{playbooks/universal → library/universal/error-handling}/error-handling.md +0 -0
  280. /package/{playbooks/universal → library/universal/observability}/observability.md +0 -0
  281. /package/{playbooks/universal → library/universal/security}/security.md +0 -0
  282. /package/{lib → src/engine}/project-location.js +0 -0
  283. /package/{lib → src}/stacks/context.js +0 -0
  284. /package/{lib/stacks/contract.js → src/stacks/rules.js} +0 -0
  285. /package/{ci → templates/ci}/expo.yml +0 -0
  286. /package/{ci → templates/ci}/laravel.yml +0 -0
  287. /package/{ci → templates/ci}/nextjs.yml +0 -0
  288. /package/{ci → templates/ci}/springboot.yml +0 -0
  289. /package/{ci → templates/ci}/vite.yml +0 -0
@@ -0,0 +1,874 @@
1
+ import { pythonPackageVersion } from '../../shared/javascript-package.js'
2
+
3
+ function versions(stack) {
4
+ const version = (name) => pythonPackageVersion(stack.profile, name, 'fastapi scaffold')
5
+ return {
6
+ fastapi: version('fastapi'),
7
+ uvicorn: version('uvicorn'),
8
+ pydanticSettings: version('pydantic-settings'),
9
+ sqlalchemy: version('sqlalchemy'),
10
+ asyncpg: version('asyncpg'),
11
+ alembic: version('alembic'),
12
+ pyjwt: version('pyjwt'),
13
+ httpx: version('httpx'),
14
+ pytest: version('pytest'),
15
+ pytestAsyncio: version('pytest-asyncio'),
16
+ ruff: version('ruff'),
17
+ mypy: version('mypy'),
18
+ }
19
+ }
20
+
21
+ function pyproject(answers, stack, v) {
22
+ return `[project]
23
+ name = "${answers.projectName}"
24
+ version = "0.1.0"
25
+ description = "${answers.projectDescription}"
26
+ readme = "README.md"
27
+ requires-python = ">=3.13"
28
+ dependencies = [
29
+ "fastapi==${v.fastapi}",
30
+ "uvicorn==${v.uvicorn}",
31
+ "pydantic-settings==${v.pydanticSettings}",
32
+ "sqlalchemy==${v.sqlalchemy}",
33
+ "asyncpg==${v.asyncpg}",
34
+ "alembic==${v.alembic}",
35
+ "pyjwt==${v.pyjwt}",
36
+ "httpx==${v.httpx}",
37
+ ]
38
+
39
+ [dependency-groups]
40
+ dev = [
41
+ "pytest==${v.pytest}",
42
+ "pytest-asyncio==${v.pytestAsyncio}",
43
+ "ruff==${v.ruff}",
44
+ "mypy==${v.mypy}",
45
+ "httpx==${v.httpx}",
46
+ ]
47
+
48
+ [build-system]
49
+ requires = ["hatchling"]
50
+ build-backend = "hatchling.build"
51
+
52
+ [tool.hatch.build.targets.wheel]
53
+ packages = ["app"]
54
+
55
+ [tool.ruff]
56
+ target-version = "py313"
57
+ line-length = 100
58
+
59
+ [tool.ruff.lint]
60
+ select = ["E", "F", "I", "UP", "B"]
61
+
62
+ [tool.mypy]
63
+ python_version = "3.13"
64
+ strict = true
65
+ ignore_missing_imports = true
66
+
67
+ [tool.pytest.ini_options]
68
+ asyncio_mode = "auto"
69
+ testpaths = ["tests"]
70
+ `
71
+ }
72
+
73
+ function backendReadme(answers) {
74
+ return `# ${answers.projectName} backend
75
+
76
+ FastAPI service for ${answers.projectDescription}.
77
+
78
+ Run \`uv sync\` to install the exact tested dependencies, then use the commands in the
79
+ repository documentation to develop, test, and operate the service.
80
+ `
81
+ }
82
+
83
+ function mainPy(stack, core) {
84
+ return `"""Application entrypoint for ${stack.architecture} profile."""
85
+
86
+ from __future__ import annotations
87
+
88
+ import logging
89
+ import uuid
90
+ from collections.abc import AsyncIterator
91
+ from contextlib import asynccontextmanager
92
+
93
+ from fastapi import FastAPI, Request
94
+ from fastapi.middleware.cors import CORSMiddleware
95
+ from fastapi.responses import JSONResponse
96
+
97
+ from ${core}.config import settings
98
+ from ${core}.db import close_engine
99
+
100
+ from .routes_health import router as health_router
101
+ from .status_router import router as status_router
102
+
103
+ logger = logging.getLogger(__name__)
104
+
105
+
106
+ class AppError(Exception):
107
+ """Typed application error with a stable code."""
108
+
109
+ def __init__(self, code: str, message: str, status: int = 400) -> None:
110
+ super().__init__(message)
111
+ self.code = code
112
+ self.status = status
113
+
114
+
115
+ @asynccontextmanager
116
+ async def lifespan(_: FastAPI) -> AsyncIterator[None]:
117
+ yield
118
+ await close_engine()
119
+
120
+
121
+ def create_app() -> FastAPI:
122
+ app = FastAPI(title="API", version="0.1.0", lifespan=lifespan)
123
+
124
+ origins = [o.strip() for o in settings.cors_allowed_origins.split(",") if o.strip()]
125
+ app.add_middleware(
126
+ CORSMiddleware,
127
+ allow_origins=origins,
128
+ allow_credentials=False,
129
+ allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
130
+ allow_headers=["Authorization", "Content-Type"],
131
+ )
132
+
133
+ @app.middleware("http")
134
+ async def request_id(request: Request, call_next): # type: ignore[no-untyped-def]
135
+ request_id = request.headers.get("X-Request-ID", str(uuid.uuid4()))
136
+ response = await call_next(request)
137
+ response.headers["X-Request-ID"] = request_id
138
+ return response
139
+
140
+ @app.exception_handler(AppError)
141
+ async def app_error_handler(request: Request, exc: AppError) -> JSONResponse: # noqa: ARG001
142
+ request_id = request.headers.get("X-Request-ID", "")
143
+ return JSONResponse(
144
+ status_code=exc.status,
145
+ content={"error": {"code": exc.code, "message": str(exc), "requestId": request_id}},
146
+ )
147
+
148
+ app.include_router(health_router)
149
+ app.include_router(status_router)
150
+ logger.info("application startup complete")
151
+ return app
152
+
153
+
154
+ app = create_app()
155
+ `
156
+ }
157
+
158
+ function configPy() {
159
+ return `"""Validated application configuration."""
160
+
161
+ from __future__ import annotations
162
+
163
+ from pydantic_settings import BaseSettings, SettingsConfigDict
164
+
165
+
166
+ class Settings(BaseSettings):
167
+ model_config = SettingsConfigDict(env_file=".env", extra="ignore")
168
+
169
+ database_url: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/app"
170
+ oidc_issuer: str = ""
171
+ oidc_audience: str = "api"
172
+ oidc_algorithms: str = "RS256"
173
+ oidc_jwks_url: str = ""
174
+ cors_allowed_origins: str = "http://localhost:3000"
175
+
176
+
177
+ settings = Settings()
178
+ `
179
+ }
180
+
181
+ function dbPy(configModule) {
182
+ return `"""Async SQLAlchemy engine and session management."""
183
+
184
+ from __future__ import annotations
185
+
186
+ from collections.abc import AsyncIterator
187
+ from datetime import datetime
188
+ from uuid import UUID
189
+
190
+ from sqlalchemy import DateTime, func
191
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
192
+ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
193
+
194
+ from ${configModule} import settings
195
+
196
+
197
+ class Base(DeclarativeBase):
198
+ pass
199
+
200
+
201
+ class Example(Base):
202
+ __tablename__ = "examples"
203
+
204
+ id: Mapped[UUID] = mapped_column(primary_key=True)
205
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
206
+ updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
207
+
208
+
209
+ _engine = create_async_engine(settings.database_url, pool_size=5, max_overflow=10)
210
+ _session_factory = async_sessionmaker(_engine, class_=AsyncSession, expire_on_commit=False)
211
+
212
+
213
+ async def get_session() -> AsyncIterator[AsyncSession]:
214
+ async with _session_factory() as session:
215
+ yield session
216
+
217
+
218
+ async def close_engine() -> None:
219
+ await _engine.dispose()
220
+ `
221
+ }
222
+
223
+ function securityPy(authentication, configModule) {
224
+ if (authentication === 'public') {
225
+ return `"""Public authentication model: no user accounts, endpoints intentionally open."""
226
+
227
+ from __future__ import annotations
228
+
229
+ from typing import Any
230
+
231
+
232
+ async def require_auth() -> Any:
233
+ """Public applications do not invent authentication; returns no principal."""
234
+ return None
235
+ `
236
+ }
237
+ if (authentication === 'undecided') {
238
+ return `"""Undecided authentication model: deny protected routes until auth is designed."""
239
+
240
+ from __future__ import annotations
241
+
242
+ from fastapi import HTTPException
243
+
244
+
245
+ async def require_auth() -> None:
246
+ """Fail closed; health stays public, every other endpoint is denied."""
247
+ raise HTTPException(status_code=403, detail="Authentication is not configured.")
248
+ `
249
+ }
250
+ return `"""OIDC bearer validation for protected routes."""
251
+
252
+ from __future__ import annotations
253
+
254
+ from typing import Annotated
255
+
256
+ import jwt
257
+ from fastapi import Depends, HTTPException
258
+ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
259
+ from jwt import PyJWKClient
260
+
261
+ from ${configModule} import settings
262
+
263
+ _bearer = HTTPBearer(auto_error=False)
264
+
265
+
266
+ def _jwks_url() -> str:
267
+ if settings.oidc_jwks_url:
268
+ return settings.oidc_jwks_url
269
+ return f"{settings.oidc_issuer.rstrip('/')}/.well-known/jwks.json"
270
+
271
+
272
+ async def require_auth(
273
+ credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(_bearer)] = None,
274
+ ) -> dict[str, object]:
275
+ """Validate the bearer access token; deny by default."""
276
+ if credentials is None or credentials.scheme.lower() != "bearer":
277
+ raise HTTPException(status_code=401, detail="Missing bearer token.")
278
+ algorithms = [a.strip() for a in settings.oidc_algorithms.split(",") if a.strip()]
279
+ try:
280
+ signing_key = PyJWKClient(_jwks_url()).get_signing_key_from_jwt(credentials.credentials)
281
+ claims: dict[str, object] = jwt.decode(
282
+ credentials.credentials,
283
+ signing_key.key,
284
+ algorithms=algorithms,
285
+ issuer=settings.oidc_issuer,
286
+ audience=settings.oidc_audience,
287
+ options={"require": ["exp", "iss", "aud"]},
288
+ )
289
+ except jwt.PyJWTError as exc:
290
+ raise HTTPException(status_code=401, detail="Invalid bearer token.") from exc
291
+ return claims
292
+ `
293
+ }
294
+
295
+ function healthRouter() {
296
+ return `"""Public health and readiness routes."""
297
+
298
+ from __future__ import annotations
299
+
300
+ from fastapi import APIRouter
301
+ from sqlalchemy import text
302
+
303
+ router = APIRouter(tags=["health"])
304
+
305
+
306
+ @router.get("/health")
307
+ async def health() -> dict[str, str]:
308
+ return {"status": "ok"}
309
+
310
+
311
+ @router.get("/ready")
312
+ async def ready() -> dict[str, str]:
313
+ from .db import _engine # noqa: PLC0415
314
+
315
+ async with _engine.connect() as connection:
316
+ await connection.execute(text("SELECT 1"))
317
+ return {"status": "ready"}
318
+ `
319
+ }
320
+
321
+ function healthRouterCore() {
322
+ return `"""Public health and readiness routes."""
323
+
324
+ from __future__ import annotations
325
+
326
+ from fastapi import APIRouter
327
+ from sqlalchemy import text
328
+
329
+ from app.core.db import _engine
330
+
331
+ router = APIRouter(tags=["health"])
332
+
333
+
334
+ @router.get("/health")
335
+ async def health() -> dict[str, str]:
336
+ return {"status": "ok"}
337
+
338
+
339
+ @router.get("/ready")
340
+ async def ready() -> dict[str, str]:
341
+ async with _engine.connect() as connection:
342
+ await connection.execute(text("SELECT 1"))
343
+ return {"status": "ready"}
344
+ `
345
+ }
346
+
347
+ function smallApiRouter(authentication) {
348
+ const guard = authentication === 'public'
349
+ ? ''
350
+ : ', Depends\n\nfrom .security import require_auth'
351
+ const dep = authentication === 'public' ? '' : ', dependencies=[Depends(require_auth)]'
352
+ return `"""Application routes for the small profile."""
353
+
354
+ from __future__ import annotations
355
+
356
+ from fastapi import APIRouter${guard}
357
+
358
+ router = APIRouter(prefix="/api", tags=["status"]${dep})
359
+
360
+
361
+ @router.get("/status")
362
+ async def status() -> dict[str, str]:
363
+ return {"status": "ok", "architecture": "small"}
364
+ `
365
+ }
366
+
367
+ function statusFeature(prefix, architecture) {
368
+ return {
369
+ [`${prefix}/schemas.py`]: `"""Transport schemas for the status feature."""
370
+
371
+ from __future__ import annotations
372
+
373
+ from pydantic import BaseModel
374
+
375
+
376
+ class StatusResponse(BaseModel):
377
+ status: str
378
+ architecture: str
379
+ `,
380
+ [`${prefix}/repository.py`]: `"""Persistence boundary for the status feature."""
381
+
382
+ from __future__ import annotations
383
+
384
+
385
+ class StatusRepository:
386
+ def current(self) -> dict[str, str]:
387
+ return {"status": "ok", "architecture": "${architecture}"}
388
+ `,
389
+ [`${prefix}/service.py`]: `"""Application operations for the status feature."""
390
+
391
+ from __future__ import annotations
392
+
393
+ from .repository import StatusRepository
394
+ from .schemas import StatusResponse
395
+
396
+
397
+ class StatusService:
398
+ def __init__(self, repository: StatusRepository | None = None) -> None:
399
+ self._repository = repository or StatusRepository()
400
+
401
+ def current(self) -> StatusResponse:
402
+ return StatusResponse(**self._repository.current())
403
+ `,
404
+ [`${prefix}/router.py`]: `"""HTTP adapter for the status feature."""
405
+
406
+ from __future__ import annotations
407
+
408
+ from fastapi import APIRouter, Depends
409
+
410
+ from app.core.security import require_auth
411
+
412
+ from .service import StatusService
413
+
414
+ router = APIRouter(prefix="/api", tags=["status"])
415
+
416
+
417
+ @router.get("/status", dependencies=[Depends(require_auth)])
418
+ async def status() -> dict[str, object]:
419
+ return StatusService().current().model_dump()
420
+ `,
421
+ }
422
+ }
423
+
424
+ function largeInit() {
425
+ return `"""Public module interface for the status feature."""
426
+
427
+ from __future__ import annotations
428
+
429
+ from .router import router
430
+ from .schemas import StatusResponse
431
+ from .service import StatusService
432
+
433
+ __all__ = ["StatusService", "StatusResponse", "router"]
434
+ `
435
+ }
436
+
437
+ function boundaryTest() {
438
+ return `"""Architecture boundary: feature modules expose only their public interface."""
439
+
440
+ from __future__ import annotations
441
+
442
+ import pathlib
443
+
444
+ MODULE = pathlib.Path(__file__).resolve().parents[1] / "app" / "modules" / "status"
445
+ PUBLIC = {"__init__.py", "router.py", "service.py", "schemas.py"}
446
+
447
+
448
+ def test_module_keeps_implementation_internal() -> None:
449
+ assert MODULE.is_dir()
450
+ internals = {"repository.py"}
451
+ assert internals <= {p.name for p in MODULE.iterdir()}
452
+ init = (MODULE / "__init__.py").read_text()
453
+ assert "StatusService" in init and "router" in init
454
+
455
+
456
+ def test_only_public_modules_are_imported() -> None:
457
+ tree = (MODULE / "router.py").read_text()
458
+ assert "repository" not in tree
459
+ `
460
+ }
461
+
462
+ function alembicIni(projectName) {
463
+ return `[alembic]
464
+ script_location = alembic
465
+ prepend_sys_path = .
466
+ version_path_separator = os
467
+
468
+ [loggers]
469
+ keys = root,sqlalchemy,alembic
470
+
471
+ [handlers]
472
+ keys = console
473
+
474
+ [formatters]
475
+ keys = generic
476
+
477
+ [logger_root]
478
+ level = WARN
479
+ handlers = console
480
+
481
+ [logger_sqlalchemy]
482
+ level = WARN
483
+ handlers =
484
+ qualname = sqlalchemy.engine
485
+
486
+ [logger_alembic]
487
+ level = INFO
488
+ handlers =
489
+ qualname = alembic
490
+
491
+ [handler_console]
492
+ class = StreamHandler
493
+ args = (sys.stderr,)
494
+ level = NOTSET
495
+ formatter = generic
496
+
497
+ [formatter_generic]
498
+ format = %(levelname)-5.5s [%(name)s] %(message)s
499
+ datefmt = %H:%M:%S
500
+ `
501
+ }
502
+
503
+ function alembicEnv(configModule, dbModule) {
504
+ return `"""Alembic async environment; run migrations explicitly with alembic upgrade head."""
505
+
506
+ from __future__ import annotations
507
+
508
+ import asyncio
509
+ from logging.config import fileConfig
510
+
511
+ from sqlalchemy import pool
512
+ from sqlalchemy.engine import Connection
513
+ from sqlalchemy.ext.asyncio import async_engine_from_config
514
+
515
+ from alembic import context
516
+ from ${configModule} import settings
517
+ from ${dbModule} import Base
518
+
519
+ config = context.config
520
+ if config.config_file_name is not None:
521
+ fileConfig(config.config_file_name)
522
+ config.set_main_option("sqlalchemy.url", settings.database_url)
523
+
524
+ target_metadata = Base.metadata
525
+
526
+
527
+ def run_migrations_offline() -> None:
528
+ context.configure(
529
+ url=settings.database_url,
530
+ target_metadata=target_metadata,
531
+ literal_binds=True,
532
+ dialect_opts={"paramstyle": "named"},
533
+ )
534
+ with context.begin_transaction():
535
+ context.run_migrations()
536
+
537
+
538
+ def do_run_migrations(connection: Connection) -> None:
539
+ context.configure(connection=connection, target_metadata=target_metadata)
540
+ with context.begin_transaction():
541
+ context.run_migrations()
542
+
543
+
544
+ async def run_migrations_online() -> None:
545
+ connectable = async_engine_from_config(
546
+ config.get_section(config.config_ini_section, {}),
547
+ prefix="sqlalchemy.",
548
+ poolclass=pool.NullPool,
549
+ )
550
+ async with connectable.connect() as connection:
551
+ await connection.run_sync(do_run_migrations)
552
+ await connectable.dispose()
553
+
554
+
555
+ if context.is_offline_mode():
556
+ run_migrations_offline()
557
+ else:
558
+ asyncio.run(run_migrations_online())
559
+ `
560
+ }
561
+
562
+ function baselineMigration() {
563
+ return `"""Baseline schema: examples table only; add product tables per feature."""
564
+
565
+ from __future__ import annotations
566
+
567
+ import sqlalchemy as sa
568
+
569
+ from alembic import op
570
+
571
+ revision = "0001_baseline"
572
+ down_revision = None
573
+ branch_labels = None
574
+ depends_on = None
575
+
576
+
577
+ def upgrade() -> None:
578
+ op.create_table(
579
+ "examples",
580
+ sa.Column("id", sa.Uuid(), primary_key=True),
581
+ sa.Column(
582
+ "created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False
583
+ ),
584
+ sa.Column(
585
+ "updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False
586
+ ),
587
+ )
588
+
589
+
590
+ def downgrade() -> None:
591
+ op.drop_table("examples")
592
+ `
593
+ }
594
+
595
+ function conftestPy() {
596
+ return `"""Shared pytest fixtures."""
597
+
598
+ from __future__ import annotations
599
+
600
+ from collections.abc import AsyncIterator
601
+
602
+ import pytest_asyncio
603
+ from httpx import ASGITransport, AsyncClient
604
+
605
+ from app.main import create_app
606
+
607
+
608
+ @pytest_asyncio.fixture
609
+ async def client() -> AsyncIterator[AsyncClient]:
610
+ app = create_app()
611
+ transport = ASGITransport(app=app)
612
+ async with AsyncClient(transport=transport, base_url="http://test") as client:
613
+ yield client
614
+ `
615
+ }
616
+
617
+ function healthTest() {
618
+ return `"""Health contract tests (no database)."""
619
+
620
+ from __future__ import annotations
621
+
622
+ import pytest
623
+ from httpx import AsyncClient
624
+
625
+
626
+ @pytest.mark.asyncio
627
+ async def test_health_is_public(client: AsyncClient) -> None:
628
+ response = await client.get("/health")
629
+ assert response.status_code == 200
630
+ assert response.json() == {"status": "ok"}
631
+ `
632
+ }
633
+
634
+ function securityTest(authentication) {
635
+ if (authentication === 'public') {
636
+ return `"""Public model: application endpoints stay open without invented auth."""
637
+
638
+ from __future__ import annotations
639
+
640
+ import pytest
641
+ from httpx import AsyncClient
642
+
643
+
644
+ @pytest.mark.asyncio
645
+ async def test_status_is_public(client: AsyncClient) -> None:
646
+ response = await client.get("/api/status")
647
+ assert response.status_code == 200
648
+ assert response.json()["status"] == "ok"
649
+ `
650
+ }
651
+ if (authentication === 'undecided') {
652
+ return `"""Undecided model: protected routes fail closed until auth is designed."""
653
+
654
+ from __future__ import annotations
655
+
656
+ import pytest
657
+ from httpx import AsyncClient
658
+
659
+
660
+ @pytest.mark.asyncio
661
+ async def test_status_fails_closed(client: AsyncClient) -> None:
662
+ response = await client.get("/api/status")
663
+ assert response.status_code == 403
664
+ `
665
+ }
666
+ return `"""OIDC model: missing and invalid bearer tokens are rejected."""
667
+
668
+ from __future__ import annotations
669
+
670
+ import pytest
671
+ from httpx import AsyncClient
672
+
673
+
674
+ @pytest.mark.asyncio
675
+ async def test_status_rejects_missing_bearer_token(client: AsyncClient) -> None:
676
+ response = await client.get("/api/status")
677
+ assert response.status_code == 401
678
+
679
+
680
+ @pytest.mark.asyncio
681
+ async def test_status_rejects_invalid_bearer_token(client: AsyncClient) -> None:
682
+ response = await client.get("/api/status", headers={"Authorization": "Bearer invalid"})
683
+ assert response.status_code == 401
684
+ `
685
+ }
686
+
687
+ export function buildFastApiFiles(answers, vars, stack) {
688
+ const v = versions(stack)
689
+ const root = stack.frontendKey === 'no-frontend' ? '' : 'backend/'
690
+ const auth = stack.authentication
691
+ const files = {
692
+ [`${root}pyproject.toml`]: pyproject(answers, stack, v),
693
+ [`${root}.python-version`]: `${stack.profile.runtimes.python}\n`,
694
+ [`${root}alembic.ini`]: alembicIni(answers.projectName),
695
+ [`${root}alembic/versions/.gitkeep`]: '',
696
+ [`${root}tests/__init__.py`]: '',
697
+ [`${root}tests/conftest.py`]: conftestPy(),
698
+ [`${root}tests/test_health.py`]: healthTest(),
699
+ [`${root}tests/test_security.py`]: securityTest(auth),
700
+ }
701
+
702
+ if (root) {
703
+ files[`${root}README.md`] = backendReadme(answers)
704
+ }
705
+
706
+ if (stack.architecture === 'small') {
707
+ files[`${root}app/__init__.py`] = ''
708
+ files[`${root}app/main.py`] = smallMain(stack)
709
+ files[`${root}app/config.py`] = configPy()
710
+ files[`${root}app/db.py`] = dbPy('.config')
711
+ files[`${root}app/security.py`] = securityPy(auth, '.config')
712
+ files[`${root}app/routes_health.py`] = healthRouter()
713
+ files[`${root}app/routes_api.py`] = smallApiRouter(auth)
714
+ files[`${root}alembic/env.py`] = alembicEnv('app.config', 'app.db')
715
+ files[`${root}alembic/versions/0001_baseline.py`] = baselineMigration()
716
+ } else if (stack.architecture === 'medium') {
717
+ files[`${root}app/__init__.py`] = ''
718
+ files[`${root}app/main.py`] = mainPy(stack, 'app.core')
719
+ files[`${root}app/routes_health.py`] = healthRouterCore()
720
+ files[`${root}app/status_router.py`] = `"""Status router alias for the medium profile."""
721
+
722
+ from __future__ import annotations
723
+
724
+ from app.features.status.router import router as router
725
+ `
726
+ files[`${root}app/core/__init__.py`] = ''
727
+ files[`${root}app/core/config.py`] = configPy()
728
+ files[`${root}app/core/db.py`] = dbPy('app.core.config')
729
+ files[`${root}app/core/security.py`] = securityPy(auth, 'app.core.config')
730
+ files[`${root}app/core/logging.py`] = `"""Structured PII-safe logging configuration."""
731
+
732
+ from __future__ import annotations
733
+
734
+ import logging
735
+
736
+
737
+ def configure_logging() -> None:
738
+ logging.basicConfig(level=logging.INFO, format="%(levelname)s [%(name)s] %(message)s")
739
+ `
740
+ files[`${root}app/features/__init__.py`] = ''
741
+ files[`${root}app/features/status/__init__.py`] = ''
742
+ Object.assign(files, Object.fromEntries(
743
+ Object.entries(statusFeature(`${root}app/features/status`, 'medium')).map(([k, val]) => [k, val]),
744
+ ))
745
+ files[`${root}alembic/env.py`] = alembicEnv('app.core.config', 'app.core.db')
746
+ files[`${root}alembic/versions/0001_baseline.py`] = baselineMigration()
747
+ } else {
748
+ files[`${root}app/__init__.py`] = ''
749
+ files[`${root}app/main.py`] = mainPy(stack, 'app.core')
750
+ files[`${root}app/routes_health.py`] = healthRouterCore()
751
+ files[`${root}app/status_router.py`] = `"""Status router alias for the large profile."""
752
+
753
+ from __future__ import annotations
754
+
755
+ from app.modules.status import router as router
756
+ `
757
+ files[`${root}app/core/__init__.py`] = ''
758
+ files[`${root}app/core/config.py`] = configPy()
759
+ files[`${root}app/core/db.py`] = dbPy('app.core.config')
760
+ files[`${root}app/core/security.py`] = securityPy(auth, 'app.core.config')
761
+ files[`${root}app/core/logging.py`] = `"""Structured PII-safe logging configuration."""
762
+
763
+ from __future__ import annotations
764
+
765
+ import logging
766
+
767
+
768
+ def configure_logging() -> None:
769
+ logging.basicConfig(level=logging.INFO, format="%(levelname)s [%(name)s] %(message)s")
770
+ `
771
+ files[`${root}app/modules/__init__.py`] = ''
772
+ files[`${root}app/modules/status/__init__.py`] = largeInit()
773
+ Object.assign(files, Object.fromEntries(
774
+ Object.entries(statusFeature(`${root}app/modules/status`, 'large'))
775
+ .filter(([k]) => !k.endsWith('/router.py'))
776
+ .map(([k, val]) => [k, val]),
777
+ ))
778
+ files[`${root}app/modules/status/router.py`] = `"""HTTP adapter for the status feature (public interface re-exported)."""
779
+
780
+ from __future__ import annotations
781
+
782
+ from fastapi import APIRouter, Depends
783
+
784
+ from app.core.security import require_auth
785
+
786
+ from .service import StatusService
787
+
788
+ router = APIRouter(prefix="/api", tags=["status"])
789
+
790
+
791
+ @router.get("/status", dependencies=[Depends(require_auth)])
792
+ async def status() -> dict[str, object]:
793
+ return StatusService().current().model_dump()
794
+ `
795
+ files[`${root}tests/test_boundaries.py`] = boundaryTest()
796
+ files[`${root}alembic/env.py`] = alembicEnv('app.core.config', 'app.core.db')
797
+ files[`${root}alembic/versions/0001_baseline.py`] = baselineMigration()
798
+ }
799
+ return files
800
+ }
801
+
802
+ function smallMain(stack) {
803
+ return `"""Application entrypoint for the small profile."""
804
+
805
+ from __future__ import annotations
806
+
807
+ import logging
808
+ import uuid
809
+ from collections.abc import AsyncIterator
810
+ from contextlib import asynccontextmanager
811
+
812
+ from fastapi import FastAPI, Request
813
+ from fastapi.middleware.cors import CORSMiddleware
814
+ from fastapi.responses import JSONResponse
815
+
816
+ from .config import settings
817
+ from .db import close_engine
818
+ from .routes_api import router as api_router
819
+ from .routes_health import router as health_router
820
+
821
+ logger = logging.getLogger(__name__)
822
+
823
+
824
+ class AppError(Exception):
825
+ """Typed application error with a stable code."""
826
+
827
+ def __init__(self, code: str, message: str, status: int = 400) -> None:
828
+ super().__init__(message)
829
+ self.code = code
830
+ self.status = status
831
+
832
+
833
+ @asynccontextmanager
834
+ async def lifespan(_: FastAPI) -> AsyncIterator[None]:
835
+ yield
836
+ await close_engine()
837
+
838
+
839
+ def create_app() -> FastAPI:
840
+ app = FastAPI(title="API", version="0.1.0", lifespan=lifespan)
841
+
842
+ origins = [o.strip() for o in settings.cors_allowed_origins.split(",") if o.strip()]
843
+ app.add_middleware(
844
+ CORSMiddleware,
845
+ allow_origins=origins,
846
+ allow_credentials=False,
847
+ allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
848
+ allow_headers=["Authorization", "Content-Type"],
849
+ )
850
+
851
+ @app.middleware("http")
852
+ async def request_id(request: Request, call_next): # type: ignore[no-untyped-def]
853
+ request_id = request.headers.get("X-Request-ID", str(uuid.uuid4()))
854
+ response = await call_next(request)
855
+ response.headers["X-Request-ID"] = request_id
856
+ return response
857
+
858
+ @app.exception_handler(AppError)
859
+ async def app_error_handler(request: Request, exc: AppError) -> JSONResponse: # noqa: ARG001
860
+ request_id = request.headers.get("X-Request-ID", "")
861
+ return JSONResponse(
862
+ status_code=exc.status,
863
+ content={"error": {"code": exc.code, "message": str(exc), "requestId": request_id}},
864
+ )
865
+
866
+ app.include_router(health_router)
867
+ app.include_router(api_router)
868
+ logger.info("application startup complete")
869
+ return app
870
+
871
+
872
+ app = create_app()
873
+ `
874
+ }