@msn-control/liftoff 0.10.4 → 0.11.2

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 (700) hide show
  1. package/DEVELOPER.md +238 -77
  2. package/README.md +38 -33
  3. package/assets/supported-stack.json +3 -136
  4. package/dist/adapters/azure/governance-assessment.d.ts +2 -0
  5. package/dist/adapters/azure/governance-assessment.js +21 -0
  6. package/dist/adapters/azure/governance-assessment.js.map +1 -0
  7. package/dist/adapters/filesystem/atomic-write.d.ts +2 -0
  8. package/dist/adapters/filesystem/atomic-write.js +74 -0
  9. package/dist/adapters/filesystem/atomic-write.js.map +1 -0
  10. package/dist/adapters/filesystem/errors.d.ts +2 -0
  11. package/dist/adapters/filesystem/errors.js +8 -0
  12. package/dist/adapters/filesystem/errors.js.map +1 -0
  13. package/dist/adapters/filesystem/manifest-file.d.ts +1 -0
  14. package/dist/adapters/filesystem/manifest-file.js +21 -0
  15. package/dist/adapters/filesystem/manifest-file.js.map +1 -0
  16. package/dist/adapters/filesystem/project-config.d.ts +4 -0
  17. package/dist/adapters/filesystem/project-config.js +157 -0
  18. package/dist/adapters/filesystem/project-config.js.map +1 -0
  19. package/dist/adapters/filesystem/project-discovery.d.ts +2 -0
  20. package/dist/adapters/filesystem/project-discovery.js +30 -0
  21. package/dist/adapters/filesystem/project-discovery.js.map +1 -0
  22. package/dist/adapters/filesystem/project-files.d.ts +6 -0
  23. package/dist/adapters/filesystem/project-files.js +68 -0
  24. package/dist/adapters/filesystem/project-files.js.map +1 -0
  25. package/dist/adapters/filesystem/project-lock.d.ts +8 -0
  26. package/dist/adapters/filesystem/project-lock.js +164 -0
  27. package/dist/adapters/filesystem/project-lock.js.map +1 -0
  28. package/dist/adapters/filesystem/project-paths.d.ts +2 -0
  29. package/dist/adapters/filesystem/project-paths.js +60 -0
  30. package/dist/adapters/filesystem/project-paths.js.map +1 -0
  31. package/dist/adapters/filesystem/project-transaction.d.ts +24 -0
  32. package/dist/adapters/filesystem/project-transaction.js +177 -0
  33. package/dist/adapters/filesystem/project-transaction.js.map +1 -0
  34. package/dist/adapters/filesystem/reviewed-update-transaction.d.ts +53 -0
  35. package/dist/adapters/filesystem/reviewed-update-transaction.js +864 -0
  36. package/dist/adapters/filesystem/reviewed-update-transaction.js.map +1 -0
  37. package/dist/adapters/filesystem/update-previews.d.ts +50 -0
  38. package/dist/adapters/filesystem/update-previews.js +613 -0
  39. package/dist/adapters/filesystem/update-previews.js.map +1 -0
  40. package/dist/adapters/git/governance-assessment.d.ts +22 -0
  41. package/dist/adapters/git/governance-assessment.js +161 -0
  42. package/dist/adapters/git/governance-assessment.js.map +1 -0
  43. package/dist/adapters/github/governance-assessment.d.ts +2 -0
  44. package/dist/adapters/github/governance-assessment.js +18 -0
  45. package/dist/adapters/github/governance-assessment.js.map +1 -0
  46. package/dist/adapters/packaged-assets/package-root.d.ts +3 -0
  47. package/dist/adapters/packaged-assets/package-root.js +21 -0
  48. package/dist/adapters/packaged-assets/package-root.js.map +1 -0
  49. package/dist/adapters/packaged-assets/supported-stack.d.ts +4 -0
  50. package/dist/adapters/packaged-assets/supported-stack.js +18 -0
  51. package/dist/adapters/packaged-assets/supported-stack.js.map +1 -0
  52. package/dist/adapters/packaged-assets/template-assets.d.ts +23 -0
  53. package/dist/adapters/packaged-assets/template-assets.js +36 -0
  54. package/dist/adapters/packaged-assets/template-assets.js.map +1 -0
  55. package/dist/adapters/process/shell-command.d.ts +6 -0
  56. package/dist/adapters/process/shell-command.js +27 -0
  57. package/dist/adapters/process/shell-command.js.map +1 -0
  58. package/dist/application/context.d.ts +25 -0
  59. package/dist/application/context.js +2 -0
  60. package/dist/application/context.js.map +1 -0
  61. package/dist/application/diagnose/doctor.d.ts +21 -0
  62. package/dist/application/diagnose/doctor.js +580 -0
  63. package/dist/application/diagnose/doctor.js.map +1 -0
  64. package/dist/application/diagnose/generated-project.d.ts +1 -0
  65. package/dist/application/diagnose/generated-project.js +105 -0
  66. package/dist/application/diagnose/generated-project.js.map +1 -0
  67. package/dist/application/diagnose/validate.d.ts +6 -0
  68. package/dist/application/diagnose/validate.js +27 -0
  69. package/dist/application/diagnose/validate.js.map +1 -0
  70. package/dist/application/initialize/fixture.d.ts +2 -0
  71. package/dist/application/initialize/fixture.js +39 -0
  72. package/dist/application/initialize/fixture.js.map +1 -0
  73. package/dist/application/initialize/inputs.d.ts +3 -0
  74. package/dist/application/initialize/inputs.js +35 -0
  75. package/dist/application/initialize/inputs.js.map +1 -0
  76. package/dist/application/initialize/use-case.d.ts +23 -0
  77. package/dist/application/initialize/use-case.js +360 -0
  78. package/dist/application/initialize/use-case.js.map +1 -0
  79. package/dist/application/migrate/use-case.d.ts +7 -0
  80. package/dist/application/migrate/use-case.js +378 -0
  81. package/dist/application/migrate/use-case.js.map +1 -0
  82. package/dist/application/project/catalog.d.ts +46 -0
  83. package/dist/application/project/catalog.js +12 -0
  84. package/dist/application/project/catalog.js.map +1 -0
  85. package/dist/application/project/manifest.d.ts +2 -0
  86. package/dist/application/project/manifest.js +29 -0
  87. package/dist/application/project/manifest.js.map +1 -0
  88. package/dist/application/project/planning.d.ts +6 -0
  89. package/dist/application/project/planning.js +12 -0
  90. package/dist/application/project/planning.js.map +1 -0
  91. package/dist/application/update/approval.d.ts +39 -0
  92. package/dist/application/update/approval.js +54 -0
  93. package/dist/application/update/approval.js.map +1 -0
  94. package/dist/application/update/command-guidance.d.ts +2 -0
  95. package/dist/application/update/command-guidance.js +14 -0
  96. package/dist/application/update/command-guidance.js.map +1 -0
  97. package/dist/application/update/inspection.d.ts +37 -0
  98. package/dist/application/update/inspection.js +235 -0
  99. package/dist/application/update/inspection.js.map +1 -0
  100. package/dist/application/update/migration-runtime.d.ts +14 -0
  101. package/dist/application/update/migration-runtime.js +178 -0
  102. package/dist/application/update/migration-runtime.js.map +1 -0
  103. package/dist/application/update/output.d.ts +134 -0
  104. package/dist/application/update/output.js +182 -0
  105. package/dist/application/update/output.js.map +1 -0
  106. package/dist/application/update/planning.d.ts +40 -0
  107. package/dist/application/update/planning.js +293 -0
  108. package/dist/application/update/planning.js.map +1 -0
  109. package/dist/application/update/preview.d.ts +49 -0
  110. package/dist/application/update/preview.js +220 -0
  111. package/dist/application/update/preview.js.map +1 -0
  112. package/dist/application/update/protected-source.d.ts +25 -0
  113. package/dist/application/update/protected-source.js +81 -0
  114. package/dist/application/update/protected-source.js.map +1 -0
  115. package/dist/application/update/reporting.d.ts +42 -0
  116. package/dist/application/update/reporting.js +326 -0
  117. package/dist/application/update/reporting.js.map +1 -0
  118. package/dist/application/update/revalidation-plan.d.ts +22 -0
  119. package/dist/application/update/revalidation-plan.js +108 -0
  120. package/dist/application/update/revalidation-plan.js.map +1 -0
  121. package/dist/application/update/revalidation.d.ts +70 -0
  122. package/dist/application/update/revalidation.js +337 -0
  123. package/dist/application/update/revalidation.js.map +1 -0
  124. package/dist/application/update/review-plan.d.ts +25 -0
  125. package/dist/application/update/review-plan.js +89 -0
  126. package/dist/application/update/review-plan.js.map +1 -0
  127. package/dist/application/update/transaction-approval.d.ts +29 -0
  128. package/dist/application/update/transaction-approval.js +82 -0
  129. package/dist/application/update/transaction-approval.js.map +1 -0
  130. package/dist/application/update/use-case.d.ts +9 -0
  131. package/dist/application/update/use-case.js +271 -0
  132. package/dist/application/update/use-case.js.map +1 -0
  133. package/dist/application/update/write-plan.d.ts +15 -0
  134. package/dist/application/update/write-plan.js +115 -0
  135. package/dist/application/update/write-plan.js.map +1 -0
  136. package/dist/application/upgrade/use-case.d.ts +6 -0
  137. package/dist/application/upgrade/use-case.js +82 -0
  138. package/dist/application/upgrade/use-case.js.map +1 -0
  139. package/dist/args.d.ts +5 -65
  140. package/dist/args.js +4 -472
  141. package/dist/args.js.map +1 -1
  142. package/dist/artifact-lifecycle.d.ts +1 -21
  143. package/dist/artifact-lifecycle.js +1 -75
  144. package/dist/artifact-lifecycle.js.map +1 -1
  145. package/dist/catalogs.d.ts +1 -50
  146. package/dist/catalogs.js +1 -472
  147. package/dist/catalogs.js.map +1 -1
  148. package/dist/cli/args/contracts.d.ts +51 -0
  149. package/dist/cli/args/contracts.js +2 -0
  150. package/dist/cli/args/contracts.js.map +1 -0
  151. package/dist/cli/args/definitions.d.ts +6 -0
  152. package/dist/cli/args/definitions.js +188 -0
  153. package/dist/cli/args/definitions.js.map +1 -0
  154. package/dist/cli/args/help.d.ts +5 -0
  155. package/dist/cli/args/help.js +142 -0
  156. package/dist/cli/args/help.js.map +1 -0
  157. package/dist/cli/args/parser.d.ts +5 -0
  158. package/dist/cli/args/parser.js +160 -0
  159. package/dist/cli/args/parser.js.map +1 -0
  160. package/dist/cli/args/readers.d.ts +4 -0
  161. package/dist/cli/args/readers.js +19 -0
  162. package/dist/cli/args/readers.js.map +1 -0
  163. package/dist/cli/commands/diagnose.d.ts +4 -0
  164. package/dist/cli/commands/diagnose.js +12 -0
  165. package/dist/cli/commands/diagnose.js.map +1 -0
  166. package/dist/cli/commands/dispatch.d.ts +3 -0
  167. package/dist/cli/commands/dispatch.js +94 -0
  168. package/dist/cli/commands/dispatch.js.map +1 -0
  169. package/dist/cli/commands/help.d.ts +3 -0
  170. package/dist/cli/commands/help.js +32 -0
  171. package/dist/cli/commands/help.js.map +1 -0
  172. package/dist/cli/commands/helpers.d.ts +8 -0
  173. package/dist/cli/commands/helpers.js +97 -0
  174. package/dist/cli/commands/helpers.js.map +1 -0
  175. package/dist/cli/commands/initialize.d.ts +3 -0
  176. package/dist/cli/commands/initialize.js +7 -0
  177. package/dist/cli/commands/initialize.js.map +1 -0
  178. package/dist/cli/commands/migrate.d.ts +3 -0
  179. package/dist/cli/commands/migrate.js +8 -0
  180. package/dist/cli/commands/migrate.js.map +1 -0
  181. package/dist/cli/commands/plan.d.ts +3 -0
  182. package/dist/cli/commands/plan.js +34 -0
  183. package/dist/cli/commands/plan.js.map +1 -0
  184. package/dist/cli/commands/reference.d.ts +5 -0
  185. package/dist/cli/commands/reference.js +43 -0
  186. package/dist/cli/commands/reference.js.map +1 -0
  187. package/dist/cli/commands/update.d.ts +3 -0
  188. package/dist/cli/commands/update.js +10 -0
  189. package/dist/cli/commands/update.js.map +1 -0
  190. package/dist/cli/commands/upgrade.d.ts +3 -0
  191. package/dist/cli/commands/upgrade.js +7 -0
  192. package/dist/cli/commands/upgrade.js.map +1 -0
  193. package/dist/cli/context.d.ts +1 -0
  194. package/dist/cli/context.js +2 -0
  195. package/dist/cli/context.js.map +1 -0
  196. package/dist/cli/project-options.d.ts +3 -0
  197. package/dist/cli/project-options.js +41 -0
  198. package/dist/cli/project-options.js.map +1 -0
  199. package/dist/cli.d.ts +3 -3
  200. package/dist/cli.js +2 -2
  201. package/dist/cli.js.map +1 -1
  202. package/dist/commands.d.ts +4 -35
  203. package/dist/commands.js +3 -2969
  204. package/dist/commands.js.map +1 -1
  205. package/dist/container-validation.d.ts +1 -1
  206. package/dist/domain/governance/activation/approvals.d.ts +351 -0
  207. package/dist/domain/governance/activation/approvals.js +528 -0
  208. package/dist/domain/governance/activation/approvals.js.map +1 -0
  209. package/dist/domain/governance/activation/canonical-json.d.ts +4 -0
  210. package/dist/domain/governance/activation/canonical-json.js +39 -0
  211. package/dist/domain/governance/activation/canonical-json.js.map +1 -0
  212. package/dist/domain/governance/activation/capabilities.d.ts +7 -0
  213. package/dist/domain/governance/activation/capabilities.js +33 -0
  214. package/dist/domain/governance/activation/capabilities.js.map +1 -0
  215. package/dist/domain/governance/activation/evidence.d.ts +63 -0
  216. package/dist/domain/governance/activation/evidence.js +444 -0
  217. package/dist/domain/governance/activation/evidence.js.map +1 -0
  218. package/dist/domain/governance/activation/graph.d.ts +397 -0
  219. package/dist/domain/governance/activation/graph.js +441 -0
  220. package/dist/domain/governance/activation/graph.js.map +1 -0
  221. package/dist/domain/governance/activation/inputs.d.ts +25 -0
  222. package/dist/domain/governance/activation/inputs.js +63 -0
  223. package/dist/domain/governance/activation/inputs.js.map +1 -0
  224. package/dist/domain/governance/activation/operations.d.ts +19 -0
  225. package/dist/domain/governance/activation/operations.js +181 -0
  226. package/dist/domain/governance/activation/operations.js.map +1 -0
  227. package/dist/domain/governance/activation/readiness.d.ts +27 -0
  228. package/dist/domain/governance/activation/readiness.js +269 -0
  229. package/dist/domain/governance/activation/readiness.js.map +1 -0
  230. package/dist/domain/governance/activation/types.d.ts +399 -0
  231. package/dist/domain/governance/activation/types.js +109 -0
  232. package/dist/domain/governance/activation/types.js.map +1 -0
  233. package/dist/domain/governance/activation/validators.d.ts +22 -0
  234. package/dist/domain/governance/activation/validators.js +1347 -0
  235. package/dist/domain/governance/activation/validators.js.map +1 -0
  236. package/dist/domain/governance/assessment/catalog.d.ts +14 -0
  237. package/dist/domain/governance/assessment/catalog.js +105 -0
  238. package/dist/domain/governance/assessment/catalog.js.map +1 -0
  239. package/dist/domain/governance/assessment/errors.d.ts +10 -0
  240. package/dist/domain/governance/assessment/errors.js +22 -0
  241. package/dist/domain/governance/assessment/errors.js.map +1 -0
  242. package/dist/domain/governance/assessment/index.d.ts +8 -0
  243. package/dist/domain/governance/assessment/index.js +9 -0
  244. package/dist/domain/governance/assessment/index.js.map +1 -0
  245. package/dist/domain/governance/assessment/live-normalize.d.ts +19 -0
  246. package/dist/domain/governance/assessment/live-normalize.js +403 -0
  247. package/dist/domain/governance/assessment/live-normalize.js.map +1 -0
  248. package/dist/domain/governance/assessment/predicates.d.ts +36 -0
  249. package/dist/domain/governance/assessment/predicates.js +545 -0
  250. package/dist/domain/governance/assessment/predicates.js.map +1 -0
  251. package/dist/domain/governance/assessment/report.d.ts +23 -0
  252. package/dist/domain/governance/assessment/report.js +266 -0
  253. package/dist/domain/governance/assessment/report.js.map +1 -0
  254. package/dist/domain/governance/assessment/sanitize.d.ts +8 -0
  255. package/dist/domain/governance/assessment/sanitize.js +60 -0
  256. package/dist/domain/governance/assessment/sanitize.js.map +1 -0
  257. package/dist/domain/governance/assessment/types.d.ts +174 -0
  258. package/dist/domain/governance/assessment/types.js +13 -0
  259. package/dist/domain/governance/assessment/types.js.map +1 -0
  260. package/dist/domain/governance/assessment/yaml.d.ts +6 -0
  261. package/dist/domain/governance/assessment/yaml.js +30 -0
  262. package/dist/domain/governance/assessment/yaml.js.map +1 -0
  263. package/dist/domain/governance/policy/identity.d.ts +64 -0
  264. package/dist/domain/governance/policy/identity.js +112 -0
  265. package/dist/domain/governance/policy/identity.js.map +1 -0
  266. package/dist/domain/migration/dependencies.d.ts +7 -0
  267. package/dist/domain/migration/dependencies.js +221 -0
  268. package/dist/domain/migration/dependencies.js.map +1 -0
  269. package/dist/domain/migration/inventory.d.ts +3 -0
  270. package/dist/domain/migration/inventory.js +15 -0
  271. package/dist/domain/migration/inventory.js.map +1 -0
  272. package/dist/domain/project/artifact-lifecycle.d.ts +69 -0
  273. package/dist/domain/project/artifact-lifecycle.js +137 -0
  274. package/dist/domain/project/artifact-lifecycle.js.map +1 -0
  275. package/dist/domain/project/catalog.d.ts +48 -0
  276. package/dist/domain/project/catalog.js +507 -0
  277. package/dist/domain/project/catalog.js.map +1 -0
  278. package/dist/domain/project/contracts.d.ts +247 -0
  279. package/dist/domain/project/contracts.js +2 -0
  280. package/dist/domain/project/contracts.js.map +1 -0
  281. package/dist/domain/project/errors.d.ts +3 -0
  282. package/dist/domain/project/errors.js +7 -0
  283. package/dist/domain/project/errors.js.map +1 -0
  284. package/dist/domain/project/infrastructure-layout.d.ts +26 -0
  285. package/dist/domain/project/infrastructure-layout.js +125 -0
  286. package/dist/domain/project/infrastructure-layout.js.map +1 -0
  287. package/dist/domain/project/inputs.d.ts +21 -0
  288. package/dist/domain/project/inputs.js +41 -0
  289. package/dist/domain/project/inputs.js.map +1 -0
  290. package/dist/domain/project/manifest/artifacts.d.ts +8 -0
  291. package/dist/domain/project/manifest/artifacts.js +150 -0
  292. package/dist/domain/project/manifest/artifacts.js.map +1 -0
  293. package/dist/domain/project/manifest/context.d.ts +13 -0
  294. package/dist/domain/project/manifest/context.js +2 -0
  295. package/dist/domain/project/manifest/context.js.map +1 -0
  296. package/dist/domain/project/manifest/fields.d.ts +6 -0
  297. package/dist/domain/project/manifest/fields.js +36 -0
  298. package/dist/domain/project/manifest/fields.js.map +1 -0
  299. package/dist/domain/project/manifest/governance.d.ts +8 -0
  300. package/dist/domain/project/manifest/governance.js +161 -0
  301. package/dist/domain/project/manifest/governance.js.map +1 -0
  302. package/dist/domain/project/manifest/project-identity.d.ts +6 -0
  303. package/dist/domain/project/manifest/project-identity.js +273 -0
  304. package/dist/domain/project/manifest/project-identity.js.map +1 -0
  305. package/dist/domain/project/manifest/reader.d.ts +9 -0
  306. package/dist/domain/project/manifest/reader.js +141 -0
  307. package/dist/domain/project/manifest/reader.js.map +1 -0
  308. package/dist/domain/project/paths.d.ts +2 -0
  309. package/dist/domain/project/paths.js +29 -0
  310. package/dist/domain/project/paths.js.map +1 -0
  311. package/dist/domain/project/planning.d.ts +19 -0
  312. package/dist/domain/project/planning.js +329 -0
  313. package/dist/domain/project/planning.js.map +1 -0
  314. package/dist/domain/project/retired-workload.d.ts +4 -0
  315. package/dist/domain/project/retired-workload.js +18 -0
  316. package/dist/domain/project/retired-workload.js.map +1 -0
  317. package/dist/domain/project/reviewed-update-artifacts.d.ts +2 -0
  318. package/dist/domain/project/reviewed-update-artifacts.js +3 -0
  319. package/dist/domain/project/reviewed-update-artifacts.js.map +1 -0
  320. package/dist/domain/project/supported-stack.d.ts +113 -0
  321. package/dist/domain/project/supported-stack.js +426 -0
  322. package/dist/domain/project/supported-stack.js.map +1 -0
  323. package/dist/domain/workstation/constraints.d.ts +6 -0
  324. package/dist/domain/workstation/constraints.js +8 -0
  325. package/dist/domain/workstation/constraints.js.map +1 -0
  326. package/dist/file-system.d.ts +10 -44
  327. package/dist/file-system.js +9 -1245
  328. package/dist/file-system.js.map +1 -1
  329. package/dist/framework-adapters.d.ts +1 -1
  330. package/dist/framework-validation.d.ts +1 -1
  331. package/dist/framework-validation.js +1 -1
  332. package/dist/framework-validation.js.map +1 -1
  333. package/dist/genai-templates.d.ts +2 -9
  334. package/dist/genai-templates.js +4 -5
  335. package/dist/genai-templates.js.map +1 -1
  336. package/dist/generators/common/artifacts.d.ts +6 -0
  337. package/dist/generators/common/artifacts.js +30 -0
  338. package/dist/generators/common/artifacts.js.map +1 -0
  339. package/dist/generators/common/base.d.ts +17 -0
  340. package/dist/generators/common/base.js +425 -0
  341. package/dist/generators/common/base.js.map +1 -0
  342. package/dist/generators/common/dotenv-validation.d.ts +2 -0
  343. package/dist/generators/common/dotenv-validation.js +80 -0
  344. package/dist/generators/common/dotenv-validation.js.map +1 -0
  345. package/dist/generators/common/environments.d.ts +3 -0
  346. package/dist/generators/common/environments.js +15 -0
  347. package/dist/generators/common/environments.js.map +1 -0
  348. package/dist/generators/common/frontend.d.ts +15 -0
  349. package/dist/generators/common/frontend.js +166 -0
  350. package/dist/generators/common/frontend.js.map +1 -0
  351. package/dist/generators/common/python-settings.d.ts +2 -0
  352. package/dist/generators/common/python-settings.js +70 -0
  353. package/dist/generators/common/python-settings.js.map +1 -0
  354. package/dist/generators/common/spec-workflow.d.ts +28 -0
  355. package/dist/generators/common/spec-workflow.js +567 -0
  356. package/dist/generators/common/spec-workflow.js.map +1 -0
  357. package/dist/generators/common/values.d.ts +13 -0
  358. package/dist/generators/common/values.js +16 -0
  359. package/dist/generators/common/values.js.map +1 -0
  360. package/dist/generators/containers/compose.d.ts +7 -0
  361. package/dist/generators/containers/compose.js +196 -0
  362. package/dist/generators/containers/compose.js.map +1 -0
  363. package/dist/generators/containers/context.d.ts +1 -0
  364. package/dist/generators/containers/context.js +43 -0
  365. package/dist/generators/containers/context.js.map +1 -0
  366. package/dist/generators/containers/images.d.ts +7 -0
  367. package/dist/generators/containers/images.js +119 -0
  368. package/dist/generators/containers/images.js.map +1 -0
  369. package/dist/generators/context.d.ts +42 -0
  370. package/dist/generators/context.js +38 -0
  371. package/dist/generators/context.js.map +1 -0
  372. package/dist/generators/genai/backend.d.ts +20 -0
  373. package/dist/generators/genai/backend.js +667 -0
  374. package/dist/generators/genai/backend.js.map +1 -0
  375. package/dist/generators/genai/configuration.d.ts +3 -0
  376. package/dist/generators/genai/configuration.js +43 -0
  377. package/dist/generators/genai/configuration.js.map +1 -0
  378. package/dist/generators/genai/database.d.ts +7 -0
  379. package/dist/generators/genai/database.js +71 -0
  380. package/dist/generators/genai/database.js.map +1 -0
  381. package/dist/generators/genai/functions.d.ts +15 -0
  382. package/dist/generators/genai/functions.js +156 -0
  383. package/dist/generators/genai/functions.js.map +1 -0
  384. package/dist/generators/genai/index.d.ts +8 -0
  385. package/dist/generators/genai/index.js +11 -0
  386. package/dist/generators/genai/index.js.map +1 -0
  387. package/dist/generators/genai/patterns.d.ts +9 -0
  388. package/dist/generators/genai/patterns.js +309 -0
  389. package/dist/generators/genai/patterns.js.map +1 -0
  390. package/dist/generators/infrastructure/azure.d.ts +18 -0
  391. package/dist/generators/infrastructure/azure.js +655 -0
  392. package/dist/generators/infrastructure/azure.js.map +1 -0
  393. package/dist/generators/infrastructure/names.d.ts +23 -0
  394. package/dist/generators/infrastructure/names.js +51 -0
  395. package/dist/generators/infrastructure/names.js.map +1 -0
  396. package/dist/generators/standard/configuration.d.ts +3 -0
  397. package/dist/generators/standard/configuration.js +26 -0
  398. package/dist/generators/standard/configuration.js.map +1 -0
  399. package/dist/generators/standard/go.d.ts +17 -0
  400. package/dist/generators/standard/go.js +338 -0
  401. package/dist/generators/standard/go.js.map +1 -0
  402. package/dist/generators/standard/index.d.ts +12 -0
  403. package/dist/generators/standard/index.js +12 -0
  404. package/dist/generators/standard/index.js.map +1 -0
  405. package/dist/generators/standard/node.d.ts +20 -0
  406. package/dist/generators/standard/node.js +314 -0
  407. package/dist/generators/standard/node.js.map +1 -0
  408. package/dist/generators/standard/python.d.ts +18 -0
  409. package/dist/generators/standard/python.js +191 -0
  410. package/dist/generators/standard/python.js.map +1 -0
  411. package/dist/go-template-assets.js +3 -3
  412. package/dist/go-template-assets.js.map +1 -1
  413. package/dist/governance-activation/activation-state.d.ts +1 -1
  414. package/dist/governance-activation/activation-state.js +49 -9
  415. package/dist/governance-activation/activation-state.js.map +1 -1
  416. package/dist/governance-activation/approvals.d.ts +1 -188
  417. package/dist/governance-activation/approvals.js +1 -507
  418. package/dist/governance-activation/approvals.js.map +1 -1
  419. package/dist/governance-activation/canonical-json.d.ts +1 -3
  420. package/dist/governance-activation/canonical-json.js +1 -35
  421. package/dist/governance-activation/canonical-json.js.map +1 -1
  422. package/dist/governance-activation/commands.d.ts +6 -1
  423. package/dist/governance-activation/commands.js +135 -107
  424. package/dist/governance-activation/commands.js.map +1 -1
  425. package/dist/governance-activation/compatibility.d.ts +30 -5
  426. package/dist/governance-activation/compatibility.js +74 -8
  427. package/dist/governance-activation/compatibility.js.map +1 -1
  428. package/dist/governance-activation/credentials.d.ts +3 -3
  429. package/dist/governance-activation/credentials.js +5 -5
  430. package/dist/governance-activation/credentials.js.map +1 -1
  431. package/dist/governance-activation/doctor.d.ts +1 -1
  432. package/dist/governance-activation/doctor.js +59 -100
  433. package/dist/governance-activation/doctor.js.map +1 -1
  434. package/dist/governance-activation/evidence.d.ts +1 -49
  435. package/dist/governance-activation/evidence.js +1 -245
  436. package/dist/governance-activation/evidence.js.map +1 -1
  437. package/dist/governance-activation/git-ignore.d.ts +9 -0
  438. package/dist/governance-activation/git-ignore.js +76 -0
  439. package/dist/governance-activation/git-ignore.js.map +1 -0
  440. package/dist/governance-activation/graph.d.ts +1 -397
  441. package/dist/governance-activation/graph.js +1 -440
  442. package/dist/governance-activation/graph.js.map +1 -1
  443. package/dist/governance-activation/historical-state.d.ts +81 -0
  444. package/dist/governance-activation/historical-state.js +817 -0
  445. package/dist/governance-activation/historical-state.js.map +1 -0
  446. package/dist/governance-activation/history-contracts.d.ts +109 -0
  447. package/dist/governance-activation/history-contracts.js +303 -0
  448. package/dist/governance-activation/history-contracts.js.map +1 -0
  449. package/dist/governance-activation/identity.d.ts +1 -35
  450. package/dist/governance-activation/identity.js +1 -88
  451. package/dist/governance-activation/identity.js.map +1 -1
  452. package/dist/governance-activation/index.d.ts +3 -0
  453. package/dist/governance-activation/index.js +3 -0
  454. package/dist/governance-activation/index.js.map +1 -1
  455. package/dist/governance-activation/inputs.d.ts +10 -0
  456. package/dist/governance-activation/inputs.js +158 -0
  457. package/dist/governance-activation/inputs.js.map +1 -0
  458. package/dist/governance-activation/migration-history.d.ts +97 -0
  459. package/dist/governance-activation/migration-history.js +388 -0
  460. package/dist/governance-activation/migration-history.js.map +1 -0
  461. package/dist/governance-activation/migration.d.ts +4 -3
  462. package/dist/governance-activation/migration.js +14 -6
  463. package/dist/governance-activation/migration.js.map +1 -1
  464. package/dist/governance-activation/phase-bootstrap-state.d.ts +3 -0
  465. package/dist/governance-activation/phase-bootstrap-state.js +128 -0
  466. package/dist/governance-activation/phase-bootstrap-state.js.map +1 -0
  467. package/dist/governance-activation/phase-discovery.d.ts +2 -0
  468. package/dist/governance-activation/phase-discovery.js +88 -0
  469. package/dist/governance-activation/phase-discovery.js.map +1 -0
  470. package/dist/governance-activation/phase-governance.d.ts +4 -0
  471. package/dist/governance-activation/phase-governance.js +141 -0
  472. package/dist/governance-activation/phase-governance.js.map +1 -0
  473. package/dist/governance-activation/phase-publication.d.ts +8 -0
  474. package/dist/governance-activation/phase-publication.js +318 -0
  475. package/dist/governance-activation/phase-publication.js.map +1 -0
  476. package/dist/governance-activation/proof-records.d.ts +3 -0
  477. package/dist/governance-activation/proof-records.js +78 -0
  478. package/dist/governance-activation/proof-records.js.map +1 -0
  479. package/dist/governance-activation/read-only.d.ts +30 -0
  480. package/dist/governance-activation/read-only.js +33 -0
  481. package/dist/governance-activation/read-only.js.map +1 -0
  482. package/dist/governance-activation/readiness.d.ts +1 -25
  483. package/dist/governance-activation/readiness.js +1 -222
  484. package/dist/governance-activation/readiness.js.map +1 -1
  485. package/dist/governance-activation/reconciliation.d.ts +1 -1
  486. package/dist/governance-activation/reconciliation.js +3 -3
  487. package/dist/governance-activation/reconciliation.js.map +1 -1
  488. package/dist/governance-activation/release-integrity.d.ts +1 -1
  489. package/dist/governance-activation/release-integrity.js +2 -2
  490. package/dist/governance-activation/release-integrity.js.map +1 -1
  491. package/dist/governance-activation/seed-lifecycle.d.ts +36 -6
  492. package/dist/governance-activation/seed-lifecycle.js +444 -77
  493. package/dist/governance-activation/seed-lifecycle.js.map +1 -1
  494. package/dist/governance-activation/source-of-truth.d.ts +5 -3
  495. package/dist/governance-activation/source-of-truth.js +32 -52
  496. package/dist/governance-activation/source-of-truth.js.map +1 -1
  497. package/dist/governance-activation/spec-kit-seed.d.ts +9 -0
  498. package/dist/governance-activation/spec-kit-seed.js +43 -0
  499. package/dist/governance-activation/spec-kit-seed.js.map +1 -0
  500. package/dist/governance-activation/task-projection.d.ts +1 -1
  501. package/dist/governance-activation/transition-planning.d.ts +17 -0
  502. package/dist/governance-activation/transition-planning.js +259 -0
  503. package/dist/governance-activation/transition-planning.js.map +1 -0
  504. package/dist/governance-activation/transition-ports.d.ts +150 -0
  505. package/dist/governance-activation/transition-ports.js +2 -0
  506. package/dist/governance-activation/transition-ports.js.map +1 -0
  507. package/dist/governance-activation/transition-process.d.ts +7 -0
  508. package/dist/governance-activation/transition-process.js +21 -0
  509. package/dist/governance-activation/transition-process.js.map +1 -0
  510. package/dist/governance-activation/transition-records.d.ts +56 -0
  511. package/dist/governance-activation/transition-records.js +208 -0
  512. package/dist/governance-activation/transition-records.js.map +1 -0
  513. package/dist/governance-activation/transitions.d.ts +14 -167
  514. package/dist/governance-activation/transitions.js +173 -2061
  515. package/dist/governance-activation/transitions.js.map +1 -1
  516. package/dist/governance-activation/types.d.ts +1 -390
  517. package/dist/governance-activation/types.js +1 -107
  518. package/dist/governance-activation/types.js.map +1 -1
  519. package/dist/governance-activation/validators.d.ts +1 -17
  520. package/dist/governance-activation/validators.js +1 -1304
  521. package/dist/governance-activation/validators.js.map +1 -1
  522. package/dist/governance-assessment/catalog.d.ts +1 -2
  523. package/dist/governance-assessment/catalog.js +18 -85
  524. package/dist/governance-assessment/catalog.js.map +1 -1
  525. package/dist/governance-assessment/command.d.ts +1 -1
  526. package/dist/governance-assessment/command.js +8 -12
  527. package/dist/governance-assessment/command.js.map +1 -1
  528. package/dist/governance-assessment/engine.d.ts +6 -11
  529. package/dist/governance-assessment/engine.js +490 -114
  530. package/dist/governance-assessment/engine.js.map +1 -1
  531. package/dist/governance-assessment/live-normalize.d.ts +1 -19
  532. package/dist/governance-assessment/live-normalize.js +1 -402
  533. package/dist/governance-assessment/live-normalize.js.map +1 -1
  534. package/dist/governance-assessment/live-transport.d.ts +2 -6
  535. package/dist/governance-assessment/live-transport.js +10 -22
  536. package/dist/governance-assessment/live-transport.js.map +1 -1
  537. package/dist/governance-assessment/live.js +55 -5
  538. package/dist/governance-assessment/live.js.map +1 -1
  539. package/dist/governance-assessment/predicates.d.ts +1 -27
  540. package/dist/governance-assessment/predicates.js +1 -329
  541. package/dist/governance-assessment/predicates.js.map +1 -1
  542. package/dist/governance-assessment/project.d.ts +20 -17
  543. package/dist/governance-assessment/project.js +101 -151
  544. package/dist/governance-assessment/project.js.map +1 -1
  545. package/dist/governance-assessment/readers.d.ts +1 -5
  546. package/dist/governance-assessment/readers.js +5 -13
  547. package/dist/governance-assessment/readers.js.map +1 -1
  548. package/dist/governance-assessment/report.d.ts +1 -23
  549. package/dist/governance-assessment/report.js +1 -265
  550. package/dist/governance-assessment/report.js.map +1 -1
  551. package/dist/governance-assessment/sanitize.d.ts +1 -8
  552. package/dist/governance-assessment/sanitize.js +1 -56
  553. package/dist/governance-assessment/sanitize.js.map +1 -1
  554. package/dist/governance-assessment/types.d.ts +1 -173
  555. package/dist/governance-assessment/types.js +1 -12
  556. package/dist/governance-assessment/types.js.map +1 -1
  557. package/dist/governance-assessment/yaml.d.ts +1 -6
  558. package/dist/governance-assessment/yaml.js +1 -29
  559. package/dist/governance-assessment/yaml.js.map +1 -1
  560. package/dist/init-filesystem.d.ts +1 -1
  561. package/dist/init-filesystem.js +126 -174
  562. package/dist/init-filesystem.js.map +1 -1
  563. package/dist/interactive.d.ts +2 -2
  564. package/dist/interactive.js +42 -41
  565. package/dist/interactive.js.map +1 -1
  566. package/dist/migrate-plan.d.ts +1 -1
  567. package/dist/migrate-plan.js +14 -8
  568. package/dist/migrate-plan.js.map +1 -1
  569. package/dist/npm-template-assets.js +5 -4
  570. package/dist/npm-template-assets.js.map +1 -1
  571. package/dist/openspec-profile.d.ts +1 -1
  572. package/dist/opentofu-template-assets.js +3 -3
  573. package/dist/opentofu-template-assets.js.map +1 -1
  574. package/dist/package-identity.d.ts +3 -0
  575. package/dist/package-identity.js +13 -2
  576. package/dist/package-identity.js.map +1 -1
  577. package/dist/planner.d.ts +1 -19
  578. package/dist/planner.js +1 -565
  579. package/dist/planner.js.map +1 -1
  580. package/dist/process-runner.d.ts +1 -1
  581. package/dist/project-dependencies.d.ts +5 -3
  582. package/dist/project-dependencies.js +74 -63
  583. package/dist/project-dependencies.js.map +1 -1
  584. package/dist/published-verifier.js +61 -30
  585. package/dist/published-verifier.js.map +1 -1
  586. package/dist/python-template-assets.js +6 -9
  587. package/dist/python-template-assets.js.map +1 -1
  588. package/dist/reconcile.d.ts +1 -1
  589. package/dist/reconcile.js +2 -2
  590. package/dist/reconcile.js.map +1 -1
  591. package/dist/repository-governance.d.ts +5 -2
  592. package/dist/repository-governance.js +140 -113
  593. package/dist/repository-governance.js.map +1 -1
  594. package/dist/runtime.js +1 -1
  595. package/dist/runtime.js.map +1 -1
  596. package/dist/scan.d.ts +6 -2
  597. package/dist/scan.js +78 -30
  598. package/dist/scan.js.map +1 -1
  599. package/dist/self-upgrade.d.ts +1 -1
  600. package/dist/self-upgrade.js +20 -7
  601. package/dist/self-upgrade.js.map +1 -1
  602. package/dist/stable-release.js +7 -1
  603. package/dist/stable-release.js.map +1 -1
  604. package/dist/standard-templates.d.ts +2 -2
  605. package/dist/standard-templates.js +6 -841
  606. package/dist/standard-templates.js.map +1 -1
  607. package/dist/supported-stack.d.ts +3 -115
  608. package/dist/supported-stack.js +4 -438
  609. package/dist/supported-stack.js.map +1 -1
  610. package/dist/telemetry/index.d.ts +1 -1
  611. package/dist/templates.d.ts +10 -20
  612. package/dist/templates.js +52 -3261
  613. package/dist/templates.js.map +1 -1
  614. package/dist/types.d.ts +1 -263
  615. package/dist/types.js +1 -1
  616. package/dist/types.js.map +1 -1
  617. package/dist/version.js +2 -6
  618. package/dist/version.js.map +1 -1
  619. package/dist/workstation-catalog.d.ts +5 -2
  620. package/dist/workstation-catalog.js +33 -2
  621. package/dist/workstation-catalog.js.map +1 -1
  622. package/dist/workstation.d.ts +4 -3
  623. package/dist/workstation.js +86 -32
  624. package/dist/workstation.js.map +1 -1
  625. package/docs/assets/liftoff-terminal.svg +3 -3
  626. package/docs/azure-deployment.md +107 -10
  627. package/docs/cli-reference.md +144 -35
  628. package/docs/configuration-and-manifests.md +152 -35
  629. package/docs/existing-repositories.md +48 -12
  630. package/docs/getting-started.md +44 -31
  631. package/docs/prerequisites.md +34 -35
  632. package/docs/project-structure.md +39 -46
  633. package/docs/repository-governance.md +96 -26
  634. package/docs/safety-and-consent.md +90 -28
  635. package/docs/spec-workflows-and-agents.md +31 -28
  636. package/docs/supported-stack.md +11 -9
  637. package/docs/troubleshooting.md +114 -54
  638. package/docs/workloads.md +52 -67
  639. package/package.json +4 -7
  640. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/UPSTREAM_LICENSE.txt +0 -21
  641. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/catalog.json +0 -520
  642. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/packaged/gitignore +0 -27
  643. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/README.md +0 -25
  644. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/components.json +0 -22
  645. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/eslint.config.js +0 -23
  646. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/index.html +0 -13
  647. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/package-lock.json +0 -6156
  648. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/package.json +0 -60
  649. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/public/power-apps.svg +0 -55
  650. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/App.tsx +0 -17
  651. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/assets/react.svg +0 -1
  652. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/mode-toggle.tsx +0 -37
  653. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/badge.tsx +0 -46
  654. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/button.tsx +0 -60
  655. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/calendar.tsx +0 -211
  656. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/card.tsx +0 -92
  657. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/chart.tsx +0 -355
  658. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/checkbox.tsx +0 -32
  659. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/command.tsx +0 -184
  660. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/dialog.tsx +0 -141
  661. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/dropdown-menu.tsx +0 -257
  662. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/input.tsx +0 -21
  663. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/label.tsx +0 -22
  664. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/popover.tsx +0 -46
  665. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/progress.tsx +0 -29
  666. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/select.tsx +0 -185
  667. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/separator.tsx +0 -26
  668. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/skeleton.tsx +0 -13
  669. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/table.tsx +0 -114
  670. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/tabs.tsx +0 -66
  671. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/textarea.tsx +0 -18
  672. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/tooltip.tsx +0 -61
  673. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/hooks/use-theme.ts +0 -11
  674. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/index.css +0 -131
  675. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/lib/utils.ts +0 -6
  676. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/main.tsx +0 -10
  677. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/pages/_layout.tsx +0 -31
  678. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/pages/home.tsx +0 -59
  679. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/pages/not-found.tsx +0 -14
  680. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/providers/query-provider.tsx +0 -26
  681. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/providers/sonner-provider.tsx +0 -23
  682. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/providers/theme-provider.tsx +0 -64
  683. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/router.tsx +0 -24
  684. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/tsconfig.app.json +0 -34
  685. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/tsconfig.json +0 -13
  686. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/tsconfig.node.json +0 -26
  687. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/vite.config.ts +0 -19
  688. package/assets/power-apps-code-app/README.md +0 -27
  689. package/dist/code-apps-plugin.d.ts +0 -13
  690. package/dist/code-apps-plugin.js +0 -69
  691. package/dist/code-apps-plugin.js.map +0 -1
  692. package/dist/power-apps-assets.d.ts +0 -30
  693. package/dist/power-apps-assets.js +0 -131
  694. package/dist/power-apps-assets.js.map +0 -1
  695. package/dist/power-apps-templates.d.ts +0 -3
  696. package/dist/power-apps-templates.js +0 -177
  697. package/dist/power-apps-templates.js.map +0 -1
  698. package/dist/power-apps-validation.d.ts +0 -1
  699. package/dist/power-apps-validation.js +0 -79
  700. package/dist/power-apps-validation.js.map +0 -1
package/dist/templates.js CHANGED
@@ -1,74 +1,28 @@
1
- import { createHash } from 'node:crypto';
2
- import { addGenAiExtensionArtifacts } from './genai-templates.js';
1
+ import { createGeneratorContext } from './generators/context.js';
2
+ import { packagedTemplateAssets } from './adapters/packaged-assets/template-assets.js';
3
+ import { packagedSupportedStack } from './adapters/packaged-assets/supported-stack.js';
4
+ import { addBaseArtifacts } from './generators/common/base.js';
5
+ import { addDockerArtifacts } from './generators/containers/compose.js';
6
+ import { addEnvironmentArtifacts } from './generators/common/environments.js';
7
+ import { addFrontendArtifacts } from './generators/common/frontend.js';
8
+ import { addGenAiExtensionArtifacts } from './generators/genai/index.js';
9
+ import { addInfrastructureArtifacts } from './generators/infrastructure/azure.js';
10
+ import { addSpecWorkflowArtifacts } from './generators/common/spec-workflow.js';
11
+ import { addStandardStackArtifacts } from './generators/standard/index.js';
3
12
  import { assertImmutableGeneratedContainerReferences } from './container-validation.js';
4
- import { addPowerAppsCodeAppArtifacts } from './power-apps-templates.js';
5
- import { addStandardStackArtifacts, renderStandardDockerfile, renderStandardEnv } from './standard-templates.js';
6
- import { liftoffVersion } from './version.js';
7
- import { renderNpmLock, renderNpmPackage } from './npm-template-assets.js';
8
- import { renderOpenTofuProviderLock, renderOpenTofuVersions } from './opentofu-template-assets.js';
9
- import { formatCommand } from './process-runner.js';
10
- import { formatContainerImage, supportedStack } from './supported-stack.js';
11
- import { buildRepositoryGovernanceArtifacts, governancePolicyVersion, renderGovernanceAssessmentGuide } from './repository-governance.js';
13
+ import { buildRepositoryGovernanceArtifacts } from './repository-governance.js';
14
+ import { createArtifactAdder } from './generators/common/artifacts.js';
15
+ import { createHash } from 'node:crypto';
12
16
  import { currentActivationIdentity } from './governance-activation/graph.js';
13
- import { OPEN_SPEC_DELIVERY, OPEN_SPEC_PROFILE, OPEN_SPEC_WORKFLOW_IDS } from './openspec-profile.js';
14
- import { renderFunctionRequirementsAsset, renderPythonLock, renderPythonPyprojectAsset } from './python-template-assets.js';
15
- import { workstationRequirementCatalog } from './workstation-catalog.js';
17
+ import { ensureTrailingNewline } from './generators/common/artifacts.js';
18
+ import { governancePolicyVersion } from './repository-governance.js';
19
+ import { liftoffVersion } from './version.js';
16
20
  const contentHash = (content) => `sha256:${createHash('sha256').update(content, 'utf8').digest('hex')}`;
17
- const DEFAULT_FUNCTION_WORKER_QUEUE_NAME = 'events';
18
- export const AZURE_NAME_LIMITS = {
19
- resourceGroup: 90,
20
- containerRegistry: 50,
21
- identity: 128,
22
- containerAppEnvironment: 60,
23
- backendContainerApp: 32,
24
- frontendContainerApp: 32,
25
- functionServicePlan: 40,
26
- functionApp: 60,
27
- postgres: 63,
28
- redis: 63,
29
- storage: 24,
30
- serviceBus: 50,
31
- communication: 63,
32
- keyVault: 24
33
- };
34
- const boundedToken = (value, length) => value.slice(0, length).replace(/-+$/g, '') || 'app';
35
- export function buildAzureResourceNames(plan, environment, resourceSuffix) {
36
- const workload = boundedToken(plan.safeProjectName, 12);
37
- const compactWorkload = boundedToken(plan.safeProjectName.replace(/-/g, ''), 8);
38
- return {
39
- resourceGroup: `rg-${workload}-${environment}`,
40
- containerRegistry: `acr${compactWorkload}${resourceSuffix}`,
41
- identity: `id-${workload}-${environment}`,
42
- containerAppEnvironment: `cae-${workload}-${environment}`,
43
- backendContainerApp: `ca-${workload}-be-${environment}`,
44
- frontendContainerApp: `ca-${workload}-fe-${environment}`,
45
- functionServicePlan: `asp-${workload}-fn-${environment}`,
46
- functionApp: `func-${workload}-${environment}-${resourceSuffix}`,
47
- postgres: `psql-${workload}-${environment}-${resourceSuffix}`,
48
- redis: `redis-${workload}-${environment}-${resourceSuffix}`,
49
- storage: `st${compactWorkload}${resourceSuffix}`,
50
- serviceBus: `sb-${workload}-${environment}-${resourceSuffix}`,
51
- communication: `acs-${workload}-${environment}-${resourceSuffix}`,
52
- keyVault: `kv-${compactWorkload}-${resourceSuffix}`
53
- };
54
- }
55
- function stableResourceSuffix(plan, environment) {
56
- return createHash('sha256')
57
- .update(`${plan.safeProjectName}:${environment}`, 'utf8')
58
- .digest('hex')
59
- .slice(0, 12);
21
+ export { AZURE_NAME_LIMITS, buildAzureResourceNames } from './generators/infrastructure/names.js';
22
+ export function resolveGeneratorContext(plan, assets = packagedTemplateAssets) {
23
+ return createGeneratorContext(plan, assets, packagedSupportedStack);
60
24
  }
61
- const pyModule = (value) => value.replace(/-/g, '_');
62
- const titleCase = (value) => value.replace(/(^|[-_\s])([a-z])/g, (_match, prefix, letter) => `${prefix ? ' ' : ''}${letter.toUpperCase()}`).trim();
63
- const sourceString = (value) => JSON.stringify(value);
64
- const scriptSourceString = (value) => sourceString(value).replaceAll('<', '\\u003c');
65
- const escapeHtml = (value) => value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;');
66
- const genAiPattern = (plan) => {
67
- return plan.pattern;
68
- };
69
- const hasFunctionWorker = (plan) => plan.workload === 'genai' && plan.provider.id === 'azure' && plan.pattern.worker;
70
- const functionWorkerName = (plan) => `${plan.pattern.id}-worker`;
71
- export function buildArtifacts(plan) {
25
+ export function buildArtifacts(plan, context = resolveGeneratorContext(plan)) {
72
26
  const artifacts = [];
73
27
  const addProject = createArtifactAdder(artifacts, 'project', 'base');
74
28
  const addDesiredState = createArtifactAdder(artifacts, 'desired-state');
@@ -76,21 +30,18 @@ export function buildArtifacts(plan) {
76
30
  const addSeed = createArtifactAdder(artifacts, 'seed');
77
31
  switch (plan.workload) {
78
32
  case 'genai':
79
- addGenAiWorkloadArtifacts(addProject, addDesiredState, artifacts, plan);
33
+ addGenAiWorkloadArtifacts(addProject, addDesiredState, artifacts, plan, context);
80
34
  break;
81
35
  case 'standard':
82
- addStandardWorkloadArtifacts(addProject, addDesiredState, artifacts, plan);
83
- break;
84
- case 'power-apps-code-app':
85
- addPowerAppsWorkloadArtifacts(addDesiredState, artifacts, plan);
36
+ addStandardWorkloadArtifacts(addProject, addDesiredState, artifacts, plan, context);
86
37
  break;
87
38
  }
88
39
  addSpecWorkflowArtifacts(addSeed, addFramework, plan);
89
40
  for (const artifact of buildRepositoryGovernanceArtifacts(plan)) {
90
41
  artifacts.push({ ...artifact, content: ensureTrailingNewline(artifact.content) });
91
42
  }
92
- if (plan.workload !== 'power-apps-code-app' && plan.includeFrontend) {
93
- addFrontendArtifacts(createArtifactAdder(artifacts, 'project', 'frontend'), plan);
43
+ if (plan.includeFrontend) {
44
+ addFrontendArtifacts(createArtifactAdder(artifacts, 'project', 'frontend'), plan, context);
94
45
  }
95
46
  assertImmutableGeneratedContainerReferences(artifacts);
96
47
  const manifest = buildManifest(plan, artifacts);
@@ -103,55 +54,20 @@ export function buildArtifacts(plan) {
103
54
  });
104
55
  return artifacts;
105
56
  }
106
- function createArtifactAdder(artifacts, lifecycle, provisioningGroup) {
107
- return (logicalName, category, pathParts, content) => {
108
- const normalizedContent = ensureTrailingNewline(content);
109
- if (lifecycle === 'project') {
110
- if (!provisioningGroup) {
111
- throw new Error(`Project artifact ${logicalName} is missing a provisioning group.`);
112
- }
113
- artifacts.push({
114
- logicalName,
115
- category,
116
- lifecycle,
117
- provisioningGroup,
118
- pathParts,
119
- content: normalizedContent
120
- });
121
- return;
122
- }
123
- artifacts.push({
124
- logicalName,
125
- category,
126
- lifecycle,
127
- pathParts,
128
- content: normalizedContent
129
- });
130
- };
131
- }
132
- function addGenAiWorkloadArtifacts(add, addDesiredState, artifacts, plan) {
133
- addBaseArtifacts(add, addDesiredState, plan);
134
- addGenAiExtensionArtifacts(add, plan, {
135
- backend: addBackendArtifacts,
136
- database: addDatabaseArtifacts,
137
- pattern: addPatternArtifacts,
138
- functions: addFunctionArtifacts
139
- });
140
- addApiWorkloadOperations(add, artifacts, plan);
141
- }
142
- function addStandardWorkloadArtifacts(add, addDesiredState, artifacts, plan) {
143
- addBaseArtifacts(add, addDesiredState, plan);
144
- addStandardStackArtifacts(add, plan);
145
- addApiWorkloadOperations(add, artifacts, plan);
57
+ function addGenAiWorkloadArtifacts(add, addDesiredState, artifacts, plan, context) {
58
+ addBaseArtifacts(add, addDesiredState, plan, context);
59
+ addGenAiExtensionArtifacts(add, plan, context);
60
+ addApiWorkloadOperations(add, artifacts, plan, context);
146
61
  }
147
- function addPowerAppsWorkloadArtifacts(addDesiredState, artifacts, plan) {
148
- addPowerAppsCodeAppArtifacts(createArtifactAdder(artifacts, 'project', 'power-apps-starter'), plan);
149
- addPowerAppsProjectConfig(addDesiredState, plan);
62
+ function addStandardWorkloadArtifacts(add, addDesiredState, artifacts, plan, context) {
63
+ addBaseArtifacts(add, addDesiredState, plan, context);
64
+ addStandardStackArtifacts(add, plan, context);
65
+ addApiWorkloadOperations(add, artifacts, plan, context);
150
66
  }
151
- function addApiWorkloadOperations(add, artifacts, plan) {
67
+ function addApiWorkloadOperations(add, artifacts, plan, context) {
152
68
  addEnvironmentArtifacts(artifacts, plan);
153
- addDockerArtifacts(add, plan);
154
- addInfrastructureArtifacts(add, artifacts, plan);
69
+ addDockerArtifacts(add, plan, context);
70
+ addInfrastructureArtifacts(add, artifacts, plan, context);
155
71
  }
156
72
  export function partitionGeneratedArtifacts(artifacts) {
157
73
  const manifest = artifacts.find((artifact) => artifact.logicalName === 'manifest');
@@ -173,30 +89,24 @@ export function partitionGeneratedArtifacts(artifacts) {
173
89
  export function buildManifest(plan, artifacts, options = {}) {
174
90
  const frameworkState = options.frameworkState ?? 'initialized';
175
91
  const agents = frameworkState === 'initialized' ? plan.agents.map((agent) => agent.id) : [];
176
- const workload = plan.workload === 'power-apps-code-app'
92
+ const workload = plan.workload === 'genai'
177
93
  ? {
178
- kind: 'power-apps-code-app',
179
- starter: plan.starter,
180
- codeAppsPlugin: plan.codeAppsPlugin
94
+ kind: 'genai',
95
+ apiStack: plan.apiStack.id,
96
+ pattern: plan.pattern.id,
97
+ cloud: plan.provider.id,
98
+ region: plan.region.slug,
99
+ frontend: plan.includeFrontend,
100
+ environments: plan.environments.map((environment) => environment.id)
181
101
  }
182
- : plan.workload === 'genai'
183
- ? {
184
- kind: 'genai',
185
- apiStack: plan.apiStack.id,
186
- pattern: plan.pattern.id,
187
- cloud: plan.provider.id,
188
- region: plan.region.slug,
189
- frontend: plan.includeFrontend,
190
- environments: plan.environments.map((environment) => environment.id)
191
- }
192
- : {
193
- kind: 'standard',
194
- apiStack: plan.apiStack.id,
195
- cloud: plan.provider.id,
196
- region: plan.region.slug,
197
- frontend: plan.includeFrontend,
198
- environments: plan.environments.map((environment) => environment.id)
199
- };
102
+ : {
103
+ kind: 'standard',
104
+ apiStack: plan.apiStack.id,
105
+ cloud: plan.provider.id,
106
+ region: plan.region.slug,
107
+ frontend: plan.includeFrontend,
108
+ environments: plan.environments.map((environment) => environment.id)
109
+ };
200
110
  return {
201
111
  artifactVersion: 7,
202
112
  generatedBy: 'Mission Control Liftoff',
@@ -244,3123 +154,4 @@ export function buildManifest(plan, artifacts, options = {}) {
244
154
  }))
245
155
  };
246
156
  }
247
- function addBaseArtifacts(add, addDesiredState, plan) {
248
- add('root-readme', 'documentation', ['README.md'], renderRootReadme(plan));
249
- add('root-gitignore', 'project', ['.gitignore'], renderGeneratedGitignore());
250
- addDesiredState('liftoff-config', 'project', ['liftoff.config.json'], JSON.stringify({
251
- projectName: plan.projectName,
252
- projectType: plan.projectType.id,
253
- apiStack: plan.apiStack.id,
254
- ...(plan.workload === 'genai' ? { pattern: plan.pattern.id } : {}),
255
- cloud: plan.provider.id,
256
- region: plan.region.slug,
257
- includeFrontend: plan.includeFrontend,
258
- environments: plan.environments.map((environment) => environment.id),
259
- specWorkflow: plan.specWorkflow.id,
260
- agents: plan.agents.map((agent) => agent.id),
261
- ...(plan.defaultAgent ? { defaultAgent: plan.defaultAgent.id } : {}),
262
- governanceProfile: plan.governanceProfile.id
263
- }, null, 2));
264
- add('env-example', 'configuration', ['.env.example'], renderEnvExample(plan));
265
- add('backend-dockerfile', 'runtime', ['Dockerfile'], plan.workload === 'genai' ? renderBackendDockerfile() : renderStandardDockerfile(plan));
266
- }
267
- function addPowerAppsProjectConfig(add, plan) {
268
- add('liftoff-config', 'project', ['liftoff.config.json'], JSON.stringify({
269
- projectName: plan.projectName,
270
- projectType: plan.projectType.id,
271
- specWorkflow: plan.specWorkflow.id,
272
- agents: plan.agents.map((agent) => agent.id),
273
- ...(plan.defaultAgent ? { defaultAgent: plan.defaultAgent.id } : {}),
274
- codeAppsPlugin: plan.codeAppsPlugin,
275
- governanceProfile: plan.governanceProfile.id
276
- }, null, 2));
277
- }
278
- function addBackendArtifacts(add, plan) {
279
- const routeModule = pyModule(genAiPattern(plan).id);
280
- add('backend-pyproject', 'backend', ['backend', 'pyproject.toml'], renderBackendPyproject(plan));
281
- add('backend-uv-lock', 'backend', ['backend', 'uv.lock'], renderPythonLock('genai', `${plan.safeProjectName}-backend`));
282
- add('backend-package', 'backend', ['backend', '__init__.py'], '');
283
- add('backend-api-package', 'backend', ['backend', 'apis', '__init__.py'], '');
284
- add('backend-main', 'backend', ['backend', 'apis', 'main.py'], renderFastApiMain(plan, routeModule));
285
- add('backend-health-routes', 'backend', ['backend', 'apis', 'routes', 'health.py'], renderHealthRoutes());
286
- add('backend-pattern-routes', 'backend', ['backend', 'apis', 'routes', `${routeModule}.py`], renderPatternRoutes(plan));
287
- add('backend-routes-package', 'backend', ['backend', 'apis', 'routes', '__init__.py'], '');
288
- add('backend-auth-dependency', 'backend', ['backend', 'apis', 'dependencies', 'auth.py'], renderAuthDependency());
289
- add('backend-config-package', 'backend', ['backend', 'config', '__init__.py'], '');
290
- add('backend-settings', 'backend', ['backend', 'config', 'settings.py'], renderSettings(plan));
291
- add('backend-orchestration-package', 'backend', ['backend', 'orchestration', '__init__.py'], '');
292
- add('backend-model-config', 'backend', ['backend', 'orchestration', 'model_config.py'], renderModelConfig(plan));
293
- add('backend-messaging-tool', 'backend', ['backend', 'orchestration', 'tools', 'messaging.py'], renderMessagingBoundary());
294
- add('backend-tools-package', 'backend', ['backend', 'orchestration', 'tools', '__init__.py'], '');
295
- add('backend-observability', 'backend', ['backend', 'observability', 'tracing.py'], renderTracing());
296
- add('backend-observability-package', 'backend', ['backend', 'observability', '__init__.py'], '');
297
- add('backend-test-health', 'backend-test', ['backend', 'tests', 'test_health.py'], renderBackendHealthTest());
298
- add('backend-test-messaging', 'backend-test', ['backend', 'tests', 'test_messaging.py'], renderMessagingTest());
299
- add('backend-test-tracing', 'backend-test', ['backend', 'tests', 'test_tracing.py'], renderTracingTest());
300
- }
301
- function addDatabaseArtifacts(add, plan) {
302
- add('database-alembic-ini', 'database', ['database', 'alembic.ini'], renderAlembicIni());
303
- add('database-alembic-env', 'database', ['database', 'migrations', 'env.py'], renderAlembicEnv());
304
- add('database-initial-migration', 'database', ['database', 'migrations', 'versions', '0001_initial.py'], renderInitialMigration(plan));
305
- add('database-schema', 'database', ['database', 'models', 'schema.sql'], renderDatabaseSchema(plan));
306
- }
307
- function addPatternArtifacts(add, plan) {
308
- const pattern = genAiPattern(plan);
309
- const routeModule = pyModule(pattern.id);
310
- add('pattern-agent', 'pattern', ['backend', 'orchestration', 'agents', `${routeModule}_agent.py`], renderPatternAgent(plan));
311
- add('pattern-agent-test', 'backend-test', ['backend', 'tests', `test_${routeModule}_orchestration.py`], renderPatternAgentTest(plan));
312
- add('pattern-prompt', 'pattern', ['backend', 'orchestration', 'prompts', `${pattern.id}.md`], renderPromptTemplate(plan));
313
- add('pattern-agent-package', 'pattern', ['backend', 'orchestration', 'agents', '__init__.py'], '');
314
- add('pattern-prompt-readme', 'pattern', ['backend', 'orchestration', 'prompts', 'README.md'], renderPromptReadme());
315
- if (pattern.id === 'rag') {
316
- add('rag-vector-store', 'pattern', ['backend', 'orchestration', 'retrieval', 'vector_store.py'], renderVectorStore());
317
- add('rag-retrieval-package', 'pattern', ['backend', 'orchestration', 'retrieval', '__init__.py'], '');
318
- }
319
- if (pattern.worker) {
320
- add('pattern-worker', 'pattern', ['backend', 'workers', `${routeModule}_worker.py`], renderPatternWorker(plan));
321
- add('backend-workers-package', 'pattern', ['backend', 'workers', '__init__.py'], '');
322
- }
323
- if (pattern.id === 'fine-tuned') {
324
- add('fine-tuned-eval-dataset', 'pattern', ['backend', 'evaluation', 'datasets', 'sample.jsonl'], '{"input":"Example request","expected":"Expected response placeholder"}');
325
- }
326
- }
327
- function addFunctionArtifacts(add, plan) {
328
- if (!hasFunctionWorker(plan)) {
329
- return;
330
- }
331
- const workerName = functionWorkerName(plan);
332
- const workerBase = ['functions', workerName];
333
- add('functions-readme', 'functions', ['functions', 'README.md'], renderFunctionsReadme());
334
- add('function-worker-readme', 'functions', [...workerBase, 'README.md'], renderFunctionWorkerReadme(plan));
335
- add('function-worker-host', 'functions', [...workerBase, 'host.json'], renderFunctionHostJson());
336
- add('function-worker-local-settings', 'functions', [...workerBase, 'local.settings.example.json'], renderFunctionLocalSettings(plan));
337
- add('function-worker-requirements', 'functions', [...workerBase, 'requirements.txt'], renderFunctionRequirements());
338
- add('function-worker-app', 'functions', [...workerBase, 'function_app.py'], renderFunctionApp(plan));
339
- add('function-worker-test', 'functions-test', [...workerBase, 'tests', 'test_function_app.py'], renderFunctionTest());
340
- add('function-worker-funcignore', 'functions', [...workerBase, '.funcignore'], renderFunctionFuncIgnore());
341
- add('function-worker-gitignore', 'functions', [...workerBase, '.gitignore'], renderFunctionGitIgnore());
342
- }
343
- function addEnvironmentArtifacts(artifacts, plan) {
344
- for (const environment of plan.environments) {
345
- const add = createArtifactAdder(artifacts, 'project', `environment:${environment.id}`);
346
- add(`environment-${environment.id}-backend`, 'environment', ['environments', environment.id, 'backend.env'], plan.workload === 'genai' ? renderBackendEnv(plan, environment.id) : renderStandardEnv(plan, environment.id));
347
- if (plan.workload === 'genai' && hasFunctionWorker(plan)) {
348
- add(`environment-${environment.id}-functions`, 'environment', ['environments', environment.id, 'functions.env'], renderFunctionsEnv(plan, environment.id));
349
- }
350
- }
351
- }
352
- function addDockerArtifacts(add, plan) {
353
- add('docker-compose', 'local-development', ['docker-compose.yml'], renderDockerCompose(plan));
354
- }
355
- function addInfrastructureArtifacts(add, artifacts, plan) {
356
- const base = ['infrastructure', 'opentofu', 'azure'];
357
- add('opentofu-versions', 'infrastructure', [...base, 'versions.tf'], renderOpenTofuVersions());
358
- add('opentofu-provider-lock', 'infrastructure', [...base, '.terraform.lock.hcl'], renderOpenTofuProviderLock());
359
- add('opentofu-providers', 'infrastructure', [...base, 'providers.tf'], renderTofuProviders());
360
- add('opentofu-variables', 'infrastructure', [...base, 'variables.tf'], renderTofuVariables(plan));
361
- add('opentofu-main', 'infrastructure', [...base, 'main.tf'], renderTofuMain(plan));
362
- add('opentofu-outputs', 'infrastructure', [...base, 'outputs.tf'], renderTofuOutputs(plan));
363
- add('opentofu-local-state', 'infrastructure', [...base, 'backend.local.tf'], renderTofuLocalState());
364
- add('opentofu-remote-state-example', 'infrastructure', [...base, 'backend.remote.example.tf'], renderTofuRemoteStateExample());
365
- add('opentofu-readme', 'infrastructure', [...base, 'README.md'], renderTofuReadme(plan));
366
- for (const environment of plan.environments) {
367
- createArtifactAdder(artifacts, 'project', `environment:${environment.id}`)(`opentofu-${environment.id}-tfvars`, 'infrastructure', [...base, 'environments', `${environment.id}.tfvars`], renderTofuTfvars(plan, environment.id));
368
- }
369
- }
370
- function addSpecWorkflowArtifacts(addSeed, addFramework, plan) {
371
- if (plan.workload === 'power-apps-code-app') {
372
- if (plan.specWorkflow.id === 'openspec') {
373
- const changeName = `bootstrap-${plan.safeProjectName}`;
374
- addSeed('openspec-config', 'seed', ['openspec', 'config.yaml'], renderPowerAppsOpenSpecConfig(plan));
375
- addSeed('openspec-seed-change-metadata', 'seed', ['openspec', 'changes', changeName, '.openspec.yaml'], 'schema: spec-driven');
376
- addSeed('openspec-seed-proposal', 'seed', ['openspec', 'changes', changeName, 'proposal.md'], renderPowerAppsSeedProposal(plan));
377
- addSeed('openspec-seed-design', 'seed', ['openspec', 'changes', changeName, 'design.md'], renderPowerAppsSeedDesign(plan));
378
- addSeed('openspec-seed-tasks', 'seed', ['openspec', 'changes', changeName, 'tasks.md'], renderSeedTasks(plan));
379
- addSeed('openspec-seed-spec', 'seed', ['openspec', 'changes', changeName, 'specs', seedCapabilityId(plan), 'spec.md'], renderSeedSpec(plan));
380
- addSeed('openspec-spec-placeholder', 'seed', ['openspec', 'specs', '.gitkeep'], '');
381
- }
382
- else {
383
- addSeed('spec-kit-constitution', 'seed', ['.specify', 'memory', 'constitution.md'], renderPowerAppsSpecKitConstitution(plan));
384
- addFramework('spec-kit-spec-template', 'framework', ['.specify', 'templates', 'spec-template.md'], renderSpecKitSpecTemplate());
385
- addFramework('spec-kit-plan-template', 'framework', ['.specify', 'templates', 'plan-template.md'], renderSpecKitPlanTemplate());
386
- addSeed('specs-placeholder', 'seed', ['specs', '.gitkeep'], '');
387
- }
388
- return;
389
- }
390
- if (plan.specWorkflow.id === 'openspec') {
391
- const changeName = `bootstrap-${plan.safeProjectName}`;
392
- addSeed('openspec-config', 'seed', ['openspec', 'config.yaml'], renderOpenSpecConfig(plan));
393
- addSeed('openspec-seed-change-metadata', 'seed', ['openspec', 'changes', changeName, '.openspec.yaml'], 'schema: spec-driven');
394
- addSeed('openspec-seed-proposal', 'seed', ['openspec', 'changes', changeName, 'proposal.md'], renderSeedProposal(plan));
395
- addSeed('openspec-seed-design', 'seed', ['openspec', 'changes', changeName, 'design.md'], renderSeedDesign(plan));
396
- addSeed('openspec-seed-tasks', 'seed', ['openspec', 'changes', changeName, 'tasks.md'], renderSeedTasks(plan));
397
- addSeed('openspec-seed-spec', 'seed', ['openspec', 'changes', changeName, 'specs', seedCapabilityId(plan), 'spec.md'], renderSeedSpec(plan));
398
- addSeed('openspec-spec-placeholder', 'seed', ['openspec', 'specs', '.gitkeep'], '');
399
- }
400
- else {
401
- addSeed('spec-kit-constitution', 'seed', ['.specify', 'memory', 'constitution.md'], renderSpecKitConstitution(plan));
402
- addFramework('spec-kit-spec-template', 'framework', ['.specify', 'templates', 'spec-template.md'], renderSpecKitSpecTemplate());
403
- addFramework('spec-kit-plan-template', 'framework', ['.specify', 'templates', 'plan-template.md'], renderSpecKitPlanTemplate());
404
- addSeed('specs-placeholder', 'seed', ['specs', '.gitkeep'], '');
405
- }
406
- }
407
- function renderPowerAppsOpenSpecConfig(plan) {
408
- return `schema: spec-driven${renderOpenSpecCopilotConfig(plan)}
409
- context: |
410
- Project: ${plan.projectName}
411
- Workload: Microsoft Power Apps code app
412
- Stack: React, Vite, TypeScript, Tailwind CSS, Power Apps SDK
413
- Starter: ${plan.starter.repository}/${plan.starter.path}@${plan.starter.commit}
414
- Runtime boundary: browser-hosted code app with connector-first data access
415
- Environment binding: explicit post-scaffold Power Apps CLI action
416
- rules:
417
- proposal:
418
- - Keep tenant, environment, connection, solution, and deployment choices explicit.
419
- design:
420
- - Prefer Power Platform connectors and generated service modules over custom APIs.
421
- - Do not add Liftoff-owned Azure or API infrastructure unless a later change explicitly introduces it.
422
- tasks:
423
- - Include npm lint and build verification.
424
- `;
425
- }
426
- function renderOpenSpecCopilotConfig(plan) {
427
- return plan.agents.some((agent) => agent.id === 'github-copilot')
428
- ? `
429
- githubCopilot:
430
- cloudAgent: ${plan.copilotCloud}
431
- `
432
- : '\n';
433
- }
434
- function renderPowerAppsSeedProposal(plan) {
435
- const capability = seedCapabilityId(plan);
436
- return `# Proposal: bootstrap-${plan.safeProjectName}
437
-
438
- ## Why
439
-
440
- Bootstrap the generated ${plan.projectName} Power Apps code app baseline created by Mission Control Liftoff.
441
-
442
- ## What Changes
443
-
444
- - Establish the generated React, Vite, TypeScript, Tailwind CSS, Power Apps SDK, workflow, and governance baseline.
445
- - Confirm that tenant binding, authentication, connector choices, solution packaging, deployment, and domain-specific product behavior are deferred to follow-up changes.
446
-
447
- ## Capabilities
448
-
449
- ### New Capabilities
450
-
451
- - \`${capability}\`: Generated Power Apps code app baseline for this Liftoff project.
452
-
453
- ### Modified Capabilities
454
-
455
- - None.
456
-
457
- ## Impact
458
-
459
- - Generated Power Apps starter files, package metadata, workflow files, and governance files.
460
- - No Liftoff-owned backend API, Docker stack, Azure resources, \`power.config.json\`, tenant binding, credentials, or deployment.
461
- `;
462
- }
463
- function renderPowerAppsSeedDesign(plan) {
464
- return `# Design: bootstrap-${plan.safeProjectName}
465
-
466
- ## Context
467
-
468
- The project uses React, Vite, TypeScript, Tailwind CSS, and Microsoft's Power Apps SDK.
469
- It was initialized from \`${plan.starter.path}\` at \`${plan.starter.commit}\`.
470
-
471
- ## Goals / Non-Goals
472
-
473
- **Goals:**
474
-
475
- - Keep the generated baseline aligned to the approved Mission Control Power Apps code app stack.
476
- - Verify only deterministic local scaffold checks before archiving this bootstrap change.
477
-
478
- **Non-Goals:**
479
-
480
- - Define domain-specific product behavior, including screens, connector behavior, tenant binding, solution packaging, deployment, or credentials, in the bootstrap change.
481
-
482
- ## Decisions
483
-
484
- - Keep pages under \`src/pages\` and shared UI under \`src/components\`.
485
- - Keep connector and generated service boundaries explicit under \`src\`.
486
- - Use TanStack Query for server state and Zustand only for local client state.
487
- - Bind to a tenant and environment only through Microsoft's Power Apps CLI.
488
-
489
- ## Risks / Trade-offs
490
-
491
- - Follow-up product changes own real business behavior; this seed only proves the generated local baseline is coherent.
492
- `;
493
- }
494
- function renderPowerAppsSpecKitConstitution(plan) {
495
- return `# ${plan.projectName} Constitution
496
-
497
- ## I. Power Apps workload boundary
498
-
499
- The application MUST remain a Power Apps code app using React, Vite, TypeScript, Tailwind CSS,
500
- the Power Apps SDK, and the Power Apps Vite plugin. Tenant and environment binding MUST remain
501
- an explicit developer action.
502
-
503
- ## II. Connector-first integration
504
-
505
- Features MUST prefer Power Platform connectors and generated service modules. A custom API or
506
- Azure resource requires an explicit specification and MUST NOT be inferred from this scaffold.
507
-
508
- ## III. Project structure
509
-
510
- Pages belong under \`src/pages\`, shared UI under \`src/components\`, and reusable providers or
511
- connector boundaries under named \`src\` modules. \`power.config.json\` is never fabricated.
512
-
513
- ## IV. Quality gates
514
-
515
- Every change MUST pass the applicable npm lint and build checks plus \`liftoff validate\`.
516
- `;
517
- }
518
- function addFrontendArtifacts(add, plan) {
519
- add('frontend-package', 'frontend', ['frontend', 'package.json'], renderFrontendPackage(plan));
520
- add('frontend-lock', 'frontend', ['frontend', 'package-lock.json'], renderNpmLock('frontend', `${plan.safeProjectName}-frontend`));
521
- add('frontend-index', 'frontend', ['frontend', 'index.html'], renderFrontendIndex(plan));
522
- add('frontend-main', 'frontend', ['frontend', 'src', 'main.ts'], renderFrontendMain());
523
- add('frontend-app', 'frontend', ['frontend', 'src', 'App.vue'], renderFrontendApp(plan));
524
- add('frontend-env-example', 'frontend', ['frontend', '.env.example'], 'VITE_API_BASE_URL=http://localhost:8000');
525
- add('frontend-styles', 'frontend', ['frontend', 'src', 'styles.css'], renderFrontendStyles());
526
- add('frontend-vite-config', 'frontend', ['frontend', 'vite.config.ts'], renderFrontendViteConfig());
527
- add('frontend-tailwind-config', 'frontend', ['frontend', 'tailwind.config.ts'], renderFrontendTailwindConfig());
528
- add('frontend-dockerfile', 'frontend', ['frontend', 'Dockerfile'], renderFrontendDockerfile());
529
- }
530
- function renderDirectBuildAndTestGuide(plan) {
531
- let backendCommands;
532
- if (plan.workload === 'genai' || plan.apiStack.id === 'python-fastapi') {
533
- const extras = plan.workload === 'genai' && hasFunctionWorker(plan)
534
- ? ' --extra functions'
535
- : '';
536
- backendCommands = `uv sync --frozen --project backend --extra test${extras}
537
- uv run --project backend python -m pytest -q backend/tests`;
538
- }
539
- else if (plan.apiStack.id === 'node-fastify') {
540
- backendCommands = `cd backend
541
- npm ci
542
- npm run build
543
- npm test`;
544
- }
545
- else {
546
- backendCommands = `cd backend
547
- go mod download
548
- go test ./...`;
549
- }
550
- const frontendCommands = plan.includeFrontend ? `
551
-
552
- Build the frontend without a running backend:
553
-
554
- \`\`\`bash
555
- cp frontend/.env.example frontend/.env
556
- cd frontend
557
- npm ci
558
- npm run build
559
- \`\`\`
560
- ` : '';
561
- const functionCommands = plan.workload === 'genai' && hasFunctionWorker(plan) ? `
562
-
563
- Run the Function worker unit tests from the same locked Python environment:
564
-
565
- \`\`\`bash
566
- (cd functions/${functionWorkerName(plan)} && uv run --project ../../backend --directory . python -m pytest -q)
567
- \`\`\`
568
- ` : '';
569
- return `## Project Dependencies, Build, And Test
570
-
571
- \`liftoff init --install-dependencies\` runs the selected stack's locked project-local dependency commands. To run or resume them manually:
572
-
573
- \`\`\`bash
574
- ${backendCommands}
575
- \`\`\`
576
-
577
- ${frontendCommands}${functionCommands}`;
578
- }
579
- function renderDeterministicSetupGuide(plan) {
580
- const backend = plan.workload === 'genai' || plan.apiStack.id === 'python-fastapi'
581
- ? '`uv run --project backend python -m pytest -q backend/tests`'
582
- : plan.apiStack.id === 'node-fastify'
583
- ? '`npm test` from `backend/` after `npm ci` and `npm run build`'
584
- : '`go test ./...` from `backend/`';
585
- const frontend = plan.includeFrontend
586
- ? '- Frontend build: `npm run build` from `frontend/` after `npm ci`.'
587
- : '- Frontend build: inapplicable because no frontend was generated.';
588
- const governance = plan.governanceProfile.id === 'none'
589
- ? 'Repository governance is disabled, so there is no `/liftoff-setup` integration, managed phase graph, or post-init governance activation path.'
590
- : 'Run `/liftoff-setup` from a selected agent. It completes, syncs, and archives the generated bootstrap seed, then stops at explicit authority gates. Commit and push are separate approvals, and rerunning `/liftoff-setup` resumes idempotently without repeating verified phases.';
591
- return `## Deterministic Setup
592
-
593
- ${governance}
594
-
595
- The local baseline contains only applicable checks:
596
-
597
- - \`liftoff validate\`
598
- - Backend tests: ${backend}
599
- ${frontend}
600
- - \`docker compose config -q\`
601
- - \`tofu fmt -check -recursive\`
602
- - \`tofu init -backend=false\`
603
- - \`tofu validate\`
604
- - strict ${plan.specWorkflow.label} validation
605
-
606
- No baseline step runs a live OpenTofu plan or apply, starts containers, deploys,
607
- mutates GitHub, or asks for cloud credentials. Absent components are recorded as
608
- inapplicable rather than simulated. After setup archives the seed, use normal
609
- OpenSpec or Spec Kit changes for features and the governed GitFlow release path
610
- after activation evidence is green.
611
- `;
612
- }
613
- function renderAdvisoryReadinessGuide(plan) {
614
- const selected = [
615
- 'docker',
616
- 'opentofu',
617
- ...(plan.provider.id === 'azure' ? ['azure-cli'] : [])
618
- ];
619
- return selected.map((id) => {
620
- const requirement = workstationRequirementCatalog[id];
621
- const mac = requirement.install.darwin;
622
- const windows = requirement.install.win32;
623
- const commands = [
624
- ...(mac ? [`macOS: \`${formatCommand(mac.command)}\``] : []),
625
- ...(windows ? [`Windows: \`${formatCommand(windows.command)}\``] : []),
626
- `Linux: ${requirement.linuxRemedies.unknown}`
627
- ].join('; ');
628
- const health = id === 'docker'
629
- ? ' After installation, start Docker Desktop or the Docker daemon.'
630
- : id === 'azure-cli'
631
- ? ' After installation, run `az login` if authentication is not ready.'
632
- : '';
633
- return `- ${requirement.label}: ${commands}.${health}`;
634
- }).join('\n');
635
- }
636
- function renderSpecWorkflowGuide(plan) {
637
- const agents = plan.agents.map((agent) => `${agent.label}${plan.defaultAgent?.id === agent.id ? ' (default integration)' : ''}`).join(', ');
638
- const ownership = plan.specWorkflow.id === 'openspec'
639
- ? 'OpenSpec workflow skills, commands, configuration, and optional cloud-agent files'
640
- : 'Spec Kit core files, integration state, and the selected Copilot or Claude skill integrations';
641
- const openSpecDetails = plan.specWorkflow.id === 'openspec'
642
- ? [
643
- `- OpenSpec profile: ${OPEN_SPEC_PROFILE}; delivery: ${OPEN_SPEC_DELIVERY}; workflows: ${OPEN_SPEC_WORKFLOW_IDS.join(', ')}`,
644
- ...(plan.agents.some((agent) => agent.id === 'github-copilot')
645
- ? ['- GitHub Copilot cloud agent: controlled by `githubCopilot.cloudAgent` in `openspec/config.yaml`']
646
- : [])
647
- ].join('\n')
648
- : '';
649
- return `## Spec-Driven Workflow And Validation
650
-
651
- - Workflow: ${plan.specWorkflow.label} ${plan.framework.version}
652
- - AI coding agents: ${agents}
653
- ${openSpecDetails ? `${openSpecDetails}\n` : ''}
654
- - Framework ownership: the official initializer owns ${ownership}. Liftoff validates these files but excludes framework-owned output and one-time seed content from managed-core hashes.
655
- - Deferred tools: advisory workstation checks may be deferred. Liftoff never claims they are installed and never installs them without \`--install-tools\`.
656
-
657
- If \`liftoff doctor\` reports a selected advisory tool as missing, use its registered readiness remedy:
658
-
659
- ${renderAdvisoryReadinessGuide(plan)}
660
-
661
- Validate the scaffold and workstation after setup:
662
-
663
- \`\`\`bash
664
- liftoff validate
665
- liftoff doctor
666
- \`\`\`
667
-
668
- For an existing OpenSpec project, change workflow delivery with \`openspec config profile\` and refresh framework-owned files with \`openspec update\`. Plain \`liftoff update\` does not regenerate OpenSpec integrations.
669
- `;
670
- }
671
- function renderGeneratedConfigurationGuide(plan) {
672
- const frontendConfiguration = plan.includeFrontend
673
- ? '\n- `frontend/.env` configures `VITE_API_BASE_URL`; the production build does not contact the backend.'
674
- : '';
675
- if (plan.workload === 'standard') {
676
- return `## Runtime Configuration
677
-
678
- Copy \`.env.example\` to \`.env\` before running outside Docker Compose. The backend requires \`DATABASE_URL\` and \`REDIS_URL\`. \`CORS_ALLOWED_ORIGINS\` is a comma-separated allowlist and defaults to the local frontend at \`http://localhost:5173\`.${frontendConfiguration}
679
- `;
680
- }
681
- return `## Starter Integration Configuration
682
-
683
- Copy \`.env.example\` to \`.env\`, then configure only the integrations you use:
684
-
685
- - \`PYDANTIC_AI_MODEL\` is required when production orchestration is invoked. If it is absent, the agent raises an explicit configuration error rather than returning a placeholder answer.
686
- - Redis Streams uses \`REDIS_URL\` and \`REDIS_STREAM_NAME\`.
687
- - Azure Service Bus uses \`SERVICE_BUS_QUEUE_NAME\` plus either \`SERVICE_BUS_CONNECTION_STRING\` or \`SERVICE_BUS_FULLY_QUALIFIED_NAMESPACE\`; set \`AZURE_CLIENT_ID\` when selecting a user-assigned managed identity.
688
- - Langfuse requires both \`LANGFUSE_PUBLIC_KEY\` and \`LANGFUSE_SECRET_KEY\`, with optional \`LANGFUSE_HOST\`. Without both keys, tracing is explicitly disabled and no remote trace ID is reported.${frontendConfiguration}
689
- - \`CORS_ALLOWED_ORIGINS\` is a comma-separated frontend-origin allowlist and defaults to \`http://localhost:5173\`.
690
- `;
691
- }
692
- function renderGeneratedUpdateGuide(plan) {
693
- const governance = plan.governanceProfile.id === 'none'
694
- ? 'Repository governance is disabled for this project, so Liftoff does not generate setup integrations, a managed phase graph, credential-policy schema, or post-init setup command.'
695
- : '`single-maintainer-gitflow` repository governance generates deterministic setup artifacts only. Review `.liftoff/governance/README.md`, then run `/liftoff-setup` from a selected agent. Live enforcement requires evidence and explicit approval; it is never inferred from generated files.';
696
- return `## Safe Liftoff Updates
697
-
698
- \`liftoff upgrade\` replaces a supported global Liftoff CLI installation; it does not inspect or modify this project. Check and apply CLI replacement separately with \`liftoff upgrade --check\` and \`liftoff upgrade\`.
699
-
700
- ${governance}
701
-
702
- \`liftoff update\` maintains only explicit Liftoff core files, currently the repository-governance policy, context, guide, phase graph, compatibility metadata, credential-policy schema, and selected-agent \`/liftoff-setup\` and \`/liftoff-governance-assess\` integrations. Use \`liftoff update --check\` for a read-only core report, or \`liftoff update --check --json\` as an automation gate that exits 0 when core state is clean and 2 when maintenance is available.
703
-
704
- Application source, tests, dependencies and locks, schemas, containers, environment files, documentation, and infrastructure become project-owned after generation. No update mode, including \`--force\`, can restore or replace them. Enabling a previously absent frontend or environment in \`liftoff.config.json\` may provision that component once at absent destinations; a collision blocks the whole component and cannot be forced.
705
-
706
- Project template modernization is a separately reviewed production change and is not performed by ordinary update or by the existing non-Liftoff \`migrate\` command. Managed-core conflicts are skipped by default; after reviewing every listed core path, \`liftoff update --force\` may replace only those core conflicts. Managed-core orphans remain on disk, and update never installs dependencies. A failed transaction is rolled back, but Liftoff retains no backup after a successful core overwrite.
707
-
708
- Liftoff rejects malformed, traversal, absolute, drive-qualified, UNC, separator-containing, or symlink-escaping manifest paths before artifact access. If the manifest is unsafe or malformed, restore \`liftoff.manifest.json\` from version control or regenerate the project with a matching Liftoff version; do not hand-edit unsafe paths. Run \`liftoff <command> --help\` for command-specific syntax because unknown flags, subcommands, values, and extra arguments fail before any write.
709
-
710
- ${plan.governanceProfile.id === 'none'
711
- ? 'No `/liftoff-governance-assess` integration is generated while governance is disabled.'
712
- : renderGovernanceAssessmentGuide()}
713
- `;
714
- }
715
- function selectedEnvironmentId(plan) {
716
- return plan.environments[0]?.id ?? 'dev';
717
- }
718
- function renderRootInfrastructureGuide(plan) {
719
- const environment = selectedEnvironmentId(plan);
720
- const governanceGate = plan.governanceProfile.id === 'none'
721
- ? ''
722
- : `These commands are reference material, not the next setup action. Do not run this
723
- sequence until the separately approved \`application-foundation\` governance phase
724
- authorizes the exact infrastructure mutation. \`/liftoff-setup\` can evaluate and
725
- resume managed phases, but it does not imply that every managed phase has an
726
- executable production adapter. An unavailable production adapter remains a
727
- blocker; do not bypass it by running the reference commands directly.
728
-
729
- `;
730
- return `## Infrastructure
731
-
732
- ${governanceGate}\`\`\`bash
733
- cd infrastructure/opentofu/azure
734
- tofu init
735
- tofu plan -var-file=environments/${environment}.tfvars
736
- tofu apply -var-file=environments/${environment}.tfvars
737
- \`\`\`
738
-
739
- The first apply uses a public bootstrap image. Follow \`infrastructure/opentofu/azure/README.md\` to build the generated backend in ACR and apply its image.
740
- `;
741
- }
742
- function renderRootReadme(plan) {
743
- if (plan.workload === 'standard') {
744
- return `# ${plan.projectName}
745
-
746
- Generated by Mission Control Liftoff.
747
-
748
- ## Stack
749
-
750
- - Project type: Standard application
751
- - API: ${plan.apiStack.label}
752
- - Database tooling: ${plan.apiStack.databaseTooling}
753
- - API reference: Scalar with OpenAPI
754
- - Cloud: ${plan.provider.label} (${plan.region.slug})
755
- - Infrastructure: OpenTofu
756
- - Database: PostgreSQL
757
- - Cache and local messaging: Redis
758
- - Local development: Docker Compose
759
- ${plan.includeFrontend ? '- Frontend: Vue 3 with Tailwind\n' : ''}
760
- ${renderDeterministicSetupGuide(plan)}
761
- ## Local Development
762
-
763
- \`\`\`bash
764
- docker compose up --build
765
- \`\`\`
766
-
767
- The backend API is available on port 8000. Health and readiness endpoints are available at \`/health\` and \`/ready\`; Scalar is exposed at \`/scalar\`.
768
-
769
- ${renderGeneratedConfigurationGuide(plan)}
770
- ${renderDirectBuildAndTestGuide(plan)}
771
- ${renderGeneratedUpdateGuide(plan)}
772
- ${renderRootInfrastructureGuide(plan)}
773
- ${renderSpecWorkflowGuide(plan)}
774
- `;
775
- }
776
- const pattern = genAiPattern(plan);
777
- const functionsStackLine = hasFunctionWorker(plan) ? `- Azure Functions worker: Python v2 Service Bus trigger under \`functions/${functionWorkerName(plan)}\`
778
- ` : '';
779
- const functionsSection = hasFunctionWorker(plan) ? `
780
- ## Azure Functions Worker
781
-
782
- Azure Functions trigger adapters live under \`functions/${functionWorkerName(plan)}\`. Keep reusable GenAI orchestration under \`backend/orchestration\`; \`backend/workers\` remains the place for backend-adjacent or containerized worker code.
783
- ` : '';
784
- const genericSection = pattern.id === 'generic' ? `
785
- ## Choosing A Specialization Later
786
-
787
- This project intentionally starts with a neutral text-in/result-out GenAI boundary. It does not assume RAG, conversation history, agent tools, streaming, fine-tuning, multi-agent coordination, or workflow orchestration.
788
-
789
- Generated application files are project-owned. When requirements become clear, specialize through a reviewed project change; \`liftoff update\` and \`--force\` do not convert this project to another GenAI pattern.
790
- ` : '';
791
- return `# ${plan.projectName}
792
-
793
- Generated by Mission Control Liftoff.
794
-
795
- ## Stack
796
-
797
- - Backend: FastAPI, PydanticAI, Pydantic settings, Scalar
798
- - Pattern: ${pattern.label}
799
- - Cloud: ${plan.provider.label} (${plan.region.slug})
800
- - Infrastructure: OpenTofu
801
- - Database: PostgreSQL with Alembic migrations${pattern.id === 'rag' ? ' and pgvector retrieval' : ''}
802
- - Cache and local messaging: Redis
803
- - Observability: Langfuse
804
- - Local development: Docker Compose
805
- ${functionsStackLine}
806
- ${plan.includeFrontend ? '- Frontend: Vue 3 with Tailwind\n' : ''}
807
- ${renderDeterministicSetupGuide(plan)}
808
- ## Local Development
809
-
810
- \`\`\`bash
811
- docker compose up --build
812
- docker compose --profile observability up --build
813
- \`\`\`
814
-
815
- The backend API is available on port 8000. Scalar is exposed at \`/scalar\`.
816
-
817
- ${renderGeneratedConfigurationGuide(plan)}
818
- ${renderDirectBuildAndTestGuide(plan)}
819
- ${renderGeneratedUpdateGuide(plan)}
820
- ${renderRootInfrastructureGuide(plan)}
821
- ${renderSpecWorkflowGuide(plan)}
822
- ${functionsSection}
823
- ${genericSection}
824
- `;
825
- }
826
- function renderGeneratedGitignore() {
827
- return `.venv/
828
- __pycache__/
829
- .pytest_cache/
830
- node_modules/
831
- dist/
832
- .env
833
- migration/legacy/
834
- *.tfstate
835
- *.tfstate.*
836
- .terraform/
837
- `;
838
- }
839
- function renderEnvExample(plan) {
840
- if (plan.workload === 'standard') {
841
- return renderStandardEnv(plan);
842
- }
843
- const pattern = genAiPattern(plan);
844
- return `APP_ENV=dev
845
- APP_NAME=${plan.safeProjectName}
846
- GENAI_PATTERN=${pattern.id}
847
- CLOUD_PROVIDER=${plan.provider.id}
848
- AZURE_REGION=${plan.region.slug}
849
- DATABASE_URL=postgresql+asyncpg://postgres:postgres@postgres:5432/${plan.safeProjectName.replace(/-/g, '_')}
850
- REDIS_URL=redis://redis:6379/0
851
- REDIS_STREAM_NAME=liftoff-events
852
- MESSAGING_TRANSPORT=redis-streams
853
- SERVICE_BUS_QUEUE_NAME=${DEFAULT_FUNCTION_WORKER_QUEUE_NAME}
854
- SERVICE_BUS_CONNECTION_STRING=
855
- SERVICE_BUS_FULLY_QUALIFIED_NAMESPACE=
856
- AZURE_CLIENT_ID=
857
- BLOB_ENDPOINT=http://azurite:10000/devstoreaccount1
858
- CORS_ALLOWED_ORIGINS=http://localhost:5173
859
- PYDANTIC_AI_MODEL=
860
- LANGFUSE_HOST=http://langfuse:3000
861
- LANGFUSE_PUBLIC_KEY=
862
- LANGFUSE_SECRET_KEY=
863
- `;
864
- }
865
- function renderBackendDockerfile() {
866
- return `FROM ${formatContainerImage(supportedStack.containers['uv-tool'])} AS uv
867
- FROM ${formatContainerImage(supportedStack.containers['python-runtime'])}
868
-
869
- WORKDIR /app
870
-
871
- ENV PYTHONDONTWRITEBYTECODE=1
872
- ENV PYTHONUNBUFFERED=1
873
- ENV PYTHONPATH=/app
874
- ENV PATH=/app/backend/.venv/bin:$PATH
875
- COPY --from=uv /uv /uvx /bin/
876
-
877
- ARG UV_DEFAULT_INDEX=https://pypi.org/simple
878
- COPY backend/pyproject.toml /app/backend/pyproject.toml
879
- COPY backend/uv.lock /app/backend/uv.lock
880
- RUN --mount=type=cache,target=/root/.cache/uv \\
881
- uv export --frozen --no-dev --no-emit-project --project /app/backend --output-file /tmp/requirements.txt \\
882
- && uv venv /app/backend/.venv \\
883
- && uv pip install --python /app/backend/.venv/bin/python --require-hashes \\
884
- --default-index "$UV_DEFAULT_INDEX" --requirements /tmp/requirements.txt \\
885
- && rm /tmp/requirements.txt
886
-
887
- COPY backend /app/backend
888
- COPY database /app/database
889
-
890
- EXPOSE 8000
891
- CMD ["uvicorn", "backend.apis.main:app", "--host", "0.0.0.0", "--port", "8000"]
892
- `;
893
- }
894
- function renderBackendPyproject(plan) {
895
- return renderPythonPyprojectAsset('genai', `${plan.safeProjectName}-backend`);
896
- }
897
- function renderFastApiMain(plan, routeModule) {
898
- return `from fastapi import FastAPI
899
- from fastapi.middleware.cors import CORSMiddleware
900
-
901
- try:
902
- from scalar_fastapi import get_scalar_api_reference
903
- except ImportError: # pragma: no cover - dependency is present in generated runtime
904
- get_scalar_api_reference = None
905
-
906
- from backend.apis.routes import health, ${routeModule}
907
- from backend.config.settings import get_settings
908
-
909
-
910
- settings = get_settings()
911
- app = FastAPI(title=settings.app_name, version="0.1.0")
912
- app.add_middleware(
913
- CORSMiddleware,
914
- allow_origins=[
915
- origin.strip()
916
- for origin in settings.cors_allowed_origins.split(",")
917
- if origin.strip()
918
- ],
919
- allow_methods=["*"],
920
- allow_headers=["*"],
921
- )
922
-
923
- app.include_router(health.router)
924
- app.include_router(${routeModule}.router)
925
-
926
-
927
- @app.get("/scalar", include_in_schema=False)
928
- def scalar_reference():
929
- if get_scalar_api_reference is None:
930
- return {"message": "Install scalar-fastapi to enable the Scalar developer portal."}
931
- return get_scalar_api_reference(openapi_url=app.openapi_url, title=f"{app.title} API")
932
- `;
933
- }
934
- function renderHealthRoutes() {
935
- return `from fastapi import APIRouter
936
-
937
- router = APIRouter(tags=["operations"])
938
-
939
-
940
- @router.get("/health")
941
- def health():
942
- return {"status": "ok"}
943
-
944
-
945
- @router.get("/ready")
946
- def ready():
947
- return {"status": "ready"}
948
- `;
949
- }
950
- function renderPatternRoutes(plan) {
951
- const pattern = genAiPattern(plan);
952
- const moduleName = pyModule(pattern.id);
953
- const agentName = `${moduleName}_agent`;
954
- const prefix = pattern.routePrefix;
955
- if (pattern.id === 'streaming') {
956
- return `from fastapi import APIRouter
957
- from fastapi.responses import StreamingResponse
958
-
959
- from backend.orchestration.agents.${agentName} import stream_response
960
-
961
- router = APIRouter(prefix="${prefix}", tags=["${pattern.id}"])
962
-
963
-
964
- @router.get("")
965
- def stream(prompt: str):
966
- return StreamingResponse(stream_response(prompt), media_type="text/event-stream")
967
- `;
968
- }
969
- if (pattern.id === 'rag') {
970
- return `from fastapi import APIRouter
971
- from pydantic import BaseModel
972
-
973
- from backend.orchestration.agents.${agentName} import answer_question, enqueue_ingestion
974
-
975
- router = APIRouter(prefix="${prefix}", tags=["rag"])
976
-
977
-
978
- class QueryRequest(BaseModel):
979
- question: str
980
-
981
-
982
- class IngestionRequest(BaseModel):
983
- source_uri: str
984
-
985
-
986
- @router.post("/query")
987
- async def query(request: QueryRequest):
988
- return await answer_question(request.question)
989
-
990
-
991
- @router.post("/ingest")
992
- async def ingest(request: IngestionRequest):
993
- return await enqueue_ingestion(request.source_uri)
994
- `;
995
- }
996
- const bodyClass = `${titleCase(pattern.id).replace(/\s/g, '')}Request`;
997
- return `from fastapi import APIRouter
998
- from pydantic import BaseModel
999
-
1000
- from backend.orchestration.agents.${agentName} import run_${moduleName}
1001
-
1002
- router = APIRouter(prefix="${prefix}", tags=["${pattern.id}"])
1003
-
1004
-
1005
- class ${bodyClass}(BaseModel):
1006
- input: str
1007
-
1008
-
1009
- @router.post("/run")
1010
- async def run(request: ${bodyClass}):
1011
- return await run_${moduleName}(request.input)
1012
- `;
1013
- }
1014
- function renderAuthDependency() {
1015
- return `from dataclasses import dataclass
1016
-
1017
-
1018
- @dataclass(frozen=True)
1019
- class CurrentUser:
1020
- subject: str = "local-developer"
1021
-
1022
-
1023
- async def get_current_user() -> CurrentUser:
1024
- return CurrentUser()
1025
- `;
1026
- }
1027
- function renderSettings(plan) {
1028
- const pattern = genAiPattern(plan);
1029
- return `from functools import lru_cache
1030
- from pydantic_settings import BaseSettings, SettingsConfigDict
1031
-
1032
-
1033
- class Settings(BaseSettings):
1034
- model_config = SettingsConfigDict(env_file=".env", extra="ignore")
1035
-
1036
- app_name: str = ${sourceString(plan.projectName)}
1037
- app_env: str = "dev"
1038
- genai_pattern: str = "${pattern.id}"
1039
- cloud_provider: str = "${plan.provider.id}"
1040
- azure_region: str = "${plan.region.slug}"
1041
- database_url: str
1042
- redis_url: str
1043
- redis_stream_name: str = "liftoff-events"
1044
- messaging_transport: str = "redis-streams"
1045
- service_bus_queue_name: str = "${DEFAULT_FUNCTION_WORKER_QUEUE_NAME}"
1046
- service_bus_connection_string: str | None = None
1047
- service_bus_fully_qualified_namespace: str | None = None
1048
- azure_client_id: str | None = None
1049
- blob_endpoint: str | None = None
1050
- cors_allowed_origins: str = "http://localhost:5173"
1051
- pydantic_ai_model: str | None = None
1052
- langfuse_host: str | None = None
1053
- langfuse_public_key: str | None = None
1054
- langfuse_secret_key: str | None = None
1055
-
1056
-
1057
- @lru_cache
1058
- def get_settings() -> Settings:
1059
- return Settings()
1060
- `;
1061
- }
1062
- function renderModelConfig(plan) {
1063
- const pattern = genAiPattern(plan);
1064
- return `import os
1065
- from dataclasses import dataclass
1066
- from typing import Protocol
1067
-
1068
-
1069
- class ModelConfigurationError(RuntimeError):
1070
- pass
1071
-
1072
-
1073
- class AgentRunner(Protocol):
1074
- async def run(self, prompt: str) -> str:
1075
- ...
1076
-
1077
-
1078
- @dataclass(frozen=True)
1079
- class ModelConfig:
1080
- model_name: str
1081
- pattern: str = "${pattern.id}"
1082
-
1083
- @classmethod
1084
- def from_environment(cls) -> "ModelConfig":
1085
- model_name = os.getenv("PYDANTIC_AI_MODEL", "").strip()
1086
- if not model_name:
1087
- raise ModelConfigurationError(
1088
- "PYDANTIC_AI_MODEL is required before invoking production GenAI orchestration. "
1089
- "Use a PydanticAI model name such as 'openai:gpt-4.1-mini'."
1090
- )
1091
- return cls(model_name=model_name)
1092
-
1093
-
1094
- class PydanticAgentRunner:
1095
- def __init__(self, config: ModelConfig):
1096
- from pydantic_ai import Agent
1097
-
1098
- self._agent = Agent(config.model_name)
1099
-
1100
- async def run(self, prompt: str) -> str:
1101
- result = await self._agent.run(prompt)
1102
- output = getattr(result, "output", None)
1103
- if output is None:
1104
- output = getattr(result, "data", None)
1105
- if output is None:
1106
- raise RuntimeError("PydanticAI returned a result without output data.")
1107
- return str(output)
1108
-
1109
-
1110
- def build_agent_runner(config: ModelConfig | None = None) -> AgentRunner:
1111
- return PydanticAgentRunner(config or ModelConfig.from_environment())
1112
- `;
1113
- }
1114
- function renderMessagingBoundary() {
1115
- return `import json
1116
- import os
1117
- from collections.abc import Callable
1118
- from typing import Any, Protocol
1119
-
1120
-
1121
- class MessagingConfigurationError(RuntimeError):
1122
- pass
1123
-
1124
-
1125
- class MessagePublisher(Protocol):
1126
- async def publish(self, topic: str, payload: dict) -> None:
1127
- ...
1128
-
1129
-
1130
- class RedisStreamClient(Protocol):
1131
- async def xadd(self, name: str, fields: dict[str, str]) -> Any:
1132
- ...
1133
-
1134
-
1135
- class ServiceBusSender(Protocol):
1136
- async def __aenter__(self) -> "ServiceBusSender":
1137
- ...
1138
-
1139
- async def __aexit__(self, exc_type, exc, traceback) -> None:
1140
- ...
1141
-
1142
- async def send_messages(self, message: Any) -> None:
1143
- ...
1144
-
1145
-
1146
- class ServiceBusClient(Protocol):
1147
- def get_queue_sender(self, *, queue_name: str) -> ServiceBusSender:
1148
- ...
1149
-
1150
-
1151
- def _serialize(topic: str, payload: dict) -> str:
1152
- return json.dumps({"topic": topic, "payload": payload}, separators=(",", ":"), sort_keys=True)
1153
-
1154
-
1155
- class RedisStreamPublisher:
1156
- def __init__(self, client: RedisStreamClient, stream_name: str):
1157
- self._client = client
1158
- self._stream_name = stream_name
1159
-
1160
- async def publish(self, topic: str, payload: dict) -> None:
1161
- await self._client.xadd(
1162
- self._stream_name,
1163
- {"topic": topic, "payload": _serialize(topic, payload)},
1164
- )
1165
-
1166
-
1167
- class AzureServiceBusPublisher:
1168
- def __init__(
1169
- self,
1170
- client: ServiceBusClient,
1171
- queue_name: str,
1172
- message_factory: Callable[[str], Any] | None = None,
1173
- ):
1174
- self._client = client
1175
- self._queue_name = queue_name
1176
- self._message_factory = message_factory or self._default_message_factory
1177
-
1178
- @staticmethod
1179
- def _default_message_factory(body: str) -> Any:
1180
- from azure.servicebus import ServiceBusMessage
1181
-
1182
- return ServiceBusMessage(body)
1183
-
1184
- async def publish(self, topic: str, payload: dict) -> None:
1185
- message = self._message_factory(_serialize(topic, payload))
1186
- async with self._client.get_queue_sender(queue_name=self._queue_name) as sender:
1187
- await sender.send_messages(message)
1188
-
1189
-
1190
- def build_message_publisher(
1191
- transport: str,
1192
- *,
1193
- redis_client: RedisStreamClient | None = None,
1194
- service_bus_client: ServiceBusClient | None = None,
1195
- message_factory: Callable[[str], Any] | None = None,
1196
- ) -> MessagePublisher:
1197
- if transport == "redis-streams":
1198
- stream_name = os.getenv("REDIS_STREAM_NAME", "liftoff-events").strip()
1199
- if not stream_name:
1200
- raise MessagingConfigurationError("REDIS_STREAM_NAME must not be empty.")
1201
- if redis_client is None:
1202
- redis_url = os.getenv("REDIS_URL", "").strip()
1203
- if not redis_url:
1204
- raise MessagingConfigurationError("REDIS_URL is required for redis-streams messaging.")
1205
- from redis.asyncio import Redis
1206
-
1207
- redis_client = Redis.from_url(redis_url, decode_responses=True)
1208
- return RedisStreamPublisher(redis_client, stream_name)
1209
-
1210
- if transport == "azure-service-bus":
1211
- queue_name = os.getenv("SERVICE_BUS_QUEUE_NAME", "").strip()
1212
- if not queue_name:
1213
- raise MessagingConfigurationError(
1214
- "SERVICE_BUS_QUEUE_NAME is required for azure-service-bus messaging."
1215
- )
1216
- if service_bus_client is None:
1217
- connection_string = os.getenv("SERVICE_BUS_CONNECTION_STRING", "").strip()
1218
- namespace = os.getenv("SERVICE_BUS_FULLY_QUALIFIED_NAMESPACE", "").strip()
1219
- from azure.servicebus.aio import ServiceBusClient as AzureServiceBusClient
1220
-
1221
- if connection_string:
1222
- service_bus_client = AzureServiceBusClient.from_connection_string(connection_string)
1223
- elif namespace:
1224
- from azure.identity.aio import DefaultAzureCredential
1225
-
1226
- client_id = os.getenv("AZURE_CLIENT_ID", "").strip() or None
1227
- credential = DefaultAzureCredential(managed_identity_client_id=client_id)
1228
- service_bus_client = AzureServiceBusClient(namespace, credential)
1229
- else:
1230
- raise MessagingConfigurationError(
1231
- "Set SERVICE_BUS_CONNECTION_STRING or SERVICE_BUS_FULLY_QUALIFIED_NAMESPACE "
1232
- "for azure-service-bus messaging."
1233
- )
1234
- return AzureServiceBusPublisher(service_bus_client, queue_name, message_factory)
1235
-
1236
- raise MessagingConfigurationError(
1237
- f"Unsupported MESSAGING_TRANSPORT '{transport}'. "
1238
- "Expected 'redis-streams' or 'azure-service-bus'."
1239
- )
1240
- `;
1241
- }
1242
- function renderTracing() {
1243
- return `import os
1244
- from contextlib import asynccontextmanager
1245
- from dataclasses import dataclass
1246
- from typing import Any, AsyncContextManager, Protocol
1247
-
1248
-
1249
- class TracingConfigurationError(RuntimeError):
1250
- pass
1251
-
1252
-
1253
- @dataclass
1254
- class TraceHandle:
1255
- enabled: bool
1256
- trace_id: str | None
1257
- output: Any = None
1258
-
1259
- def set_output(self, output: Any) -> None:
1260
- self.output = output
1261
-
1262
-
1263
- class Tracer(Protocol):
1264
- def trace(self, name: str, input_data: Any = None) -> AsyncContextManager[TraceHandle]:
1265
- ...
1266
-
1267
-
1268
- class DisabledTracer:
1269
- @asynccontextmanager
1270
- async def trace(self, name: str, input_data: Any = None):
1271
- del name, input_data
1272
- yield TraceHandle(enabled=False, trace_id=None)
1273
-
1274
-
1275
- class LangfuseTracer:
1276
- def __init__(self, client: Any):
1277
- self._client = client
1278
-
1279
-
1280
- @asynccontextmanager
1281
- async def trace(self, name: str, input_data: Any = None):
1282
- remote_trace = self._client.start_observation(
1283
- name=name,
1284
- as_type="span",
1285
- input=input_data,
1286
- )
1287
- remote_id = getattr(remote_trace, "trace_id", None)
1288
- handle = TraceHandle(
1289
- enabled=True,
1290
- trace_id=str(remote_id) if remote_id is not None else None,
1291
- )
1292
- try:
1293
- yield handle
1294
- except Exception as error:
1295
- remote_trace.update(level="ERROR", status_message=str(error))
1296
- raise
1297
- else:
1298
- remote_trace.update(output=handle.output)
1299
- finally:
1300
- remote_trace.end()
1301
-
1302
-
1303
- def build_tracer(
1304
- *,
1305
- client: Any = None,
1306
- public_key: str | None = None,
1307
- secret_key: str | None = None,
1308
- host: str | None = None,
1309
- ) -> Tracer:
1310
- if client is not None:
1311
- return LangfuseTracer(client)
1312
-
1313
- resolved_public_key = public_key or os.getenv("LANGFUSE_PUBLIC_KEY", "").strip()
1314
- resolved_secret_key = secret_key or os.getenv("LANGFUSE_SECRET_KEY", "").strip()
1315
- if not resolved_public_key and not resolved_secret_key:
1316
- return DisabledTracer()
1317
- if not resolved_public_key or not resolved_secret_key:
1318
- raise TracingConfigurationError(
1319
- "LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY must be configured together."
1320
- )
1321
-
1322
- from langfuse import Langfuse
1323
-
1324
- resolved_host = host or os.getenv("LANGFUSE_HOST", "").strip() or None
1325
- kwargs = {
1326
- "public_key": resolved_public_key,
1327
- "secret_key": resolved_secret_key,
1328
- }
1329
- if resolved_host:
1330
- kwargs["host"] = resolved_host
1331
- return LangfuseTracer(Langfuse(**kwargs))
1332
- `;
1333
- }
1334
- function renderBackendHealthTest() {
1335
- return `from fastapi.testclient import TestClient
1336
-
1337
- from backend.apis.main import app
1338
-
1339
-
1340
- def test_health():
1341
- client = TestClient(app)
1342
- response = client.get("/health")
1343
- assert response.status_code == 200
1344
- assert response.json()["status"] == "ok"
1345
-
1346
-
1347
- def test_cors_preflight_for_local_frontend():
1348
- response = TestClient(app).options(
1349
- "/health",
1350
- headers={
1351
- "Origin": "http://localhost:5173",
1352
- "Access-Control-Request-Method": "GET",
1353
- },
1354
- )
1355
- assert response.status_code == 200
1356
- assert response.headers["access-control-allow-origin"] == "http://localhost:5173"
1357
- `;
1358
- }
1359
- function renderMessagingTest() {
1360
- return `import asyncio
1361
- import json
1362
-
1363
- from backend.orchestration.tools.messaging import build_message_publisher
1364
-
1365
-
1366
- class FakeRedisClient:
1367
- def __init__(self):
1368
- self.calls = []
1369
-
1370
- async def xadd(self, name, fields):
1371
- self.calls.append((name, fields))
1372
-
1373
-
1374
- class FakeSender:
1375
- def __init__(self):
1376
- self.messages = []
1377
-
1378
- async def __aenter__(self):
1379
- return self
1380
-
1381
- async def __aexit__(self, exc_type, exc, traceback):
1382
- return None
1383
-
1384
- async def send_messages(self, message):
1385
- self.messages.append(message)
1386
-
1387
-
1388
- class FakeServiceBusClient:
1389
- def __init__(self, sender):
1390
- self.sender = sender
1391
- self.queue_names = []
1392
-
1393
- def get_queue_sender(self, *, queue_name):
1394
- self.queue_names.append(queue_name)
1395
- return self.sender
1396
-
1397
-
1398
- def test_redis_stream_publisher_uses_xadd(monkeypatch):
1399
- monkeypatch.setenv("REDIS_STREAM_NAME", "orchestration-events")
1400
- client = FakeRedisClient()
1401
- publisher = build_message_publisher("redis-streams", redis_client=client)
1402
-
1403
- asyncio.run(publisher.publish("rag.ingest", {"source_uri": "az://document"}))
1404
-
1405
- stream_name, fields = client.calls[0]
1406
- assert stream_name == "orchestration-events"
1407
- assert fields["topic"] == "rag.ingest"
1408
- assert json.loads(fields["payload"]) == {
1409
- "payload": {"source_uri": "az://document"},
1410
- "topic": "rag.ingest",
1411
- }
1412
-
1413
-
1414
- def test_service_bus_publisher_uses_async_sender(monkeypatch):
1415
- monkeypatch.setenv("SERVICE_BUS_QUEUE_NAME", "orchestration-jobs")
1416
- sender = FakeSender()
1417
- client = FakeServiceBusClient(sender)
1418
- publisher = build_message_publisher(
1419
- "azure-service-bus",
1420
- service_bus_client=client,
1421
- message_factory=lambda body: body,
1422
- )
1423
-
1424
- asyncio.run(publisher.publish("workflow.run", {"job_id": "job-1"}))
1425
-
1426
- assert client.queue_names == ["orchestration-jobs"]
1427
- assert json.loads(sender.messages[0]) == {
1428
- "payload": {"job_id": "job-1"},
1429
- "topic": "workflow.run",
1430
- }
1431
- `;
1432
- }
1433
- function renderTracingTest() {
1434
- return `import asyncio
1435
-
1436
- import pytest
1437
-
1438
- from backend.observability.tracing import (
1439
- TracingConfigurationError,
1440
- build_tracer,
1441
- )
1442
-
1443
-
1444
- class FakeRemoteTrace:
1445
- trace_id = "trace-123"
1446
-
1447
- def __init__(self):
1448
- self.updates = []
1449
- self.ended = False
1450
-
1451
- def update(self, **values):
1452
- self.updates.append(values)
1453
-
1454
- def end(self):
1455
- self.ended = True
1456
-
1457
-
1458
- class FakeLangfuse:
1459
- def __init__(self):
1460
- self.calls = []
1461
- self.remote_trace = FakeRemoteTrace()
1462
-
1463
- def start_observation(self, **values):
1464
- self.calls.append(values)
1465
- return self.remote_trace
1466
-
1467
-
1468
- def test_unconfigured_tracing_is_explicitly_disabled(monkeypatch):
1469
- monkeypatch.delenv("LANGFUSE_PUBLIC_KEY", raising=False)
1470
- monkeypatch.delenv("LANGFUSE_SECRET_KEY", raising=False)
1471
-
1472
- async def scenario():
1473
- async with build_tracer().trace("offline") as trace:
1474
- assert trace.enabled is False
1475
- assert trace.trace_id is None
1476
-
1477
- asyncio.run(scenario())
1478
-
1479
-
1480
- def test_configured_tracing_updates_langfuse_operation():
1481
- client = FakeLangfuse()
1482
-
1483
- async def scenario():
1484
- async with build_tracer(client=client).trace(
1485
- "agent.run",
1486
- {"prompt": "hello"},
1487
- ) as trace:
1488
- assert trace.enabled is True
1489
- assert trace.trace_id == "trace-123"
1490
- trace.set_output({"answer": "world"})
1491
-
1492
- asyncio.run(scenario())
1493
- assert client.calls == [
1494
- {"name": "agent.run", "as_type": "span", "input": {"prompt": "hello"}}
1495
- ]
1496
- assert client.remote_trace.updates == [{"output": {"answer": "world"}}]
1497
- assert client.remote_trace.ended is True
1498
-
1499
-
1500
- def test_partial_langfuse_configuration_fails(monkeypatch):
1501
- monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "public")
1502
- monkeypatch.delenv("LANGFUSE_SECRET_KEY", raising=False)
1503
- with pytest.raises(TracingConfigurationError, match="configured together"):
1504
- build_tracer()
1505
- `;
1506
- }
1507
- function renderAlembicIni() {
1508
- return `[alembic]
1509
- script_location = %(here)s/migrations
1510
- sqlalchemy.url = driver://user:pass@localhost/dbname
1511
- `;
1512
- }
1513
- function renderAlembicEnv() {
1514
- return `import os
1515
-
1516
- from alembic import context
1517
- from sqlalchemy import create_engine
1518
-
1519
-
1520
- def run_migrations_online():
1521
- database_url = os.environ.get("DATABASE_URL")
1522
- if not database_url:
1523
- raise RuntimeError("DATABASE_URL is required to run migrations")
1524
- database_url = database_url.replace("postgresql+asyncpg://", "postgresql+psycopg://", 1)
1525
- database_url = database_url.replace("postgresql://", "postgresql+psycopg://", 1)
1526
- connectable = create_engine(database_url)
1527
- with connectable.connect() as connection:
1528
- context.configure(connection=connection, target_metadata=None)
1529
- with context.begin_transaction():
1530
- context.run_migrations()
1531
-
1532
-
1533
- run_migrations_online()
1534
- `;
1535
- }
1536
- function renderInitialMigration(plan) {
1537
- const vectorExtension = genAiPattern(plan).id === 'rag' ? ' op.execute("CREATE EXTENSION IF NOT EXISTS vector")\n' : '';
1538
- return `from alembic import op
1539
- import sqlalchemy as sa
1540
-
1541
- revision = "0001_initial"
1542
- down_revision = None
1543
- branch_labels = None
1544
- depends_on = None
1545
-
1546
-
1547
- def upgrade():
1548
- ${vectorExtension} op.create_table(
1549
- "events",
1550
- sa.Column("id", sa.Integer(), primary_key=True),
1551
- sa.Column("event_type", sa.String(length=120), nullable=False),
1552
- sa.Column("payload", sa.JSON(), nullable=False),
1553
- sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
1554
- )
1555
-
1556
-
1557
- def downgrade():
1558
- op.drop_table("events")
1559
- `;
1560
- }
1561
- function renderDatabaseSchema(plan) {
1562
- return `CREATE TABLE IF NOT EXISTS events (
1563
- id SERIAL PRIMARY KEY,
1564
- event_type VARCHAR(120) NOT NULL,
1565
- payload JSONB NOT NULL,
1566
- created_at TIMESTAMPTZ NOT NULL DEFAULT now()
1567
- );
1568
- ${genAiPattern(plan).id === 'rag' ? '\nCREATE EXTENSION IF NOT EXISTS vector;\n' : ''}`;
1569
- }
1570
- function renderPatternAgent(plan) {
1571
- const pattern = genAiPattern(plan);
1572
- const moduleName = pyModule(pattern.id);
1573
- if (pattern.id === 'rag') {
1574
- return `import os
1575
-
1576
- from backend.observability.tracing import Tracer, build_tracer
1577
- from backend.orchestration.model_config import AgentRunner, build_agent_runner
1578
- from backend.orchestration.tools.messaging import MessagePublisher, build_message_publisher
1579
-
1580
-
1581
- async def _run_agent(
1582
- operation: str,
1583
- prompt: str,
1584
- runner: AgentRunner | None,
1585
- tracer: Tracer | None,
1586
- ) -> str:
1587
- selected_runner = runner or build_agent_runner()
1588
- selected_tracer = tracer or build_tracer()
1589
- async with selected_tracer.trace(operation, {"prompt": prompt}) as trace:
1590
- output = await selected_runner.run(prompt)
1591
- trace.set_output({"text": output})
1592
- return output
1593
-
1594
-
1595
- async def answer_question(
1596
- question: str,
1597
- *,
1598
- runner: AgentRunner | None = None,
1599
- tracer: Tracer | None = None,
1600
- ) -> dict:
1601
- answer = await _run_agent(
1602
- "rag.query",
1603
- f"Answer the question using retrieved evidence when available.\\nQuestion: {question}",
1604
- runner,
1605
- tracer,
1606
- )
1607
- return {
1608
- "answer": answer,
1609
- "question": question,
1610
- "citations": [],
1611
- }
1612
-
1613
-
1614
- async def enqueue_ingestion(
1615
- source_uri: str,
1616
- *,
1617
- publisher: MessagePublisher | None = None,
1618
- ) -> dict:
1619
- selected_publisher = publisher or build_message_publisher(
1620
- os.getenv("MESSAGING_TRANSPORT", "redis-streams")
1621
- )
1622
- await selected_publisher.publish("rag.ingest", {"source_uri": source_uri})
1623
- return {"status": "queued", "source_uri": source_uri}
1624
- `;
1625
- }
1626
- if (pattern.id === 'streaming') {
1627
- return `import json
1628
-
1629
- from backend.observability.tracing import Tracer, build_tracer
1630
- from backend.orchestration.model_config import AgentRunner, build_agent_runner
1631
-
1632
-
1633
- async def stream_response(
1634
- prompt: str,
1635
- *,
1636
- runner: AgentRunner | None = None,
1637
- tracer: Tracer | None = None,
1638
- ):
1639
- selected_runner = runner or build_agent_runner()
1640
- selected_tracer = tracer or build_tracer()
1641
- async with selected_tracer.trace("streaming.run", {"prompt": prompt}) as trace:
1642
- output = await selected_runner.run(
1643
- f"Respond concisely and safely to this streaming request:\\n{prompt}"
1644
- )
1645
- trace.set_output({"text": output})
1646
- yield f"data: {json.dumps({'text': output})}\\n\\n"
1647
- `;
1648
- }
1649
- const instruction = pattern.id === 'generic'
1650
- ? 'Respond safely and usefully to this general-purpose AI request:'
1651
- : `Run the ${pattern.label} orchestration contract for this input:`;
1652
- return `from backend.observability.tracing import Tracer, build_tracer
1653
- from backend.orchestration.model_config import AgentRunner, build_agent_runner
1654
-
1655
-
1656
- async def run_${moduleName}(
1657
- input_text: str,
1658
- *,
1659
- runner: AgentRunner | None = None,
1660
- tracer: Tracer | None = None,
1661
- ) -> dict:
1662
- selected_runner = runner or build_agent_runner()
1663
- selected_tracer = tracer or build_tracer()
1664
- prompt = (
1665
- "${instruction}\\n"
1666
- f"{input_text}"
1667
- )
1668
- async with selected_tracer.trace("${pattern.id}.run", {"input": input_text}) as trace:
1669
- output = await selected_runner.run(prompt)
1670
- trace.set_output({"result": output})
1671
- return {
1672
- "result": output,
1673
- "input": input_text,
1674
- }
1675
- `;
1676
- }
1677
- function renderPatternAgentTest(plan) {
1678
- const pattern = genAiPattern(plan);
1679
- const moduleName = pyModule(pattern.id);
1680
- const agentModule = `backend.orchestration.agents.${moduleName}_agent`;
1681
- if (pattern.id === 'rag') {
1682
- return `import asyncio
1683
-
1684
- import pytest
1685
-
1686
- from ${agentModule} import answer_question, enqueue_ingestion
1687
- from backend.observability.tracing import DisabledTracer
1688
- from backend.orchestration.model_config import ModelConfigurationError
1689
-
1690
-
1691
- class FakeRunner:
1692
- async def run(self, prompt):
1693
- assert "Question: What is Liftoff?" in prompt
1694
- return "Liftoff is the generated orchestration starter."
1695
-
1696
-
1697
- class FakePublisher:
1698
- def __init__(self):
1699
- self.messages = []
1700
-
1701
- async def publish(self, topic, payload):
1702
- self.messages.append((topic, payload))
1703
-
1704
-
1705
- def test_rag_query_uses_injected_runner_without_network():
1706
- result = asyncio.run(
1707
- answer_question(
1708
- "What is Liftoff?",
1709
- runner=FakeRunner(),
1710
- tracer=DisabledTracer(),
1711
- )
1712
- )
1713
- assert result == {
1714
- "answer": "Liftoff is the generated orchestration starter.",
1715
- "question": "What is Liftoff?",
1716
- "citations": [],
1717
- }
1718
-
1719
-
1720
- def test_rag_ingestion_uses_injected_publisher():
1721
- publisher = FakePublisher()
1722
- result = asyncio.run(
1723
- enqueue_ingestion("az://documents/one.pdf", publisher=publisher)
1724
- )
1725
- assert result == {
1726
- "status": "queued",
1727
- "source_uri": "az://documents/one.pdf",
1728
- }
1729
- assert publisher.messages == [
1730
- ("rag.ingest", {"source_uri": "az://documents/one.pdf"})
1731
- ]
1732
-
1733
-
1734
- def test_missing_model_configuration_is_explicit(monkeypatch):
1735
- monkeypatch.delenv("PYDANTIC_AI_MODEL", raising=False)
1736
- with pytest.raises(ModelConfigurationError, match="PYDANTIC_AI_MODEL is required"):
1737
- asyncio.run(answer_question("unconfigured"))
1738
- `;
1739
- }
1740
- if (pattern.id === 'streaming') {
1741
- return `import asyncio
1742
-
1743
- import pytest
1744
-
1745
- from ${agentModule} import stream_response
1746
- from backend.observability.tracing import DisabledTracer
1747
- from backend.orchestration.model_config import ModelConfigurationError
1748
-
1749
-
1750
- class FakeRunner:
1751
- async def run(self, prompt):
1752
- assert "stream this" in prompt
1753
- return "offline streamed answer"
1754
-
1755
-
1756
- def test_streaming_uses_injected_runner_without_network():
1757
- async def collect():
1758
- return [
1759
- chunk
1760
- async for chunk in stream_response(
1761
- "stream this",
1762
- runner=FakeRunner(),
1763
- tracer=DisabledTracer(),
1764
- )
1765
- ]
1766
-
1767
- chunks = asyncio.run(collect())
1768
- assert chunks == ['data: {"text": "offline streamed answer"}\\n\\n']
1769
-
1770
-
1771
- def test_missing_model_configuration_is_explicit(monkeypatch):
1772
- monkeypatch.delenv("PYDANTIC_AI_MODEL", raising=False)
1773
-
1774
- async def collect():
1775
- return [chunk async for chunk in stream_response("unconfigured")]
1776
-
1777
- with pytest.raises(ModelConfigurationError, match="PYDANTIC_AI_MODEL is required"):
1778
- asyncio.run(collect())
1779
- `;
1780
- }
1781
- return `import asyncio
1782
-
1783
- import pytest
1784
-
1785
- from ${agentModule} import run_${moduleName}
1786
- from backend.observability.tracing import DisabledTracer
1787
- from backend.orchestration.model_config import ModelConfigurationError
1788
-
1789
-
1790
- class FakeRunner:
1791
- async def run(self, prompt):
1792
- assert "offline input" in prompt
1793
- return "offline ${pattern.id} result"
1794
-
1795
-
1796
- def test_${moduleName}_uses_injected_runner_without_network():
1797
- result = asyncio.run(
1798
- run_${moduleName}(
1799
- "offline input",
1800
- runner=FakeRunner(),
1801
- tracer=DisabledTracer(),
1802
- )
1803
- )
1804
- assert result == {
1805
- "result": "offline ${pattern.id} result",
1806
- "input": "offline input",
1807
- }
1808
-
1809
-
1810
- def test_missing_model_configuration_is_explicit(monkeypatch):
1811
- monkeypatch.delenv("PYDANTIC_AI_MODEL", raising=False)
1812
- with pytest.raises(ModelConfigurationError, match="PYDANTIC_AI_MODEL is required"):
1813
- asyncio.run(run_${moduleName}("unconfigured"))
1814
- `;
1815
- }
1816
- function renderPromptTemplate(plan) {
1817
- const pattern = genAiPattern(plan);
1818
- if (pattern.id === 'generic') {
1819
- return `# Generic GenAI System Prompt
1820
-
1821
- Respond safely and usefully to the general-purpose request.
1822
-
1823
- Do not assume retrieval, conversation memory, tools, streaming, fine-tuning, multi-agent coordination, or workflow behavior unless the project explicitly adds it.
1824
- `;
1825
- }
1826
- return `# ${pattern.label} Prompt
1827
-
1828
- You are implementing a ${pattern.label} generated by Mission Control Liftoff.
1829
-
1830
- Use PydanticAI orchestration and return outputs that match the API contract.
1831
- `;
1832
- }
1833
- function renderPromptReadme() {
1834
- return `# Prompt Templates
1835
-
1836
- Store prompt templates here and reference them from the PydanticAI orchestration layer.
1837
- `;
1838
- }
1839
- function renderVectorStore() {
1840
- return `from typing import Protocol
1841
-
1842
-
1843
- class VectorStore(Protocol):
1844
- async def search(self, query: str, limit: int = 5) -> list[dict]:
1845
- ...
1846
-
1847
-
1848
- class PgVectorStore:
1849
- async def search(self, query: str, limit: int = 5) -> list[dict]:
1850
- return []
1851
- `;
1852
- }
1853
- function renderPatternWorker(plan) {
1854
- const pattern = genAiPattern(plan);
1855
- return `async def run_worker() -> None:
1856
- # Consume ${pattern.label} jobs from the configured messaging boundary.
1857
- return None
1858
- `;
1859
- }
1860
- function renderFunctionsReadme() {
1861
- return `# Azure Functions Workers
1862
-
1863
- Azure Functions trigger adapters live under \`functions/<worker-name>\`.
1864
-
1865
- Keep reusable GenAI orchestration, model configuration, prompt handling, and domain logic under \`backend/orchestration\`. Use \`backend/workers\` for backend-adjacent or containerized workers; use this folder for Azure Functions runtime files such as \`host.json\`, trigger bindings, local settings, and Function app tests.
1866
- `;
1867
- }
1868
- function renderFunctionWorkerReadme(plan) {
1869
- const workerName = functionWorkerName(plan);
1870
- const pattern = genAiPattern(plan);
1871
- return `# ${workerName}
1872
-
1873
- Azure Functions worker scaffold for ${pattern.label}.
1874
-
1875
- This Function app uses the Python v2 decorator programming model and a Service Bus queue trigger. The trigger adapter should stay thin: decode the message, validate the envelope, and call shared code from \`backend/orchestration\` after that shared code is packaged with the Function app.
1876
-
1877
- Deployed triggers use \`ServiceBusConnection__fullyQualifiedNamespace\` and \`ServiceBusConnection__clientId\` to select the same user-assigned identity that OpenTofu grants the Service Bus Data Receiver role. \`SERVICEBUS_QUEUE_NAME\` is populated from \`function_worker_queue_name\`. Function host storage uses the complete \`AzureWebJobsStorage\` connection setting.
1878
-
1879
- ## Local Development
1880
-
1881
- \`\`\`bash
1882
- uv sync --frozen --project ../../backend --extra test --extra functions
1883
- cp local.settings.example.json local.settings.json
1884
- uv run --project ../../backend --directory . python -m pytest -q
1885
- func start
1886
- \`\`\`
1887
- `;
1888
- }
1889
- function renderFunctionHostJson() {
1890
- return JSON.stringify({
1891
- version: '2.0',
1892
- extensionBundle: {
1893
- id: 'Microsoft.Azure.Functions.ExtensionBundle',
1894
- version: '[4.*, 5.0.0)'
1895
- }
1896
- }, null, 2);
1897
- }
1898
- function renderFunctionLocalSettings(plan) {
1899
- const pattern = genAiPattern(plan);
1900
- return JSON.stringify({
1901
- IsEncrypted: false,
1902
- Values: {
1903
- AzureWebJobsStorage: 'UseDevelopmentStorage=true',
1904
- FUNCTIONS_WORKER_RUNTIME: 'python',
1905
- SERVICEBUS_QUEUE_NAME: DEFAULT_FUNCTION_WORKER_QUEUE_NAME,
1906
- ServiceBusConnection__fullyQualifiedNamespace: '<service-bus-namespace>.servicebus.windows.net',
1907
- GENAI_PATTERN: pattern.id,
1908
- SHARED_ORCHESTRATION_ROOT: '../../backend'
1909
- }
1910
- }, null, 2);
1911
- }
1912
- function renderFunctionRequirements() {
1913
- return renderFunctionRequirementsAsset();
1914
- }
1915
- function renderFunctionApp(plan) {
1916
- const pattern = genAiPattern(plan);
1917
- const moduleName = pyModule(pattern.id);
1918
- return `import json
1919
- import logging
1920
-
1921
- import azure.functions as func
1922
-
1923
-
1924
- app = func.FunctionApp()
1925
-
1926
-
1927
- def decode_message_payload(body: str) -> dict:
1928
- try:
1929
- value = json.loads(body)
1930
- except json.JSONDecodeError:
1931
- return {"raw": body}
1932
- if isinstance(value, dict):
1933
- return value
1934
- return {"value": value}
1935
-
1936
-
1937
- @app.service_bus_queue_trigger(
1938
- arg_name="message",
1939
- queue_name="%SERVICEBUS_QUEUE_NAME%",
1940
- connection="ServiceBusConnection",
1941
- )
1942
- def process_${moduleName}_work(message: func.ServiceBusMessage) -> None:
1943
- payload = decode_message_payload(message.get_body().decode("utf-8"))
1944
- logging.info("Received ${pattern.id} worker message with keys: %s", sorted(payload.keys()))
1945
- # Keep this adapter thin; call backend.orchestration code from packaged shared modules.
1946
- `;
1947
- }
1948
- function renderFunctionTest() {
1949
- return `from function_app import decode_message_payload
1950
-
1951
-
1952
- def test_decode_message_payload_for_json_object():
1953
- assert decode_message_payload('{"source_uri":"az://documents/example.pdf"}') == {
1954
- "source_uri": "az://documents/example.pdf"
1955
- }
1956
-
1957
-
1958
- def test_decode_message_payload_for_plain_text():
1959
- assert decode_message_payload("plain text") == {"raw": "plain text"}
1960
- `;
1961
- }
1962
- function renderFunctionFuncIgnore() {
1963
- return `.venv/
1964
- __pycache__/
1965
- .pytest_cache/
1966
- local.settings.json
1967
- tests/
1968
- `;
1969
- }
1970
- function renderFunctionGitIgnore() {
1971
- return `.venv/
1972
- __pycache__/
1973
- .pytest_cache/
1974
- local.settings.json
1975
- `;
1976
- }
1977
- function renderBackendEnv(plan, environment) {
1978
- const pattern = genAiPattern(plan);
1979
- const transport = environment === 'dev' ? 'redis-streams' : 'azure-service-bus';
1980
- return `APP_ENV=${environment}
1981
- APP_NAME=${plan.safeProjectName}
1982
- GENAI_PATTERN=${pattern.id}
1983
- CLOUD_PROVIDER=${plan.provider.id}
1984
- AZURE_REGION=${plan.region.slug}
1985
- DATABASE_URL=postgresql+asyncpg://postgres:postgres@postgres:5432/${plan.safeProjectName.replace(/-/g, '_')}
1986
- REDIS_URL=redis://redis:6379/0
1987
- REDIS_STREAM_NAME=liftoff-events
1988
- MESSAGING_TRANSPORT=${transport}
1989
- SERVICE_BUS_QUEUE_NAME=${DEFAULT_FUNCTION_WORKER_QUEUE_NAME}
1990
- SERVICE_BUS_CONNECTION_STRING=
1991
- SERVICE_BUS_FULLY_QUALIFIED_NAMESPACE=${environment === 'dev' ? '' : '<service-bus-namespace>.servicebus.windows.net'}
1992
- AZURE_CLIENT_ID=
1993
- BLOB_ENDPOINT=
1994
- CORS_ALLOWED_ORIGINS=http://localhost:5173
1995
- PYDANTIC_AI_MODEL=
1996
- LANGFUSE_HOST=${environment === 'dev' ? 'http://langfuse:3000' : ''}
1997
- LANGFUSE_PUBLIC_KEY=
1998
- LANGFUSE_SECRET_KEY=
1999
- `;
2000
- }
2001
- function renderFunctionsEnv(plan, environment) {
2002
- const pattern = genAiPattern(plan);
2003
- return `APP_ENV=${environment}
2004
- APP_NAME=${plan.safeProjectName}
2005
- GENAI_PATTERN=${pattern.id}
2006
- FUNCTIONS_WORKER_RUNTIME=python
2007
- SERVICEBUS_QUEUE_NAME=${DEFAULT_FUNCTION_WORKER_QUEUE_NAME}
2008
- ServiceBusConnection__fullyQualifiedNamespace=<service-bus-namespace>.servicebus.windows.net
2009
- ServiceBusConnection__clientId=<managed-identity-client-id>
2010
- AzureWebJobsStorage=<storage-connection-string>
2011
- SHARED_ORCHESTRATION_ROOT=../../backend
2012
- `;
2013
- }
2014
- function renderDockerCompose(plan) {
2015
- const localEnvironment = plan.environments.find((environment) => environment.id === 'dev') ?? plan.environments[0];
2016
- const frontendService = plan.includeFrontend ? `
2017
- frontend:
2018
- build:
2019
- context: ./frontend
2020
- ports:
2021
- - "5173:80"
2022
- depends_on:
2023
- - backend
2024
- ` : '';
2025
- const postgresImage = plan.workload === 'genai'
2026
- ? formatContainerImage(supportedStack.containers.pgvector)
2027
- : formatContainerImage(supportedStack.containers.postgres);
2028
- return `services:
2029
- backend:
2030
- build:
2031
- context: .
2032
- dockerfile: Dockerfile
2033
- env_file:
2034
- - ./environments/${localEnvironment.id}/backend.env
2035
- environment:
2036
- MESSAGING_TRANSPORT: redis-streams
2037
- BLOB_ENDPOINT: http://azurite:10000/devstoreaccount1
2038
- ports:
2039
- - "8000:8000"
2040
- depends_on:
2041
- - postgres
2042
- - redis
2043
- - azurite
2044
- - mailpit
2045
- ${frontendService}
2046
- postgres:
2047
- image: ${postgresImage}
2048
- environment:
2049
- POSTGRES_USER: postgres
2050
- POSTGRES_PASSWORD: postgres
2051
- POSTGRES_DB: ${plan.safeProjectName.replace(/-/g, '_')}
2052
- ports:
2053
- - "5432:5432"
2054
- healthcheck:
2055
- test: ["CMD-SHELL", "pg_isready -U postgres"]
2056
- interval: 3s
2057
- timeout: 3s
2058
- retries: 10
2059
-
2060
- redis:
2061
- image: ${formatContainerImage(supportedStack.containers.redis)}
2062
- ports:
2063
- - "6379:6379"
2064
-
2065
- azurite:
2066
- image: ${formatContainerImage(supportedStack.containers.azurite)}
2067
- command: azurite --blobHost 0.0.0.0
2068
- ports:
2069
- - "10000:10000"
2070
-
2071
- mailpit:
2072
- image: ${formatContainerImage(supportedStack.containers.mailpit)}
2073
- ports:
2074
- - "8025:8025"
2075
-
2076
- ${plan.workload === 'genai' ? ` langfuse-worker:
2077
- image: ${formatContainerImage(supportedStack.containers['langfuse-worker'])}
2078
- profiles:
2079
- - observability
2080
- depends_on: &langfuse-dependencies
2081
- postgres:
2082
- condition: service_healthy
2083
- langfuse-redis:
2084
- condition: service_healthy
2085
- clickhouse:
2086
- condition: service_healthy
2087
- minio:
2088
- condition: service_healthy
2089
- environment: &langfuse-environment
2090
- DATABASE_URL: postgresql://postgres:postgres@postgres:5432/${plan.safeProjectName.replace(/-/g, '_')}
2091
- NEXTAUTH_URL: http://localhost:3000
2092
- SALT: local-development-salt
2093
- ENCRYPTION_KEY: 0000000000000000000000000000000000000000000000000000000000000000
2094
- TELEMETRY_ENABLED: "false"
2095
- CLICKHOUSE_MIGRATION_URL: clickhouse://clickhouse:9000
2096
- CLICKHOUSE_URL: http://clickhouse:8123
2097
- CLICKHOUSE_USER: clickhouse
2098
- CLICKHOUSE_PASSWORD: clickhouse
2099
- REDIS_HOST: langfuse-redis
2100
- REDIS_PORT: 6379
2101
- REDIS_AUTH: langfuse-redis
2102
- LANGFUSE_S3_EVENT_UPLOAD_BUCKET: langfuse
2103
- LANGFUSE_S3_EVENT_UPLOAD_REGION: auto
2104
- LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: minio
2105
- LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: miniosecret
2106
- LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: http://minio:9000
2107
- LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: "true"
2108
- LANGFUSE_S3_MEDIA_UPLOAD_BUCKET: langfuse
2109
- LANGFUSE_S3_MEDIA_UPLOAD_REGION: auto
2110
- LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID: minio
2111
- LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY: miniosecret
2112
- LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: http://minio:9000
2113
- LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE: "true"
2114
-
2115
- langfuse:
2116
- image: ${formatContainerImage(supportedStack.containers['langfuse-web'])}
2117
- profiles:
2118
- - observability
2119
- depends_on: *langfuse-dependencies
2120
- environment:
2121
- <<: *langfuse-environment
2122
- NEXTAUTH_SECRET: local-development-secret
2123
- LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: http://localhost:9090
2124
- ports:
2125
- - "3000:3000"
2126
-
2127
- clickhouse:
2128
- image: ${formatContainerImage(supportedStack.containers.clickhouse)}
2129
- profiles:
2130
- - observability
2131
- user: "101:101"
2132
- environment:
2133
- CLICKHOUSE_DB: default
2134
- CLICKHOUSE_USER: clickhouse
2135
- CLICKHOUSE_PASSWORD: clickhouse
2136
- volumes:
2137
- - langfuse-clickhouse-data:/var/lib/clickhouse
2138
- - langfuse-clickhouse-logs:/var/log/clickhouse-server
2139
- healthcheck:
2140
- test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1"]
2141
- interval: 5s
2142
- timeout: 5s
2143
- retries: 10
2144
-
2145
- minio:
2146
- image: ${formatContainerImage(supportedStack.containers.minio)}
2147
- profiles:
2148
- - observability
2149
- entrypoint: sh
2150
- command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data'
2151
- environment:
2152
- MINIO_ROOT_USER: minio
2153
- MINIO_ROOT_PASSWORD: miniosecret
2154
- ports:
2155
- - "9090:9000"
2156
- - "127.0.0.1:9091:9001"
2157
- volumes:
2158
- - langfuse-minio-data:/data
2159
- healthcheck:
2160
- test: ["CMD", "mc", "ready", "local"]
2161
- interval: 1s
2162
- timeout: 5s
2163
- retries: 5
2164
-
2165
- langfuse-redis:
2166
- image: ${formatContainerImage(supportedStack.containers.redis)}
2167
- profiles:
2168
- - observability
2169
- command: ["redis-server", "--requirepass", "langfuse-redis", "--maxmemory-policy", "noeviction"]
2170
- volumes:
2171
- - langfuse-redis-data:/data
2172
- healthcheck:
2173
- test: ["CMD", "redis-cli", "-a", "langfuse-redis", "ping"]
2174
- interval: 3s
2175
- timeout: 10s
2176
- retries: 10
2177
- ` : ''}
2178
- ${plan.workload === 'genai' ? `volumes:
2179
- langfuse-clickhouse-data:
2180
- langfuse-clickhouse-logs:
2181
- langfuse-minio-data:
2182
- langfuse-redis-data:
2183
- ` : ''}
2184
- `;
2185
- }
2186
- function renderTofuProviders() {
2187
- return `provider "azurerm" {
2188
- features {}
2189
- }
2190
- `;
2191
- }
2192
- function renderTofuVariables(plan) {
2193
- const frontendVariables = plan.includeFrontend ? `
2194
- variable "frontend_image" {
2195
- type = string
2196
- default = "${formatContainerImage(supportedStack.containers['container-apps-bootstrap'])}"
2197
- description = "Frontend image. Replace the bootstrap image with the generated frontend image after pushing it to ACR."
2198
- }
2199
- ` : '';
2200
- const functionVariables = hasFunctionWorker(plan) ? `
2201
- variable "function_worker_queue_name" {
2202
- type = string
2203
- default = "events"
2204
- description = "Service Bus queue consumed by the generated Azure Functions worker."
2205
- }
2206
-
2207
- variable "functions_python_version" {
2208
- type = string
2209
- default = "${supportedStack.runtimes.python.releaseLine}"
2210
- description = "Python runtime version for the generated Azure Functions worker."
2211
- }
2212
- ` : '';
2213
- return `variable "environment" {
2214
- type = string
2215
- description = "Deployment environment name."
2216
- }
2217
-
2218
- variable "location" {
2219
- type = string
2220
- description = "Azure region slug."
2221
- default = "${plan.region.slug}"
2222
- }
2223
-
2224
- variable "resource_suffix" {
2225
- type = string
2226
- description = "Twelve-character lowercase alphanumeric suffix for globally scoped Azure resource names."
2227
-
2228
- validation {
2229
- condition = can(regex("^[a-z0-9]{12}$", var.resource_suffix))
2230
- error_message = "resource_suffix must contain exactly 12 lowercase letters or numbers."
2231
- }
2232
- }
2233
-
2234
- variable "backend_image" {
2235
- type = string
2236
- default = "${formatContainerImage(supportedStack.containers['container-apps-bootstrap'])}"
2237
- description = "Backend image. Replace the bootstrap image with the generated backend image after pushing it to ACR."
2238
- }
2239
-
2240
- variable "backend_target_port" {
2241
- type = number
2242
- default = 80
2243
- description = "Backend ingress port. Set to 8000 when switching from the bootstrap image to the generated backend."
2244
- }
2245
- ${frontendVariables}
2246
- variable "postgres_admin_password" {
2247
- type = string
2248
- sensitive = true
2249
- description = "PostgreSQL administrator password supplied at apply time."
2250
- }
2251
-
2252
- variable "enable_private_networking" {
2253
- type = bool
2254
- default = false
2255
- description = "Enable production-oriented private-networking-ready settings."
2256
- }
2257
- ${functionVariables}
2258
- `;
2259
- }
2260
- function renderTofuMain(plan) {
2261
- const functionPattern = plan.workload === 'genai' && hasFunctionWorker(plan)
2262
- ? genAiPattern(plan)
2263
- : undefined;
2264
- const names = buildAzureResourceNames(plan, '${var.environment}', '${var.resource_suffix}');
2265
- const queueName = hasFunctionWorker(plan)
2266
- ? 'var.function_worker_queue_name'
2267
- : JSON.stringify(DEFAULT_FUNCTION_WORKER_QUEUE_NAME);
2268
- const projectIdentityEnv = plan.workload === 'genai' ? `
2269
- env {
2270
- name = "GENAI_PATTERN"
2271
- value = "${genAiPattern(plan).id}"
2272
- }
2273
- ` : `
2274
- env {
2275
- name = "API_STACK"
2276
- value = "${plan.apiStack.id}"
2277
- }
2278
- `;
2279
- const frontendCorsEnvironment = plan.includeFrontend ? `
2280
- env {
2281
- name = "CORS_ALLOWED_ORIGINS"
2282
- value = "https://\${azurerm_container_app.frontend.ingress[0].fqdn}"
2283
- }
2284
- ` : '';
2285
- const frontendContainer = plan.includeFrontend ? `
2286
- resource "azurerm_container_app" "frontend" {
2287
- name = "${names.frontendContainerApp}"
2288
- container_app_environment_id = azurerm_container_app_environment.main.id
2289
- resource_group_name = azurerm_resource_group.main.name
2290
- revision_mode = "Single"
2291
-
2292
- identity {
2293
- type = "UserAssigned"
2294
- identity_ids = [azurerm_user_assigned_identity.app.id]
2295
- }
2296
-
2297
- registry {
2298
- server = azurerm_container_registry.main.login_server
2299
- identity = azurerm_user_assigned_identity.app.id
2300
- }
2301
-
2302
- template {
2303
- container {
2304
- name = "frontend"
2305
- image = var.frontend_image
2306
- cpu = 0.25
2307
- memory = "0.5Gi"
2308
- }
2309
- }
2310
-
2311
- ingress {
2312
- external_enabled = true
2313
- target_port = 80
2314
- traffic_weight {
2315
- percentage = 100
2316
- latest_revision = true
2317
- }
2318
- }
2319
-
2320
- depends_on = [azurerm_role_assignment.acr_pull]
2321
- }
2322
- ` : '';
2323
- const functionWorker = hasFunctionWorker(plan) ? `
2324
- resource "azurerm_service_plan" "functions" {
2325
- name = "${names.functionServicePlan}"
2326
- resource_group_name = azurerm_resource_group.main.name
2327
- location = azurerm_resource_group.main.location
2328
- os_type = "Linux"
2329
- sku_name = "Y1"
2330
- }
2331
-
2332
- resource "azurerm_linux_function_app" "worker" {
2333
- name = "${names.functionApp}"
2334
- resource_group_name = azurerm_resource_group.main.name
2335
- location = azurerm_resource_group.main.location
2336
- service_plan_id = azurerm_service_plan.functions.id
2337
- storage_account_name = azurerm_storage_account.main.name
2338
- storage_account_access_key = azurerm_storage_account.main.primary_access_key
2339
-
2340
- identity {
2341
- type = "UserAssigned"
2342
- identity_ids = [azurerm_user_assigned_identity.app.id]
2343
- }
2344
-
2345
- site_config {
2346
- application_stack {
2347
- python_version = var.functions_python_version
2348
- }
2349
- }
2350
-
2351
- app_settings = {
2352
- APP_ENV = var.environment
2353
- APP_NAME = "${plan.safeProjectName}"
2354
- GENAI_PATTERN = "${functionPattern?.id}"
2355
- FUNCTIONS_WORKER_RUNTIME = "python"
2356
- SERVICEBUS_QUEUE_NAME = var.function_worker_queue_name
2357
- ServiceBusConnection__clientId = azurerm_user_assigned_identity.app.client_id
2358
- ServiceBusConnection__fullyQualifiedNamespace = "\${azurerm_servicebus_namespace.main.name}.servicebus.windows.net"
2359
- SHARED_ORCHESTRATION_ROOT = "../../backend"
2360
- }
2361
- }
2362
-
2363
- resource "azurerm_role_assignment" "function_servicebus_receiver" {
2364
- scope = azurerm_servicebus_namespace.main.id
2365
- role_definition_name = "Azure Service Bus Data Receiver"
2366
- principal_id = azurerm_user_assigned_identity.app.principal_id
2367
- }
2368
-
2369
- resource "azurerm_role_assignment" "function_storage_blob_contributor" {
2370
- scope = azurerm_storage_account.main.id
2371
- role_definition_name = "Storage Blob Data Contributor"
2372
- principal_id = azurerm_user_assigned_identity.app.principal_id
2373
- }
2374
- ` : '';
2375
- return `resource "azurerm_resource_group" "main" {
2376
- name = "${names.resourceGroup}"
2377
- location = var.location
2378
- }
2379
-
2380
- resource "azurerm_container_registry" "main" {
2381
- name = "${names.containerRegistry}"
2382
- resource_group_name = azurerm_resource_group.main.name
2383
- location = azurerm_resource_group.main.location
2384
- sku = "Basic"
2385
- admin_enabled = false
2386
- }
2387
-
2388
- resource "azurerm_user_assigned_identity" "app" {
2389
- name = "${names.identity}"
2390
- resource_group_name = azurerm_resource_group.main.name
2391
- location = azurerm_resource_group.main.location
2392
- }
2393
-
2394
- resource "azurerm_role_assignment" "acr_pull" {
2395
- scope = azurerm_container_registry.main.id
2396
- role_definition_name = "AcrPull"
2397
- principal_id = azurerm_user_assigned_identity.app.principal_id
2398
- }
2399
-
2400
- resource "azurerm_container_app_environment" "main" {
2401
- name = "${names.containerAppEnvironment}"
2402
- resource_group_name = azurerm_resource_group.main.name
2403
- location = azurerm_resource_group.main.location
2404
- }
2405
-
2406
- resource "azurerm_container_app" "backend" {
2407
- name = "${names.backendContainerApp}"
2408
- container_app_environment_id = azurerm_container_app_environment.main.id
2409
- resource_group_name = azurerm_resource_group.main.name
2410
- revision_mode = "Single"
2411
-
2412
- identity {
2413
- type = "UserAssigned"
2414
- identity_ids = [azurerm_user_assigned_identity.app.id]
2415
- }
2416
-
2417
- registry {
2418
- server = azurerm_container_registry.main.login_server
2419
- identity = azurerm_user_assigned_identity.app.id
2420
- }
2421
-
2422
- secret {
2423
- name = "database-url"
2424
- value = "postgresql://liftoffadmin:\${urlencode(var.postgres_admin_password)}@\${azurerm_postgresql_flexible_server.main.fqdn}:5432/postgres?sslmode=require"
2425
- }
2426
-
2427
- secret {
2428
- name = "redis-url"
2429
- value = "rediss://:\${urlencode(azurerm_redis_cache.main.primary_access_key)}@\${azurerm_redis_cache.main.hostname}:\${azurerm_redis_cache.main.ssl_port}/0"
2430
- }
2431
-
2432
- template {
2433
- container {
2434
- name = "backend"
2435
- image = var.backend_image
2436
- cpu = 0.5
2437
- memory = "1Gi"
2438
-
2439
- env {
2440
- name = "APP_ENV"
2441
- value = var.environment
2442
- }
2443
-
2444
- env {
2445
- name = "APP_NAME"
2446
- value = "${plan.safeProjectName}"
2447
- }
2448
-
2449
- env {
2450
- name = "PROJECT_TYPE"
2451
- value = "${plan.projectType.id}"
2452
- }
2453
- ${projectIdentityEnv}
2454
- env {
2455
- name = "CLOUD_PROVIDER"
2456
- value = "azure"
2457
- }
2458
-
2459
- env {
2460
- name = "AZURE_REGION"
2461
- value = var.location
2462
- }
2463
- ${frontendCorsEnvironment}
2464
-
2465
- env {
2466
- name = "DATABASE_URL"
2467
- secret_name = "database-url"
2468
- }
2469
-
2470
- env {
2471
- name = "REDIS_URL"
2472
- secret_name = "redis-url"
2473
- }
2474
-
2475
- env {
2476
- name = "MESSAGING_TRANSPORT"
2477
- value = "azure-service-bus"
2478
- }
2479
-
2480
- env {
2481
- name = "BLOB_ENDPOINT"
2482
- value = azurerm_storage_account.main.primary_blob_endpoint
2483
- }
2484
- }
2485
- }
2486
-
2487
- ingress {
2488
- external_enabled = true
2489
- target_port = var.backend_target_port
2490
- traffic_weight {
2491
- percentage = 100
2492
- latest_revision = true
2493
- }
2494
- }
2495
-
2496
- depends_on = [azurerm_role_assignment.acr_pull]
2497
- }
2498
- ${frontendContainer}
2499
- resource "azurerm_postgresql_flexible_server" "main" {
2500
- name = "${names.postgres}"
2501
- resource_group_name = azurerm_resource_group.main.name
2502
- location = azurerm_resource_group.main.location
2503
- version = "16"
2504
- administrator_login = "liftoffadmin"
2505
- administrator_password = var.postgres_admin_password
2506
- storage_mb = 32768
2507
- sku_name = "B_Standard_B1ms"
2508
- }
2509
-
2510
- resource "azurerm_postgresql_flexible_server_firewall_rule" "azure_services" {
2511
- count = var.enable_private_networking ? 0 : 1
2512
- name = "AllowAzureServices"
2513
- server_id = azurerm_postgresql_flexible_server.main.id
2514
- start_ip_address = "0.0.0.0"
2515
- end_ip_address = "0.0.0.0"
2516
- }
2517
-
2518
- resource "azurerm_redis_cache" "main" {
2519
- name = "${names.redis}"
2520
- location = azurerm_resource_group.main.location
2521
- resource_group_name = azurerm_resource_group.main.name
2522
- capacity = 0
2523
- family = "C"
2524
- sku_name = "Basic"
2525
- }
2526
-
2527
- resource "azurerm_storage_account" "main" {
2528
- name = "${names.storage}"
2529
- resource_group_name = azurerm_resource_group.main.name
2530
- location = azurerm_resource_group.main.location
2531
- account_tier = "Standard"
2532
- account_replication_type = "LRS"
2533
- }
2534
-
2535
- resource "azurerm_storage_container" "documents" {
2536
- name = "documents"
2537
- storage_account_id = azurerm_storage_account.main.id
2538
- container_access_type = "private"
2539
- }
2540
-
2541
- resource "azurerm_servicebus_namespace" "main" {
2542
- name = "${names.serviceBus}"
2543
- location = azurerm_resource_group.main.location
2544
- resource_group_name = azurerm_resource_group.main.name
2545
- sku = "Standard"
2546
- }
2547
-
2548
- resource "azurerm_servicebus_queue" "events" {
2549
- name = ${queueName}
2550
- namespace_id = azurerm_servicebus_namespace.main.id
2551
- }
2552
- ${functionWorker}
2553
-
2554
- resource "azurerm_communication_service" "main" {
2555
- name = "${names.communication}"
2556
- resource_group_name = azurerm_resource_group.main.name
2557
- data_location = "United States"
2558
- }
2559
-
2560
- resource "azurerm_key_vault" "main" {
2561
- name = "${names.keyVault}"
2562
- location = azurerm_resource_group.main.location
2563
- resource_group_name = azurerm_resource_group.main.name
2564
- tenant_id = data.azurerm_client_config.current.tenant_id
2565
- sku_name = "standard"
2566
- rbac_authorization_enabled = true
2567
- }
2568
-
2569
- data "azurerm_client_config" "current" {}
2570
- `;
2571
- }
2572
- function renderTofuOutputs(plan) {
2573
- const functionOutputs = hasFunctionWorker(plan) ? `
2574
- output "function_app_name" {
2575
- value = azurerm_linux_function_app.worker.name
2576
- }
2577
-
2578
- output "function_worker_queue_name" {
2579
- value = azurerm_servicebus_queue.events.name
2580
- }
2581
- ` : '';
2582
- return `output "backend_url" {
2583
- value = azurerm_container_app.backend.ingress[0].fqdn
2584
- }
2585
-
2586
- ${plan.includeFrontend ? `output "frontend_url" {
2587
- value = azurerm_container_app.frontend.ingress[0].fqdn
2588
- }
2589
- ` : ''}${functionOutputs}output "container_registry" {
2590
- value = azurerm_container_registry.main.login_server
2591
- }
2592
-
2593
- output "container_registry_name" {
2594
- value = azurerm_container_registry.main.name
2595
- }
2596
- `;
2597
- }
2598
- function renderTofuLocalState() {
2599
- return `# Local state is the V1 default for first-use simplicity.
2600
- # Teams can replace this file with backend.remote.example.tf when adopting shared state.
2601
- `;
2602
- }
2603
- function renderTofuRemoteStateExample() {
2604
- return `# Rename to backend.tf and configure values for shared state.
2605
- # terraform {
2606
- # backend "azurerm" {
2607
- # resource_group_name = "rg-opentofu-state"
2608
- # storage_account_name = "stliftoffstate"
2609
- # container_name = "tfstate"
2610
- # key = "mission-control/liftoff.tfstate"
2611
- # }
2612
- # }
2613
- `;
2614
- }
2615
- function renderTofuReadme(plan) {
2616
- const env = selectedEnvironmentId(plan);
2617
- const governanceGate = plan.governanceProfile.id === 'none' ? '' : `
2618
- ## Governance Gate
2619
-
2620
- The commands below are reference material, not authorization to mutate Azure.
2621
- Do not run the plan/apply, ACR build, or image-replacement sequence until the
2622
- separately approved \`application-foundation\` governance phase authorizes the
2623
- exact operation. \`/liftoff-setup\` can evaluate and resume managed phases, but it
2624
- does not imply that every managed phase has an executable production adapter.
2625
- An unavailable production adapter remains a blocker; do not bypass it by running
2626
- these commands directly.
2627
- `;
2628
- const functionSection = plan.workload === 'genai' && hasFunctionWorker(plan) ? `
2629
- ## Azure Functions Worker
2630
-
2631
- This project includes an Azure Functions worker under \`functions/${functionWorkerName(plan)}\`. The OpenTofu configuration attaches one user-assigned identity, grants its principal the Service Bus Data Receiver role, and selects it through \`ServiceBusConnection__clientId\` plus \`ServiceBusConnection__fullyQualifiedNamespace\`. \`function_worker_queue_name\` provisions the queue, configures \`SERVICEBUS_QUEUE_NAME\`, and drives the worker queue output. Function host storage uses the complete key-backed \`AzureWebJobsStorage\` connection setting.
2632
- ` : '';
2633
- return `# Azure OpenTofu
2634
-
2635
- Azure is the complete V1 provider for this Liftoff project.
2636
- ${governanceGate}
2637
-
2638
- ## Bootstrap Infrastructure
2639
-
2640
- The first apply uses a public bootstrap image so Azure Container Apps can start before the new ACR contains application images.
2641
-
2642
- \`\`\`bash
2643
- tofu init
2644
- tofu plan -var-file=environments/${env}.tfvars
2645
- tofu apply -var-file=environments/${env}.tfvars
2646
- \`\`\`
2647
-
2648
- Build the generated backend in ACR, then replace the bootstrap image:
2649
-
2650
- \`\`\`bash
2651
- ACR_NAME="$(tofu output -raw container_registry_name)"
2652
- SOURCE_SHA="$(git rev-parse HEAD)"
2653
- az acr build --registry "$ACR_NAME" --image ${plan.safeProjectName}-backend:"$SOURCE_SHA" ../../..
2654
- ${plan.includeFrontend ? `BACKEND_URL="https://$(tofu output -raw backend_url)"
2655
- az acr build --registry "$ACR_NAME" --image ${plan.safeProjectName}-frontend:"$SOURCE_SHA" --build-arg VITE_API_BASE_URL="$BACKEND_URL" ../../../frontend
2656
- ` : ''}\`\`\`
2657
-
2658
- Persist the deployed images in \`environments/${env}.tfvars\` so future applies do not restore the bootstrap image:
2659
-
2660
- \`\`\`hcl
2661
- backend_image = "<login-server>/${plan.safeProjectName}-backend@sha256:<manifest-digest>"
2662
- backend_target_port = 8000
2663
- ${plan.includeFrontend ? `frontend_image = "<login-server>/${plan.safeProjectName}-frontend@sha256:<manifest-digest>"
2664
- ` : ''}\`\`\`
2665
-
2666
- \`\`\`bash
2667
- tofu apply -var-file=environments/${env}.tfvars
2668
- \`\`\`
2669
-
2670
- Local OpenTofu state is generated by default. Use \`backend.remote.example.tf\` as the starting point for team remote state.
2671
- The default PostgreSQL firewall permits Azure-hosted services. Replace it with private networking before production; set \`enable_private_networking=true\` only when the required VNet, delegated subnet, and private DNS resources are added.
2672
-
2673
- ## Azure Name Suffixes
2674
-
2675
- Each environment tfvars file contains a deterministic 12-character lowercase alphanumeric \`resource_suffix\` used by globally scoped Azure names. If Azure reports that a name is already taken, replace that environment's suffix with another unique value matching \`^[a-z0-9]{12}$\`; \`tofu validate\` rejects invalid overrides before deployment.
2676
- ${functionSection}
2677
- `;
2678
- }
2679
- function renderTofuTfvars(plan, environment) {
2680
- const values = [
2681
- ['environment', JSON.stringify(environment)],
2682
- ['location', JSON.stringify(plan.region.slug)],
2683
- ['resource_suffix', JSON.stringify(stableResourceSuffix(plan, environment))],
2684
- ['backend_image', JSON.stringify(formatContainerImage(supportedStack.containers['container-apps-bootstrap']))],
2685
- ['backend_target_port', '80'],
2686
- ['enable_private_networking', 'false']
2687
- ];
2688
- if (plan.includeFrontend) {
2689
- values.push(['frontend_image', JSON.stringify(formatContainerImage(supportedStack.containers['container-apps-bootstrap']))]);
2690
- }
2691
- if (hasFunctionWorker(plan)) {
2692
- values.push(['function_worker_queue_name', JSON.stringify(DEFAULT_FUNCTION_WORKER_QUEUE_NAME)], ['functions_python_version', JSON.stringify(supportedStack.runtimes.python.releaseLine)]);
2693
- }
2694
- const width = Math.max(...values.map(([key]) => key.length));
2695
- return values.map(([key, value]) => `${key.padEnd(width)} = ${value}`).join('\n');
2696
- }
2697
- function renderOpenSpecConfig(plan) {
2698
- const frontendRule = plan.includeFrontend ? '\n - Keep frontend code under frontend.' : '';
2699
- if (plan.workload === 'standard') {
2700
- const backendRule = plan.apiStack.id === 'python-fastapi'
2701
- ? 'Keep backend API code under backend/apis.'
2702
- : plan.apiStack.id === 'node-fastify'
2703
- ? 'Keep backend API code under backend/src.'
2704
- : 'Keep the Go entrypoint under backend/cmd/api and reusable code under backend/internal.';
2705
- return `schema: spec-driven${renderOpenSpecCopilotConfig(plan)}
2706
-
2707
- context: |
2708
- Project generated by Mission Control Liftoff.
2709
- Project type: Standard application.
2710
- API stack: ${plan.apiStack.label}.
2711
- Database tooling: ${plan.apiStack.databaseTooling}.
2712
- API developer portal: Scalar.
2713
- Infrastructure: OpenTofu.
2714
- Primary cloud: Azure (${plan.region.slug}).
2715
- Local development: Docker Compose.
2716
- Database: PostgreSQL.
2717
- Cache and local messaging: Redis.
2718
- Environments: ${plan.environments.map((environment) => environment.id).join(', ')}.
2719
-
2720
- rules:
2721
- specs:
2722
- - Requirements must describe observable product behavior.
2723
- - Cloud behavior must identify environment differences for generated environments.
2724
- design:
2725
- - Use ${plan.apiStack.framework} for backend APIs.
2726
- - Use ${plan.apiStack.databaseTooling} for database access and migrations.
2727
- - Use OpenTofu for infrastructure changes.${frontendRule}
2728
- - ${backendRule}
2729
- - Keep database artifacts under database.
2730
- tasks:
2731
- - Include local Docker Compose verification.
2732
- - Include OpenTofu validation for generated infrastructure.
2733
- `;
2734
- }
2735
- const pattern = genAiPattern(plan);
2736
- const functionsContext = hasFunctionWorker(plan) ? `
2737
- Azure Functions worker: functions/${functionWorkerName(plan)}.` : '';
2738
- const functionsRule = hasFunctionWorker(plan) ? `
2739
- - Keep Azure Functions trigger adapters under functions/${functionWorkerName(plan)}.
2740
- - Keep reusable GenAI orchestration under backend/orchestration.` : '';
2741
- return `schema: spec-driven${renderOpenSpecCopilotConfig(plan)}
2742
-
2743
- context: |
2744
- Project generated by Mission Control Liftoff.
2745
- GenAI pattern: ${pattern.label}
2746
- Application framework: FastAPI + PydanticAI.
2747
- API developer portal: Scalar.
2748
- Infrastructure: OpenTofu.
2749
- Primary cloud: Azure (${plan.region.slug}).
2750
- Local development: Docker Compose.
2751
- Database: PostgreSQL with Alembic migrations.
2752
- Cache and local messaging: Redis.
2753
- Observability: Langfuse.
2754
- Environments: ${plan.environments.map((environment) => environment.id).join(', ')}.
2755
- ${functionsContext}
2756
-
2757
- rules:
2758
- specs:
2759
- - Requirements must describe observable product behavior.
2760
- - Cloud behavior must identify environment differences for generated environments.
2761
- design:
2762
- - Use PydanticAI for orchestration logic.
2763
- - Use Pydantic settings models for runtime configuration.
2764
- - Use OpenTofu for infrastructure changes.${frontendRule}
2765
- - Keep backend API code under backend/apis.
2766
- ${functionsRule}
2767
- - Keep database artifacts under database.
2768
- tasks:
2769
- - Include local Docker Compose verification.
2770
- - Include OpenTofu validation for generated infrastructure.
2771
- `;
2772
- }
2773
- function renderSeedProposal(plan) {
2774
- const capability = seedCapabilityId(plan);
2775
- if (plan.workload === 'standard') {
2776
- return `## Why
2777
-
2778
- Bootstrap the generated ${plan.apiStack.label} standard application baseline created by Mission Control Liftoff.
2779
-
2780
- ## What Changes
2781
-
2782
- - Establish the approved backend, infrastructure, local development, and governance baseline.
2783
- - Confirm that domain-specific product behavior is deferred to follow-up spec-driven changes.
2784
-
2785
- ## Capabilities
2786
-
2787
- ### New Capabilities
2788
-
2789
- - \`${capability}\`: Generated standard application baseline for this Liftoff project.
2790
-
2791
- ### Modified Capabilities
2792
-
2793
- - None.
2794
-
2795
- ## Impact
2796
-
2797
- - Generated ${plan.apiStack.label} backend, OpenTofu infrastructure, Docker Compose local development, and governance files.
2798
- `;
2799
- }
2800
- const pattern = genAiPattern(plan);
2801
- const functionsChange = hasFunctionWorker(plan) ? '\n- Establish Azure Functions worker trigger adapters for event-driven processing.' : '';
2802
- return `## Why
2803
-
2804
- Bootstrap the generated ${pattern.label} application baseline created by Mission Control Liftoff.
2805
-
2806
- ## What Changes
2807
-
2808
- - Establish the approved backend, infrastructure, local development, and governance baseline.
2809
- - Confirm that domain-specific product behavior is deferred to follow-up spec-driven changes.
2810
- ${functionsChange}
2811
-
2812
- ## Capabilities
2813
-
2814
- ### New Capabilities
2815
-
2816
- - \`${capability}\`: Generated application baseline for this Liftoff project.
2817
-
2818
- ### Modified Capabilities
2819
-
2820
- - None.
2821
-
2822
- ## Impact
2823
-
2824
- - Generated FastAPI/PydanticAI backend, OpenTofu infrastructure, Docker Compose local development, and governance files.
2825
- `;
2826
- }
2827
- function renderSeedDesign(plan) {
2828
- if (plan.workload === 'standard') {
2829
- return `## Context
2830
-
2831
- This standard project was generated with Liftoff using ${plan.apiStack.label}, Azure, OpenTofu, and ${plan.specWorkflow.label}.
2832
-
2833
- ## Goals / Non-Goals
2834
-
2835
- **Goals:**
2836
-
2837
- - Keep the generated baseline aligned to the approved Mission Control stack.
2838
- - Verify only deterministic local scaffold checks before archiving this bootstrap change.
2839
-
2840
- **Non-Goals:**
2841
-
2842
- - Define domain-specific product behavior in the bootstrap change.
2843
- - Deploy infrastructure, start containers, mutate GitHub, or perform cloud plan/apply operations.
2844
-
2845
- ## Decisions
2846
-
2847
- - Use ${plan.apiStack.framework} for backend APIs.
2848
- - Use ${plan.apiStack.databaseTooling} for PostgreSQL integration.
2849
- - Use OpenTofu for Azure infrastructure.
2850
- - Use Docker Compose for local development.
2851
-
2852
- ## Risks / Trade-offs
2853
-
2854
- - Follow-up product changes own real business behavior; this seed only proves the generated local baseline is coherent.
2855
- `;
2856
- }
2857
- const pattern = genAiPattern(plan);
2858
- const functionsDecision = hasFunctionWorker(plan) ? '\n- Keep Azure Functions trigger adapters under functions/' + functionWorkerName(plan) + ' and shared GenAI logic under backend/orchestration.' : '';
2859
- return `## Context
2860
-
2861
- This project was generated with Liftoff using ${pattern.label}, Azure, OpenTofu, and ${plan.specWorkflow.label}.
2862
-
2863
- ## Goals / Non-Goals
2864
-
2865
- **Goals:**
2866
-
2867
- - Keep the generated baseline aligned to the approved Mission Control stack.
2868
- - Verify only deterministic local scaffold checks before archiving this bootstrap change.
2869
-
2870
- **Non-Goals:**
2871
-
2872
- - Define domain-specific product behavior in the bootstrap change.
2873
- - Deploy infrastructure, start containers, mutate GitHub, or perform cloud plan/apply operations.
2874
-
2875
- ## Decisions
2876
-
2877
- - Use FastAPI and PydanticAI for backend APIs and orchestration.
2878
- - Use OpenTofu for Azure infrastructure.
2879
- - Use Docker Compose for local development.
2880
- ${functionsDecision}
2881
-
2882
- ## Risks / Trade-offs
2883
-
2884
- - Follow-up product changes own real business behavior; this seed only proves the generated local baseline is coherent.
2885
- `;
2886
- }
2887
- function seedCapabilityId(plan) {
2888
- if (plan.workload === 'power-apps-code-app') {
2889
- return 'power-apps-code-app-baseline';
2890
- }
2891
- if (plan.workload === 'standard') {
2892
- return `${plan.apiStack.id}-application-baseline`;
2893
- }
2894
- return `${genAiPattern(plan).id}-application-baseline`;
2895
- }
2896
- function seedCapabilityPurpose(plan) {
2897
- if (plan.workload === 'power-apps-code-app') {
2898
- return `Define the generated ${plan.projectName} Power Apps code app baseline that setup verifies and archives before domain-specific implementation or deployment begins.`;
2899
- }
2900
- if (plan.workload === 'standard') {
2901
- return `Define the generated ${plan.apiStack.label} application baseline that setup verifies and archives before domain-specific implementation or deployment begins.`;
2902
- }
2903
- return `Define the generated ${genAiPattern(plan).label} application baseline that setup verifies and archives before domain-specific implementation or deployment begins.`;
2904
- }
2905
- function renderSeedSpec(plan) {
2906
- const capability = seedCapabilityId(plan);
2907
- const purpose = seedCapabilityPurpose(plan);
2908
- if (plan.workload === 'power-apps-code-app') {
2909
- return `## Purpose
2910
-
2911
- ${purpose}
2912
-
2913
- ## ADDED Requirements
2914
-
2915
- ### Requirement: Generated Power Apps code app baseline is locally verifiable
2916
- The bootstrap seed SHALL describe only the generated Power Apps code app scaffold, workflow configuration, and local validation boundary. Domain-specific screens, connector behavior, tenant binding, solution packaging, deployment, credentials, and other product behavior SHALL be deferred to later changes.
2917
-
2918
- #### Scenario: Generated baseline is present
2919
- - **WHEN** the \`bootstrap-${plan.safeProjectName}\` change is generated
2920
- - **THEN** it declares the \`${capability}\` capability
2921
- - **AND** the generated React, Vite, TypeScript, Tailwind CSS, Power Apps SDK, workflow, and governance files are in scope
2922
-
2923
- #### Scenario: Product behavior is deferred
2924
- - **WHEN** the bootstrap change is completed
2925
- - **THEN** no domain-specific connector, tenant, environment, solution, credential, deployment, backend API, Docker, or OpenTofu behavior is introduced
2926
-
2927
- #### Scenario: Local baseline checks pass
2928
- - **WHEN** deterministic setup verifies the seed
2929
- - **THEN** \`liftoff validate\`, the root frontend build, and strict OpenSpec validation have passed
2930
- - **AND** backend, Docker, worker, and OpenTofu checks are recorded as inapplicable without placeholder commands
2931
- `;
2932
- }
2933
- if (plan.workload === 'standard') {
2934
- return `## Purpose
2935
-
2936
- ${purpose}
2937
-
2938
- ## ADDED Requirements
2939
-
2940
- ### Requirement: Generated ${plan.apiStack.label} baseline is locally verifiable
2941
- The bootstrap seed SHALL describe only the generated ${plan.apiStack.label} backend, optional frontend, Docker Compose, OpenTofu, workflow configuration, and governance baseline. Domain-specific product behavior SHALL be deferred to later changes.
2942
-
2943
- #### Scenario: Generated baseline is present
2944
- - **WHEN** the \`bootstrap-${plan.safeProjectName}\` change is generated
2945
- - **THEN** it declares the \`${capability}\` capability
2946
- - **AND** the generated backend, database, Docker Compose, OpenTofu, workflow, and governance files are in scope
2947
-
2948
- #### Scenario: Product behavior is deferred
2949
- - **WHEN** the bootstrap change is completed
2950
- - **THEN** no domain-specific API, data model, UI, deployment, GitHub, or cloud mutation is introduced
2951
-
2952
- #### Scenario: Local baseline checks pass
2953
- - **WHEN** deterministic setup verifies the seed
2954
- - **THEN** \`liftoff validate\`, backend tests, Docker Compose configuration validation, OpenTofu formatting, backend-disabled initialization, OpenTofu validation, and strict OpenSpec validation have passed
2955
- - **AND** frontend checks are run only when a frontend was generated
2956
- `;
2957
- }
2958
- const pattern = genAiPattern(plan);
2959
- const worker = hasFunctionWorker(plan)
2960
- ? 'Azure Functions worker, '
2961
- : '';
2962
- return `## Purpose
2963
-
2964
- ${purpose}
2965
-
2966
- ## ADDED Requirements
2967
-
2968
- ### Requirement: Generated ${pattern.label} baseline is locally verifiable
2969
- The bootstrap seed SHALL describe only the generated FastAPI/PydanticAI backend, ${worker}optional frontend, Docker Compose, OpenTofu, workflow configuration, and governance baseline. Domain-specific product behavior SHALL be deferred to later changes.
2970
-
2971
- #### Scenario: Generated baseline is present
2972
- - **WHEN** the \`bootstrap-${plan.safeProjectName}\` change is generated
2973
- - **THEN** it declares the \`${capability}\` capability
2974
- - **AND** the generated GenAI backend, database, Docker Compose, OpenTofu, workflow, governance${hasFunctionWorker(plan) ? ', and worker' : ''} files are in scope
2975
-
2976
- #### Scenario: Product behavior is deferred
2977
- - **WHEN** the bootstrap change is completed
2978
- - **THEN** no domain-specific prompts, tools, retrieval corpus, agent policy, UI, deployment, GitHub, or cloud mutation is introduced
2979
-
2980
- #### Scenario: Local baseline checks pass
2981
- - **WHEN** deterministic setup verifies the seed
2982
- - **THEN** \`liftoff validate\`, backend tests,${hasFunctionWorker(plan) ? ' worker tests,' : ''} Docker Compose configuration validation, OpenTofu formatting, backend-disabled initialization, OpenTofu validation, and strict OpenSpec validation have passed
2983
- - **AND** frontend checks are run only when a frontend was generated
2984
- `;
2985
- }
2986
- function seedTaskChecks(plan) {
2987
- const checks = [
2988
- {
2989
- id: '2.1',
2990
- label: 'Run Liftoff manifest validation',
2991
- command: { executable: 'liftoff', args: ['validate'] }
2992
- }
2993
- ];
2994
- if (plan.workload === 'power-apps-code-app') {
2995
- checks.push({
2996
- id: '2.2',
2997
- label: 'Record backend tests as inapplicable',
2998
- inapplicable: 'no Liftoff backend is generated for Power Apps code apps'
2999
- });
3000
- }
3001
- else if (plan.workload === 'genai' || plan.apiStack.id === 'python-fastapi') {
3002
- checks.push({
3003
- id: '2.2',
3004
- label: 'Run backend tests',
3005
- command: { executable: 'uv', args: ['run', '--project', 'backend', 'python', '-m', 'pytest', '-q', 'backend/tests'] }
3006
- });
3007
- }
3008
- else if (plan.apiStack.id === 'node-fastify') {
3009
- checks.push({
3010
- id: '2.2',
3011
- label: 'Run backend tests',
3012
- command: { executable: 'npm', args: ['test'] },
3013
- cwd: ['backend']
3014
- });
3015
- }
3016
- else {
3017
- checks.push({
3018
- id: '2.2',
3019
- label: 'Run backend tests',
3020
- command: { executable: 'go', args: ['test', './...'] },
3021
- cwd: ['backend']
3022
- });
3023
- }
3024
- if (plan.workload === 'genai' && hasFunctionWorker(plan)) {
3025
- checks.push({
3026
- id: '2.3',
3027
- label: 'Run generated worker tests',
3028
- command: { executable: 'uv', args: ['run', '--project', '../../backend', '--directory', '.', 'python', '-m', 'pytest', '-q'] },
3029
- cwd: ['functions', functionWorkerName(plan)]
3030
- });
3031
- }
3032
- else {
3033
- checks.push({
3034
- id: '2.3',
3035
- label: 'Record worker tests as inapplicable',
3036
- inapplicable: 'no generated worker boundary is present'
3037
- });
3038
- }
3039
- if (plan.workload === 'power-apps-code-app') {
3040
- checks.push({
3041
- id: '2.4',
3042
- label: 'Run frontend build',
3043
- command: { executable: 'npm', args: ['run', 'build'] }
3044
- });
3045
- }
3046
- else if (plan.includeFrontend) {
3047
- checks.push({
3048
- id: '2.4',
3049
- label: 'Run frontend build',
3050
- command: { executable: 'npm', args: ['run', 'build'] },
3051
- cwd: ['frontend']
3052
- });
3053
- }
3054
- else {
3055
- checks.push({
3056
- id: '2.4',
3057
- label: 'Record frontend build as inapplicable',
3058
- inapplicable: 'no generated frontend is present'
3059
- });
3060
- }
3061
- if (plan.workload === 'power-apps-code-app') {
3062
- checks.push({
3063
- id: '2.5',
3064
- label: 'Record Docker Compose validation as inapplicable',
3065
- inapplicable: 'no generated Docker Compose boundary is present'
3066
- });
3067
- checks.push({
3068
- id: '2.6',
3069
- label: 'Record OpenTofu formatting as inapplicable',
3070
- inapplicable: 'no generated OpenTofu boundary is present'
3071
- });
3072
- checks.push({
3073
- id: '2.7',
3074
- label: 'Record OpenTofu initialization as inapplicable',
3075
- inapplicable: 'no generated OpenTofu boundary is present'
3076
- });
3077
- checks.push({
3078
- id: '2.8',
3079
- label: 'Record OpenTofu validation as inapplicable',
3080
- inapplicable: 'no generated OpenTofu boundary is present'
3081
- });
3082
- }
3083
- else {
3084
- checks.push({
3085
- id: '2.5',
3086
- label: 'Validate Docker Compose configuration without startup',
3087
- command: { executable: 'docker', args: ['compose', 'config', '-q'] }
3088
- });
3089
- checks.push({
3090
- id: '2.6',
3091
- label: 'Check OpenTofu formatting',
3092
- command: { executable: 'tofu', args: ['fmt', '-check', '-recursive'] },
3093
- cwd: ['infrastructure', 'opentofu', 'azure']
3094
- });
3095
- checks.push({
3096
- id: '2.7',
3097
- label: 'Initialize OpenTofu without a remote backend',
3098
- command: { executable: 'tofu', args: ['init', '-backend=false'] },
3099
- cwd: ['infrastructure', 'opentofu', 'azure']
3100
- });
3101
- checks.push({
3102
- id: '2.8',
3103
- label: 'Validate OpenTofu configuration without plan or apply',
3104
- command: { executable: 'tofu', args: ['validate'] },
3105
- cwd: ['infrastructure', 'opentofu', 'azure']
3106
- });
3107
- }
3108
- checks.push({
3109
- id: '2.9',
3110
- label: 'Run strict OpenSpec validation',
3111
- command: { executable: 'openspec', args: ['validate', `bootstrap-${plan.safeProjectName}`, '--strict'] }
3112
- });
3113
- return checks;
3114
- }
3115
- function renderSeedTaskCheck(check) {
3116
- if (!check.command) {
3117
- return `- [ ] ${check.id} ${check.label}: inapplicable because ${check.inapplicable}.`;
3118
- }
3119
- const cwd = check.cwd && check.cwd.length > 0 ? ` from \`${check.cwd.join('/')}\`` : '';
3120
- return `- [ ] ${check.id} ${check.label}${cwd}: \`${formatCommand(check.command)}\`.`;
3121
- }
3122
- function renderSeedTasks(plan) {
3123
- return `## 1. Bootstrap Scope
3124
-
3125
- - [ ] 1.1 Confirm generated baseline files, \`liftoff.manifest.json\`, OpenSpec configuration, and the \`${seedCapabilityId(plan)}\` delta spec are present.
3126
- - [ ] 1.2 Confirm domain-specific product behavior is deferred to follow-up OpenSpec changes; do not replace generated placeholders in this bootstrap change.
3127
-
3128
- ## 2. Local Baseline Checks
3129
-
3130
- ${seedTaskChecks(plan).map(renderSeedTaskCheck).join('\n')}
3131
-
3132
- ## 3. Completion
3133
-
3134
- - [ ] 3.1 After every applicable local check succeeds and every absent component is recorded inapplicable, archive with \`openspec archive bootstrap-${plan.safeProjectName} --yes\`, then require \`openspec validate --all --strict\` to pass. OpenSpec archive updates the main specs as part of archive; do not pass \`--skip-specs\`.
3135
-
3136
- No task in this bootstrap seed starts containers, runs cloud plan/apply, mutates GitHub, or implements domain-specific product behavior.
3137
- `;
3138
- }
3139
- function renderSpecKitConstitution(plan) {
3140
- if (plan.workload === 'standard') {
3141
- const backendLayout = plan.apiStack.id === 'python-fastapi'
3142
- ? 'backend/apis'
3143
- : plan.apiStack.id === 'node-fastify' ? 'backend/src' : 'backend/cmd/api and backend/internal';
3144
- return `# Mission Control Liftoff Constitution
3145
-
3146
- ## Principle 1: Approved Application Stack
3147
- Generated backend services MUST use ${plan.apiStack.framework}, ${plan.apiStack.databaseTooling}, and Scalar for API documentation.
3148
-
3149
- ## Principle 2: Standard Project Layout
3150
- Backend APIs live under ${backendLayout}. Database artifacts live under database.${plan.includeFrontend ? ' Frontend code lives under frontend.' : ''}
3151
-
3152
- ## Principle 3: Infrastructure As Code
3153
- Cloud infrastructure MUST be defined with OpenTofu. Azure is the supported V1 provider.
3154
-
3155
- ## Principle 4: Local Development Parity
3156
- Projects MUST include Docker Compose for local development with PostgreSQL, Redis, local blob storage, and local messaging behavior.
3157
-
3158
- ## Principle 5: Observability And Operations
3159
- Services MUST use structured logging and environment-specific configuration for ${plan.environments.map((environment) => environment.id).join(', ')}.
3160
- `;
3161
- }
3162
- const functionsLayout = plan.workload === 'genai' && hasFunctionWorker(plan)
3163
- ? ` Azure Functions trigger adapters live under functions/${functionWorkerName(plan)} and call shared orchestration from backend/orchestration.`
3164
- : '';
3165
- return `# Mission Control Liftoff Constitution
3166
-
3167
- ## Principle 1: Approved Application Stack
3168
- Generated backend services MUST use FastAPI, PydanticAI, Pydantic configuration models, and Scalar for API documentation.
3169
-
3170
- ## Principle 2: Standard Project Layout
3171
- Backend APIs live under backend/apis. Database artifacts live under database.${plan.includeFrontend ? ' Frontend code lives under frontend.' : ''}${functionsLayout}
3172
-
3173
- ## Principle 3: Infrastructure As Code
3174
- Cloud infrastructure MUST be defined with OpenTofu. Azure is the supported V1 provider.
3175
-
3176
- ## Principle 4: Local Development Parity
3177
- Projects MUST include Docker Compose for local development with PostgreSQL, Redis, local blob storage, and local messaging behavior.
3178
-
3179
- ## Principle 5: Observability And Operations
3180
- LLM workflows MUST include Langfuse tracing hooks and environment-specific configuration for ${plan.environments.map((environment) => environment.id).join(', ')}.
3181
- `;
3182
- }
3183
- function renderSpecKitSpecTemplate() {
3184
- return `# Feature Specification
3185
-
3186
- ## User Scenarios
3187
-
3188
- ## Requirements
3189
-
3190
- ## Success Criteria
3191
- `;
3192
- }
3193
- function renderSpecKitPlanTemplate() {
3194
- return `# Implementation Plan
3195
-
3196
- ## Technical Context
3197
-
3198
- ## Constitution Check
3199
-
3200
- ## Tasks
3201
- `;
3202
- }
3203
- function renderFrontendPackage(plan) {
3204
- return renderNpmPackage('frontend', `${plan.safeProjectName}-frontend`);
3205
- }
3206
- function renderFrontendIndex(plan) {
3207
- return `<div id="app"></div><script type="module" src="/src/main.ts"></script><title>${escapeHtml(plan.projectName)}</title>`;
3208
- }
3209
- function renderFrontendMain() {
3210
- return `import { createApp } from 'vue';
3211
- import App from './App.vue';
3212
- import './styles.css';
3213
-
3214
- createApp(App).mount('#app');
3215
- `;
3216
- }
3217
- function renderFrontendApp(plan) {
3218
- const descriptor = plan.workload === 'genai'
3219
- ? genAiPattern(plan).id === 'generic'
3220
- ? genAiPattern(plan).label
3221
- : `${genAiPattern(plan).label} starter`
3222
- : `${plan.apiStack.label} starter`;
3223
- const apiContract = plan.workload === 'standard'
3224
- ? { route: '/api', method: 'GET', bodyField: '', queryParameter: '', requiresInput: false }
3225
- : genAiPattern(plan).id === 'rag'
3226
- ? { route: `${genAiPattern(plan).routePrefix}/query`, method: 'POST', bodyField: 'question', queryParameter: '', requiresInput: true }
3227
- : genAiPattern(plan).id === 'streaming'
3228
- ? { route: genAiPattern(plan).routePrefix, method: 'GET', bodyField: '', queryParameter: 'prompt', requiresInput: true }
3229
- : { route: `${genAiPattern(plan).routePrefix}/run`, method: 'POST', bodyField: 'input', queryParameter: '', requiresInput: true };
3230
- return `<script setup lang="ts">
3231
- import { ref } from 'vue';
3232
-
3233
- const title = ${scriptSourceString(plan.projectName)};
3234
- const starter = ${scriptSourceString(plan.frontendStarter)};
3235
- const descriptor = ${scriptSourceString(descriptor)};
3236
- const apiBaseUrl = (import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000').replace(/\\/+$/, '');
3237
- const route = ${scriptSourceString(apiContract.route)};
3238
- const method = ${scriptSourceString(apiContract.method)};
3239
- const bodyField = ${scriptSourceString(apiContract.bodyField)};
3240
- const queryParameter = ${scriptSourceString(apiContract.queryParameter)};
3241
- const requiresInput = ${apiContract.requiresInput};
3242
-
3243
- const input = ref('');
3244
- const loading = ref(false);
3245
- const result = ref('');
3246
- const errorMessage = ref('');
3247
-
3248
- async function submit(): Promise<void> {
3249
- const value = input.value.trim();
3250
- if (requiresInput && !value) {
3251
- errorMessage.value = 'Enter a value before running the starter.';
3252
- return;
3253
- }
3254
-
3255
- loading.value = true;
3256
- result.value = '';
3257
- errorMessage.value = '';
3258
- try {
3259
- const query = queryParameter
3260
- ? '?' + queryParameter + '=' + encodeURIComponent(value)
3261
- : '';
3262
- const request: RequestInit = { method };
3263
- if (method === 'POST') {
3264
- request.headers = { 'Content-Type': 'application/json' };
3265
- request.body = JSON.stringify({ [bodyField]: value });
3266
- }
3267
- const response = await fetch(apiBaseUrl + route + query, request);
3268
- const responseText = await response.text();
3269
- if (!response.ok) {
3270
- throw new Error(
3271
- 'Backend request failed (' + response.status + '): ' +
3272
- (responseText || response.statusText)
3273
- );
3274
- }
3275
- if ((response.headers.get('content-type') || '').includes('application/json')) {
3276
- result.value = JSON.stringify(JSON.parse(responseText), null, 2);
3277
- } else {
3278
- result.value = responseText;
3279
- }
3280
- } catch (error) {
3281
- errorMessage.value = error instanceof Error
3282
- ? error.message
3283
- : 'The backend request failed unexpectedly.';
3284
- } finally {
3285
- loading.value = false;
3286
- }
3287
- }
3288
- </script>
3289
-
3290
- <template>
3291
- <main class="min-h-screen bg-slate-50 text-slate-950">
3292
- <section class="mx-auto flex min-h-screen w-full max-w-5xl flex-col gap-6 px-6 py-10">
3293
- <header>
3294
- <p class="text-sm font-semibold uppercase tracking-wide text-emerald-700">Mission Control Liftoff</p>
3295
- <h1 class="mt-2 text-3xl font-bold">{{ title }}</h1>
3296
- <p class="mt-2 text-slate-600">{{ descriptor }}</p>
3297
- </header>
3298
- <section class="rounded-lg border border-slate-200 bg-white p-6 shadow-sm">
3299
- <h2 class="text-xl font-semibold">{{ starter }}</h2>
3300
- <p class="mt-2 text-sm text-slate-500">API: {{ apiBaseUrl }}{{ route }}</p>
3301
- <textarea
3302
- v-if="requiresInput"
3303
- v-model="input"
3304
- class="mt-4 min-h-40 w-full rounded-md border border-slate-300 p-3"
3305
- :disabled="loading"
3306
- placeholder="Enter input for the generated backend."
3307
- />
3308
- <button
3309
- class="mt-4 rounded-md bg-emerald-700 px-4 py-2 font-semibold text-white disabled:cursor-not-allowed disabled:opacity-60"
3310
- :disabled="loading"
3311
- type="button"
3312
- @click="submit"
3313
- >
3314
- {{ loading ? 'Running...' : 'Run' }}
3315
- </button>
3316
- <p v-if="errorMessage" class="mt-4 rounded-md bg-red-50 p-3 text-red-800" role="alert">
3317
- {{ errorMessage }}
3318
- </p>
3319
- <pre v-if="result" class="mt-4 overflow-auto rounded-md bg-slate-950 p-4 text-sm text-white" aria-live="polite">{{ result }}</pre>
3320
- </section>
3321
- </section>
3322
- </main>
3323
- </template>
3324
- `;
3325
- }
3326
- function renderFrontendStyles() {
3327
- return `@import "tailwindcss";
3328
- `;
3329
- }
3330
- function renderFrontendViteConfig() {
3331
- return `import { defineConfig } from 'vite';
3332
- import tailwindcss from '@tailwindcss/vite';
3333
- import vue from '@vitejs/plugin-vue';
3334
-
3335
- export default defineConfig({ plugins: [vue(), tailwindcss()] });
3336
- `;
3337
- }
3338
- function renderFrontendTailwindConfig() {
3339
- return `import type { Config } from 'tailwindcss';
3340
-
3341
- export default {
3342
- content: ['./index.html', './src/**/*.{vue,ts}'],
3343
- theme: { extend: {} },
3344
- plugins: []
3345
- } satisfies Config;
3346
- `;
3347
- }
3348
- function renderFrontendDockerfile() {
3349
- return `FROM ${formatContainerImage(supportedStack.containers['node-runtime'])} AS build
3350
- WORKDIR /app
3351
- ARG NPM_CONFIG_REGISTRY=https://registry.npmjs.org
3352
- COPY package.json package-lock.json* ./
3353
- RUN npm ci --ignore-scripts --no-audit --no-fund --registry="$NPM_CONFIG_REGISTRY"
3354
- COPY . .
3355
- ARG VITE_API_BASE_URL=http://localhost:8000
3356
- ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
3357
- RUN npm run build
3358
-
3359
- FROM ${formatContainerImage(supportedStack.containers['nginx-runtime'])}
3360
- COPY --from=build /app/dist /usr/share/nginx/html
3361
- `;
3362
- }
3363
- function ensureTrailingNewline(content) {
3364
- return content.endsWith('\n') ? content : `${content}\n`;
3365
- }
3366
157
  //# sourceMappingURL=templates.js.map