@msn-control/liftoff 0.1.1

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.
@@ -0,0 +1,1148 @@
1
+ const pyModule = (value) => value.replace(/-/g, '_');
2
+ const titleCase = (value) => value.replace(/(^|[-_\s])([a-z])/g, (_match, prefix, letter) => `${prefix ? ' ' : ''}${letter.toUpperCase()}`).trim();
3
+ export function buildArtifacts(plan) {
4
+ const artifacts = [];
5
+ const add = (logicalName, category, pathParts, content) => {
6
+ artifacts.push({ logicalName, category, pathParts, content: ensureTrailingNewline(content) });
7
+ };
8
+ addBaseArtifacts(add, plan);
9
+ addBackendArtifacts(add, plan);
10
+ addDatabaseArtifacts(add, plan);
11
+ addPatternArtifacts(add, plan);
12
+ addEnvironmentArtifacts(add, plan);
13
+ addDockerArtifacts(add, plan);
14
+ addInfrastructureArtifacts(add, plan);
15
+ addGovernanceArtifacts(add, plan);
16
+ if (plan.includeFrontend) {
17
+ addFrontendArtifacts(add, plan);
18
+ }
19
+ const manifest = buildManifest(plan, artifacts);
20
+ artifacts.push({
21
+ logicalName: 'manifest',
22
+ category: 'manifest',
23
+ pathParts: ['liftoff.manifest.json'],
24
+ content: `${JSON.stringify(manifest, null, 2)}\n`
25
+ });
26
+ return artifacts;
27
+ }
28
+ export function buildManifest(plan, artifacts) {
29
+ return {
30
+ artifactVersion: 1,
31
+ generatedBy: 'Mission Control Liftoff',
32
+ project: {
33
+ name: plan.projectName,
34
+ pattern: plan.pattern.id,
35
+ cloud: plan.provider.id,
36
+ region: plan.region.slug,
37
+ frontend: plan.includeFrontend,
38
+ specWorkflow: plan.specWorkflow.id,
39
+ environments: plan.environments.map((environment) => environment.id)
40
+ },
41
+ artifacts: artifacts.map((artifact) => ({
42
+ logicalName: artifact.logicalName,
43
+ category: artifact.category,
44
+ pathParts: artifact.pathParts
45
+ }))
46
+ };
47
+ }
48
+ function addBaseArtifacts(add, plan) {
49
+ add('root-readme', 'documentation', ['README.md'], renderRootReadme(plan));
50
+ add('root-gitignore', 'project', ['.gitignore'], renderGeneratedGitignore());
51
+ add('liftoff-config', 'project', ['liftoff.config.json'], JSON.stringify({
52
+ projectName: plan.projectName,
53
+ pattern: plan.pattern.id,
54
+ cloud: plan.provider.id,
55
+ region: plan.region.slug,
56
+ includeFrontend: plan.includeFrontend,
57
+ environments: plan.environments.map((environment) => environment.id),
58
+ specWorkflow: plan.specWorkflow.id
59
+ }, null, 2));
60
+ add('env-example', 'configuration', ['.env.example'], renderEnvExample(plan));
61
+ add('backend-dockerfile', 'runtime', ['Dockerfile'], renderBackendDockerfile());
62
+ }
63
+ function addBackendArtifacts(add, plan) {
64
+ const routeModule = pyModule(plan.pattern.id);
65
+ add('backend-pyproject', 'backend', ['backend', 'pyproject.toml'], renderBackendPyproject(plan));
66
+ add('backend-package', 'backend', ['backend', '__init__.py'], '');
67
+ add('backend-api-package', 'backend', ['backend', 'apis', '__init__.py'], '');
68
+ add('backend-main', 'backend', ['backend', 'apis', 'main.py'], renderFastApiMain(plan, routeModule));
69
+ add('backend-health-routes', 'backend', ['backend', 'apis', 'routes', 'health.py'], renderHealthRoutes());
70
+ add('backend-pattern-routes', 'backend', ['backend', 'apis', 'routes', `${routeModule}.py`], renderPatternRoutes(plan));
71
+ add('backend-routes-package', 'backend', ['backend', 'apis', 'routes', '__init__.py'], '');
72
+ add('backend-auth-dependency', 'backend', ['backend', 'apis', 'dependencies', 'auth.py'], renderAuthDependency());
73
+ add('backend-config-package', 'backend', ['backend', 'config', '__init__.py'], '');
74
+ add('backend-settings', 'backend', ['backend', 'config', 'settings.py'], renderSettings(plan));
75
+ add('backend-orchestration-package', 'backend', ['backend', 'orchestration', '__init__.py'], '');
76
+ add('backend-model-config', 'backend', ['backend', 'orchestration', 'model_config.py'], renderModelConfig(plan));
77
+ add('backend-messaging-tool', 'backend', ['backend', 'orchestration', 'tools', 'messaging.py'], renderMessagingBoundary());
78
+ add('backend-tools-package', 'backend', ['backend', 'orchestration', 'tools', '__init__.py'], '');
79
+ add('backend-observability', 'backend', ['backend', 'observability', 'tracing.py'], renderTracing());
80
+ add('backend-observability-package', 'backend', ['backend', 'observability', '__init__.py'], '');
81
+ add('backend-test-health', 'backend-test', ['backend', 'tests', 'test_health.py'], renderBackendHealthTest());
82
+ }
83
+ function addDatabaseArtifacts(add, plan) {
84
+ add('database-alembic-ini', 'database', ['database', 'alembic.ini'], renderAlembicIni());
85
+ add('database-alembic-env', 'database', ['database', 'migrations', 'env.py'], renderAlembicEnv());
86
+ add('database-initial-migration', 'database', ['database', 'migrations', 'versions', '0001_initial.py'], renderInitialMigration(plan));
87
+ add('database-schema', 'database', ['database', 'models', 'schema.sql'], renderDatabaseSchema(plan));
88
+ }
89
+ function addPatternArtifacts(add, plan) {
90
+ const routeModule = pyModule(plan.pattern.id);
91
+ add('pattern-agent', 'pattern', ['backend', 'orchestration', 'agents', `${routeModule}_agent.py`], renderPatternAgent(plan));
92
+ add('pattern-prompt', 'pattern', ['backend', 'orchestration', 'prompts', `${plan.pattern.id}.md`], renderPromptTemplate(plan));
93
+ add('pattern-agent-package', 'pattern', ['backend', 'orchestration', 'agents', '__init__.py'], '');
94
+ add('pattern-prompt-readme', 'pattern', ['backend', 'orchestration', 'prompts', 'README.md'], renderPromptReadme());
95
+ if (plan.pattern.id === 'rag') {
96
+ add('rag-vector-store', 'pattern', ['backend', 'orchestration', 'retrieval', 'vector_store.py'], renderVectorStore());
97
+ add('rag-retrieval-package', 'pattern', ['backend', 'orchestration', 'retrieval', '__init__.py'], '');
98
+ }
99
+ if (plan.pattern.worker) {
100
+ add('pattern-worker', 'pattern', ['backend', 'workers', `${routeModule}_worker.py`], renderPatternWorker(plan));
101
+ add('backend-workers-package', 'pattern', ['backend', 'workers', '__init__.py'], '');
102
+ }
103
+ if (plan.pattern.id === 'fine-tuned') {
104
+ add('fine-tuned-eval-dataset', 'pattern', ['backend', 'evaluation', 'datasets', 'sample.jsonl'], '{"input":"Example request","expected":"Expected response placeholder"}');
105
+ }
106
+ }
107
+ function addEnvironmentArtifacts(add, plan) {
108
+ for (const environment of plan.environments) {
109
+ add(`environment-${environment.id}-backend`, 'environment', ['environments', environment.id, 'backend.env'], renderBackendEnv(plan, environment.id));
110
+ }
111
+ }
112
+ function addDockerArtifacts(add, plan) {
113
+ add('docker-compose', 'local-development', ['docker-compose.yml'], renderDockerCompose(plan));
114
+ }
115
+ function addInfrastructureArtifacts(add, plan) {
116
+ const base = ['infrastructure', 'opentofu', 'azure'];
117
+ add('opentofu-versions', 'infrastructure', [...base, 'versions.tf'], renderTofuVersions());
118
+ add('opentofu-providers', 'infrastructure', [...base, 'providers.tf'], renderTofuProviders());
119
+ add('opentofu-variables', 'infrastructure', [...base, 'variables.tf'], renderTofuVariables(plan));
120
+ add('opentofu-main', 'infrastructure', [...base, 'main.tf'], renderTofuMain(plan));
121
+ add('opentofu-outputs', 'infrastructure', [...base, 'outputs.tf'], renderTofuOutputs(plan));
122
+ add('opentofu-local-state', 'infrastructure', [...base, 'backend.local.tf'], renderTofuLocalState());
123
+ add('opentofu-remote-state-example', 'infrastructure', [...base, 'backend.remote.example.tf'], renderTofuRemoteStateExample());
124
+ add('opentofu-readme', 'infrastructure', [...base, 'README.md'], renderTofuReadme(plan));
125
+ for (const environment of plan.environments) {
126
+ add(`opentofu-${environment.id}-tfvars`, 'infrastructure', [...base, 'environments', `${environment.id}.tfvars`], renderTofuTfvars(plan, environment.id));
127
+ }
128
+ }
129
+ function addGovernanceArtifacts(add, plan) {
130
+ if (plan.specWorkflow.id === 'openspec') {
131
+ const changeName = `bootstrap-${plan.safeProjectName}`;
132
+ add('openspec-config', 'governance', ['openspec', 'config.yaml'], renderOpenSpecConfig(plan));
133
+ add('openspec-seed-change-metadata', 'governance', ['openspec', 'changes', changeName, '.openspec.yaml'], 'schema: spec-driven');
134
+ add('openspec-seed-proposal', 'governance', ['openspec', 'changes', changeName, 'proposal.md'], renderSeedProposal(plan));
135
+ add('openspec-seed-design', 'governance', ['openspec', 'changes', changeName, 'design.md'], renderSeedDesign(plan));
136
+ add('openspec-seed-tasks', 'governance', ['openspec', 'changes', changeName, 'tasks.md'], renderSeedTasks());
137
+ add('openspec-spec-placeholder', 'governance', ['openspec', 'specs', '.gitkeep'], '');
138
+ }
139
+ else {
140
+ add('spec-kit-constitution', 'governance', ['.specify', 'memory', 'constitution.md'], renderSpecKitConstitution(plan));
141
+ add('spec-kit-spec-template', 'governance', ['.specify', 'templates', 'spec-template.md'], renderSpecKitSpecTemplate());
142
+ add('spec-kit-plan-template', 'governance', ['.specify', 'templates', 'plan-template.md'], renderSpecKitPlanTemplate());
143
+ add('specs-placeholder', 'governance', ['specs', '.gitkeep'], '');
144
+ }
145
+ }
146
+ function addFrontendArtifacts(add, plan) {
147
+ add('frontend-package', 'frontend', ['frontend', 'package.json'], renderFrontendPackage(plan));
148
+ add('frontend-index', 'frontend', ['frontend', 'index.html'], renderFrontendIndex(plan));
149
+ add('frontend-main', 'frontend', ['frontend', 'src', 'main.ts'], renderFrontendMain());
150
+ add('frontend-app', 'frontend', ['frontend', 'src', 'App.vue'], renderFrontendApp(plan));
151
+ add('frontend-styles', 'frontend', ['frontend', 'src', 'styles.css'], renderFrontendStyles());
152
+ add('frontend-vite-config', 'frontend', ['frontend', 'vite.config.ts'], renderFrontendViteConfig());
153
+ add('frontend-tailwind-config', 'frontend', ['frontend', 'tailwind.config.ts'], renderFrontendTailwindConfig());
154
+ add('frontend-dockerfile', 'frontend', ['frontend', 'Dockerfile'], renderFrontendDockerfile());
155
+ }
156
+ function renderRootReadme(plan) {
157
+ return `# ${plan.projectName}
158
+
159
+ Generated by Mission Control Liftoff.
160
+
161
+ ## Stack
162
+
163
+ - Backend: FastAPI, PydanticAI, Pydantic settings, Scalar
164
+ - Pattern: ${plan.pattern.label}
165
+ - Cloud: ${plan.provider.label} (${plan.region.slug})
166
+ - Infrastructure: OpenTofu
167
+ - Database: PostgreSQL with Alembic migrations${plan.pattern.id === 'rag' ? ' and pgvector retrieval' : ''}
168
+ - Cache and local messaging: Redis
169
+ - Observability: Langfuse
170
+ - Local development: Docker Compose
171
+ ${plan.includeFrontend ? '- Frontend: Vue 3 with Tailwind\n' : ''}
172
+ ## Local Development
173
+
174
+ \`\`\`bash
175
+ docker compose up --build
176
+ docker compose --profile observability up --build
177
+ \`\`\`
178
+
179
+ The backend API is available on port 8000. Scalar is exposed at \`/scalar\`.
180
+
181
+ ## Infrastructure
182
+
183
+ \`\`\`bash
184
+ cd infrastructure/opentofu/azure
185
+ tofu init
186
+ tofu plan -var-file=environments/dev.tfvars
187
+ tofu apply -var-file=environments/dev.tfvars
188
+ \`\`\`
189
+
190
+ ## Spec-Driven Workflow
191
+
192
+ Selected workflow: ${plan.specWorkflow.label}.
193
+ `;
194
+ }
195
+ function renderGeneratedGitignore() {
196
+ return `.venv/
197
+ __pycache__/
198
+ .pytest_cache/
199
+ node_modules/
200
+ dist/
201
+ .env
202
+ *.tfstate
203
+ *.tfstate.*
204
+ .terraform/
205
+ `;
206
+ }
207
+ function renderEnvExample(plan) {
208
+ return `APP_ENV=dev
209
+ APP_NAME=${plan.safeProjectName}
210
+ GENAI_PATTERN=${plan.pattern.id}
211
+ CLOUD_PROVIDER=${plan.provider.id}
212
+ AZURE_REGION=${plan.region.slug}
213
+ DATABASE_URL=postgresql+asyncpg://postgres:postgres@postgres:5432/${plan.safeProjectName.replace(/-/g, '_')}
214
+ REDIS_URL=redis://redis:6379/0
215
+ MESSAGING_TRANSPORT=redis-streams
216
+ BLOB_ENDPOINT=http://azurite:10000/devstoreaccount1
217
+ LANGFUSE_HOST=http://langfuse:3000
218
+ `;
219
+ }
220
+ function renderBackendDockerfile() {
221
+ return `FROM python:3.12-slim
222
+
223
+ WORKDIR /app
224
+
225
+ ENV PYTHONDONTWRITEBYTECODE=1
226
+ ENV PYTHONUNBUFFERED=1
227
+ ENV PYTHONPATH=/app
228
+
229
+ COPY backend/pyproject.toml /app/backend/pyproject.toml
230
+ RUN pip install --no-cache-dir /app/backend
231
+
232
+ COPY backend /app/backend
233
+ COPY database /app/database
234
+
235
+ EXPOSE 8000
236
+ CMD ["uvicorn", "backend.apis.main:app", "--host", "0.0.0.0", "--port", "8000"]
237
+ `;
238
+ }
239
+ function renderBackendPyproject(plan) {
240
+ return `[project]
241
+ name = "${plan.safeProjectName}-backend"
242
+ version = "0.1.0"
243
+ requires-python = ">=3.12"
244
+ dependencies = [
245
+ "fastapi>=0.111",
246
+ "uvicorn[standard]>=0.30",
247
+ "pydantic>=2.7",
248
+ "pydantic-settings>=2.3",
249
+ "pydantic-ai>=0.0.13",
250
+ "scalar-fastapi>=1.0",
251
+ "sqlalchemy[asyncio]>=2.0",
252
+ "asyncpg>=0.29",
253
+ "alembic>=1.13",
254
+ "redis>=5.0",
255
+ "langfuse>=2.39",
256
+ "azure-servicebus>=7.12",
257
+ "azure-storage-blob>=12.20",
258
+ "azure-communication-email>=1.0"
259
+ ]
260
+
261
+ [project.optional-dependencies]
262
+ test = ["pytest>=8.2", "httpx>=0.27"]
263
+
264
+ [build-system]
265
+ requires = ["setuptools>=70"]
266
+ build-backend = "setuptools.build_meta"
267
+
268
+ [tool.pytest.ini_options]
269
+ pythonpath = [".."]
270
+ testpaths = ["tests"]
271
+ `;
272
+ }
273
+ function renderFastApiMain(plan, routeModule) {
274
+ return `from fastapi import FastAPI
275
+
276
+ try:
277
+ from scalar_fastapi import get_scalar_api_reference
278
+ except ImportError: # pragma: no cover - dependency is present in generated runtime
279
+ get_scalar_api_reference = None
280
+
281
+ from backend.apis.routes import health, ${routeModule}
282
+ from backend.config.settings import get_settings
283
+
284
+
285
+ settings = get_settings()
286
+ app = FastAPI(title=settings.app_name, version="0.1.0")
287
+
288
+ app.include_router(health.router)
289
+ app.include_router(${routeModule}.router)
290
+
291
+
292
+ @app.get("/scalar", include_in_schema=False)
293
+ def scalar_reference():
294
+ if get_scalar_api_reference is None:
295
+ return {"message": "Install scalar-fastapi to enable the Scalar developer portal."}
296
+ return get_scalar_api_reference(openapi_url=app.openapi_url, title=f"{app.title} API")
297
+ `;
298
+ }
299
+ function renderHealthRoutes() {
300
+ return `from fastapi import APIRouter
301
+
302
+ router = APIRouter(tags=["operations"])
303
+
304
+
305
+ @router.get("/health")
306
+ def health():
307
+ return {"status": "ok"}
308
+
309
+
310
+ @router.get("/ready")
311
+ def ready():
312
+ return {"status": "ready"}
313
+ `;
314
+ }
315
+ function renderPatternRoutes(plan) {
316
+ const moduleName = pyModule(plan.pattern.id);
317
+ const agentName = `${moduleName}_agent`;
318
+ const prefix = plan.pattern.routePrefix;
319
+ if (plan.pattern.id === 'streaming') {
320
+ return `from fastapi import APIRouter
321
+ from fastapi.responses import StreamingResponse
322
+
323
+ from backend.orchestration.agents.${agentName} import stream_response
324
+
325
+ router = APIRouter(prefix="${prefix}", tags=["${plan.pattern.id}"])
326
+
327
+
328
+ @router.get("")
329
+ def stream(prompt: str):
330
+ return StreamingResponse(stream_response(prompt), media_type="text/event-stream")
331
+ `;
332
+ }
333
+ if (plan.pattern.id === 'rag') {
334
+ return `from fastapi import APIRouter
335
+ from pydantic import BaseModel
336
+
337
+ from backend.orchestration.agents.${agentName} import answer_question, enqueue_ingestion
338
+
339
+ router = APIRouter(prefix="${prefix}", tags=["rag"])
340
+
341
+
342
+ class QueryRequest(BaseModel):
343
+ question: str
344
+
345
+
346
+ class IngestionRequest(BaseModel):
347
+ source_uri: str
348
+
349
+
350
+ @router.post("/query")
351
+ async def query(request: QueryRequest):
352
+ return await answer_question(request.question)
353
+
354
+
355
+ @router.post("/ingest")
356
+ async def ingest(request: IngestionRequest):
357
+ return await enqueue_ingestion(request.source_uri)
358
+ `;
359
+ }
360
+ const bodyClass = `${titleCase(plan.pattern.id).replace(/\s/g, '')}Request`;
361
+ return `from fastapi import APIRouter
362
+ from pydantic import BaseModel
363
+
364
+ from backend.orchestration.agents.${agentName} import run_${moduleName}
365
+
366
+ router = APIRouter(prefix="${prefix}", tags=["${plan.pattern.id}"])
367
+
368
+
369
+ class ${bodyClass}(BaseModel):
370
+ input: str
371
+
372
+
373
+ @router.post("/run")
374
+ async def run(request: ${bodyClass}):
375
+ return await run_${moduleName}(request.input)
376
+ `;
377
+ }
378
+ function renderAuthDependency() {
379
+ return `from dataclasses import dataclass
380
+
381
+
382
+ @dataclass(frozen=True)
383
+ class CurrentUser:
384
+ subject: str = "local-developer"
385
+
386
+
387
+ async def get_current_user() -> CurrentUser:
388
+ return CurrentUser()
389
+ `;
390
+ }
391
+ function renderSettings(plan) {
392
+ return `from functools import lru_cache
393
+ from pydantic_settings import BaseSettings, SettingsConfigDict
394
+
395
+
396
+ class Settings(BaseSettings):
397
+ model_config = SettingsConfigDict(env_file=".env", extra="ignore")
398
+
399
+ app_name: str = "${plan.projectName}"
400
+ app_env: str = "dev"
401
+ genai_pattern: str = "${plan.pattern.id}"
402
+ cloud_provider: str = "${plan.provider.id}"
403
+ azure_region: str = "${plan.region.slug}"
404
+ database_url: str
405
+ redis_url: str
406
+ messaging_transport: str = "redis-streams"
407
+ blob_endpoint: str | None = None
408
+ langfuse_host: str | None = None
409
+
410
+
411
+ @lru_cache
412
+ def get_settings() -> Settings:
413
+ return Settings()
414
+ `;
415
+ }
416
+ function renderModelConfig(plan) {
417
+ return `from pydantic import BaseModel, Field
418
+
419
+
420
+ class ModelConfig(BaseModel):
421
+ provider: str = Field(default="azure-openai")
422
+ deployment_name: str = Field(default="gpt-4.1")
423
+ embedding_deployment_name: str = Field(default="text-embedding-3-large")
424
+ pattern: str = Field(default="${plan.pattern.id}")
425
+ `;
426
+ }
427
+ function renderMessagingBoundary() {
428
+ return `from typing import Protocol
429
+
430
+
431
+ class MessagePublisher(Protocol):
432
+ async def publish(self, topic: str, payload: dict) -> None:
433
+ ...
434
+
435
+
436
+ class RedisStreamPublisher:
437
+ async def publish(self, topic: str, payload: dict) -> None:
438
+ # Implement with redis.asyncio in application code.
439
+ return None
440
+
441
+
442
+ class AzureServiceBusPublisher:
443
+ async def publish(self, topic: str, payload: dict) -> None:
444
+ # Implement with azure-servicebus in cloud configuration.
445
+ return None
446
+
447
+
448
+ def build_message_publisher(transport: str) -> MessagePublisher:
449
+ if transport == "azure-service-bus":
450
+ return AzureServiceBusPublisher()
451
+ return RedisStreamPublisher()
452
+ `;
453
+ }
454
+ function renderTracing() {
455
+ return `from contextlib import asynccontextmanager
456
+
457
+
458
+ @asynccontextmanager
459
+ async def trace_llm_operation(name: str):
460
+ # Wire Langfuse callbacks here when model credentials are configured.
461
+ yield {"trace": name}
462
+ `;
463
+ }
464
+ function renderBackendHealthTest() {
465
+ return `from fastapi.testclient import TestClient
466
+
467
+ from backend.apis.main import app
468
+
469
+
470
+ def test_health():
471
+ client = TestClient(app)
472
+ response = client.get("/health")
473
+ assert response.status_code == 200
474
+ assert response.json()["status"] == "ok"
475
+ `;
476
+ }
477
+ function renderAlembicIni() {
478
+ return `[alembic]
479
+ script_location = migrations
480
+ sqlalchemy.url = driver://user:pass@localhost/dbname
481
+ `;
482
+ }
483
+ function renderAlembicEnv() {
484
+ return `from alembic import context
485
+
486
+
487
+ def run_migrations_online():
488
+ context.configure(url=context.get_x_argument(as_dictionary=True).get("database_url"))
489
+ with context.begin_transaction():
490
+ context.run_migrations()
491
+
492
+
493
+ run_migrations_online()
494
+ `;
495
+ }
496
+ function renderInitialMigration(plan) {
497
+ const vectorExtension = plan.pattern.id === 'rag' ? ' op.execute("CREATE EXTENSION IF NOT EXISTS vector")\n' : '';
498
+ return `from alembic import op
499
+ import sqlalchemy as sa
500
+
501
+ revision = "0001_initial"
502
+ down_revision = None
503
+ branch_labels = None
504
+ depends_on = None
505
+
506
+
507
+ def upgrade():
508
+ ${vectorExtension} op.create_table(
509
+ "events",
510
+ sa.Column("id", sa.Integer(), primary_key=True),
511
+ sa.Column("event_type", sa.String(length=120), nullable=False),
512
+ sa.Column("payload", sa.JSON(), nullable=False),
513
+ sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
514
+ )
515
+
516
+
517
+ def downgrade():
518
+ op.drop_table("events")
519
+ `;
520
+ }
521
+ function renderDatabaseSchema(plan) {
522
+ return `CREATE TABLE IF NOT EXISTS events (
523
+ id SERIAL PRIMARY KEY,
524
+ event_type VARCHAR(120) NOT NULL,
525
+ payload JSONB NOT NULL,
526
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
527
+ );
528
+ ${plan.pattern.id === 'rag' ? '\nCREATE EXTENSION IF NOT EXISTS vector;\n' : ''}`;
529
+ }
530
+ function renderPatternAgent(plan) {
531
+ const moduleName = pyModule(plan.pattern.id);
532
+ if (plan.pattern.id === 'rag') {
533
+ return `from backend.orchestration.tools.messaging import build_message_publisher
534
+
535
+
536
+ async def answer_question(question: str) -> dict:
537
+ return {
538
+ "answer": "Replace this placeholder with PydanticAI retrieval orchestration.",
539
+ "question": question,
540
+ "citations": [],
541
+ }
542
+
543
+
544
+ async def enqueue_ingestion(source_uri: str) -> dict:
545
+ publisher = build_message_publisher("redis-streams")
546
+ await publisher.publish("rag.ingest", {"source_uri": source_uri})
547
+ return {"status": "queued", "source_uri": source_uri}
548
+ `;
549
+ }
550
+ if (plan.pattern.id === 'streaming') {
551
+ return `async def stream_response(prompt: str):
552
+ yield f"data: Starting response for {prompt}\\n\\n"
553
+ yield "data: Replace this placeholder with PydanticAI streaming orchestration.\\n\\n"
554
+ `;
555
+ }
556
+ return `async def run_${moduleName}(input_text: str) -> dict:
557
+ return {
558
+ "result": "Replace this placeholder with ${plan.pattern.label} PydanticAI orchestration.",
559
+ "input": input_text,
560
+ }
561
+ `;
562
+ }
563
+ function renderPromptTemplate(plan) {
564
+ return `# ${plan.pattern.label} Prompt
565
+
566
+ You are implementing a ${plan.pattern.label} generated by Mission Control Liftoff.
567
+
568
+ Use PydanticAI orchestration and return outputs that match the API contract.
569
+ `;
570
+ }
571
+ function renderPromptReadme() {
572
+ return `# Prompt Templates
573
+
574
+ Store prompt templates here and reference them from the PydanticAI orchestration layer.
575
+ `;
576
+ }
577
+ function renderVectorStore() {
578
+ return `from typing import Protocol
579
+
580
+
581
+ class VectorStore(Protocol):
582
+ async def search(self, query: str, limit: int = 5) -> list[dict]:
583
+ ...
584
+
585
+
586
+ class PgVectorStore:
587
+ async def search(self, query: str, limit: int = 5) -> list[dict]:
588
+ return []
589
+ `;
590
+ }
591
+ function renderPatternWorker(plan) {
592
+ return `async def run_worker() -> None:
593
+ # Consume ${plan.pattern.label} jobs from the configured messaging boundary.
594
+ return None
595
+ `;
596
+ }
597
+ function renderBackendEnv(plan, environment) {
598
+ const transport = environment === 'dev' ? 'redis-streams' : 'azure-service-bus';
599
+ return `APP_ENV=${environment}
600
+ APP_NAME=${plan.safeProjectName}
601
+ GENAI_PATTERN=${plan.pattern.id}
602
+ CLOUD_PROVIDER=${plan.provider.id}
603
+ AZURE_REGION=${plan.region.slug}
604
+ DATABASE_URL=postgresql+asyncpg://postgres:postgres@postgres:5432/${plan.safeProjectName.replace(/-/g, '_')}
605
+ REDIS_URL=redis://redis:6379/0
606
+ MESSAGING_TRANSPORT=${transport}
607
+ BLOB_ENDPOINT=
608
+ LANGFUSE_HOST=
609
+ `;
610
+ }
611
+ function renderDockerCompose(plan) {
612
+ const frontendService = plan.includeFrontend ? `
613
+ frontend:
614
+ build:
615
+ context: ./frontend
616
+ ports:
617
+ - "5173:80"
618
+ depends_on:
619
+ - backend
620
+ ` : '';
621
+ return `services:
622
+ backend:
623
+ build:
624
+ context: .
625
+ dockerfile: Dockerfile
626
+ env_file:
627
+ - ./environments/dev/backend.env
628
+ ports:
629
+ - "8000:8000"
630
+ depends_on:
631
+ - postgres
632
+ - redis
633
+ - azurite
634
+ - mailpit
635
+ ${frontendService}
636
+ postgres:
637
+ image: pgvector/pgvector:pg16
638
+ environment:
639
+ POSTGRES_USER: postgres
640
+ POSTGRES_PASSWORD: postgres
641
+ POSTGRES_DB: ${plan.safeProjectName.replace(/-/g, '_')}
642
+ ports:
643
+ - "5432:5432"
644
+
645
+ redis:
646
+ image: redis:7-alpine
647
+ ports:
648
+ - "6379:6379"
649
+
650
+ azurite:
651
+ image: mcr.microsoft.com/azure-storage/azurite
652
+ command: azurite --blobHost 0.0.0.0
653
+ ports:
654
+ - "10000:10000"
655
+
656
+ mailpit:
657
+ image: axllent/mailpit:latest
658
+ ports:
659
+ - "8025:8025"
660
+
661
+ langfuse:
662
+ image: langfuse/langfuse:2
663
+ profiles:
664
+ - observability
665
+ environment:
666
+ DATABASE_URL: postgresql://postgres:postgres@postgres:5432/${plan.safeProjectName.replace(/-/g, '_')}
667
+ NEXTAUTH_SECRET: local-development-placeholder
668
+ SALT: local-development-placeholder
669
+ ports:
670
+ - "3000:3000"
671
+ `;
672
+ }
673
+ function renderTofuVersions() {
674
+ return `terraform {
675
+ required_version = ">= 1.6.0"
676
+ required_providers {
677
+ azurerm = {
678
+ source = "hashicorp/azurerm"
679
+ version = "~> 3.110"
680
+ }
681
+ }
682
+ }
683
+ `;
684
+ }
685
+ function renderTofuProviders() {
686
+ return `provider "azurerm" {
687
+ features {}
688
+ }
689
+ `;
690
+ }
691
+ function renderTofuVariables(plan) {
692
+ return `variable "environment" {
693
+ type = string
694
+ description = "Deployment environment name."
695
+ }
696
+
697
+ variable "location" {
698
+ type = string
699
+ description = "Azure region slug."
700
+ default = "${plan.region.slug}"
701
+ }
702
+
703
+ variable "resource_suffix" {
704
+ type = string
705
+ description = "Globally unique suffix for Azure resource names."
706
+ }
707
+
708
+ variable "postgres_admin_password" {
709
+ type = string
710
+ sensitive = true
711
+ description = "PostgreSQL administrator password supplied at apply time."
712
+ }
713
+
714
+ variable "enable_private_networking" {
715
+ type = bool
716
+ default = false
717
+ description = "Enable production-oriented private-networking-ready settings."
718
+ }
719
+ `;
720
+ }
721
+ function renderTofuMain(plan) {
722
+ const frontendContainer = plan.includeFrontend ? `
723
+ resource "azurerm_container_app" "frontend" {
724
+ name = "ca-${plan.safeProjectName}-frontend-\${var.environment}"
725
+ container_app_environment_id = azurerm_container_app_environment.main.id
726
+ resource_group_name = azurerm_resource_group.main.name
727
+ revision_mode = "Single"
728
+
729
+ template {
730
+ container {
731
+ name = "frontend"
732
+ image = "\${azurerm_container_registry.main.login_server}/${plan.safeProjectName}-frontend:latest"
733
+ cpu = 0.25
734
+ memory = "0.5Gi"
735
+ }
736
+ }
737
+
738
+ ingress {
739
+ external_enabled = true
740
+ target_port = 80
741
+ traffic_weight {
742
+ percentage = 100
743
+ latest_revision = true
744
+ }
745
+ }
746
+ }
747
+ ` : '';
748
+ return `locals {
749
+ name_prefix = "${plan.safeProjectName}-\${var.environment}"
750
+ }
751
+
752
+ resource "azurerm_resource_group" "main" {
753
+ name = "rg-\${local.name_prefix}"
754
+ location = var.location
755
+ }
756
+
757
+ resource "azurerm_container_registry" "main" {
758
+ name = "acr\${replace(var.resource_suffix, "-", "")}"
759
+ resource_group_name = azurerm_resource_group.main.name
760
+ location = azurerm_resource_group.main.location
761
+ sku = "Basic"
762
+ admin_enabled = false
763
+ }
764
+
765
+ resource "azurerm_user_assigned_identity" "app" {
766
+ name = "id-\${local.name_prefix}"
767
+ resource_group_name = azurerm_resource_group.main.name
768
+ location = azurerm_resource_group.main.location
769
+ }
770
+
771
+ resource "azurerm_container_app_environment" "main" {
772
+ name = "cae-\${local.name_prefix}"
773
+ resource_group_name = azurerm_resource_group.main.name
774
+ location = azurerm_resource_group.main.location
775
+ }
776
+
777
+ resource "azurerm_container_app" "backend" {
778
+ name = "ca-${plan.safeProjectName}-backend-\${var.environment}"
779
+ container_app_environment_id = azurerm_container_app_environment.main.id
780
+ resource_group_name = azurerm_resource_group.main.name
781
+ revision_mode = "Single"
782
+
783
+ identity {
784
+ type = "UserAssigned"
785
+ identity_ids = [azurerm_user_assigned_identity.app.id]
786
+ }
787
+
788
+ template {
789
+ container {
790
+ name = "backend"
791
+ image = "\${azurerm_container_registry.main.login_server}/${plan.safeProjectName}-backend:latest"
792
+ cpu = 0.5
793
+ memory = "1Gi"
794
+ }
795
+ }
796
+
797
+ ingress {
798
+ external_enabled = true
799
+ target_port = 8000
800
+ traffic_weight {
801
+ percentage = 100
802
+ latest_revision = true
803
+ }
804
+ }
805
+ }
806
+ ${frontendContainer}
807
+ resource "azurerm_postgresql_flexible_server" "main" {
808
+ name = "psql-\${local.name_prefix}-\${var.resource_suffix}"
809
+ resource_group_name = azurerm_resource_group.main.name
810
+ location = azurerm_resource_group.main.location
811
+ version = "16"
812
+ administrator_login = "liftoffadmin"
813
+ administrator_password = var.postgres_admin_password
814
+ storage_mb = 32768
815
+ sku_name = "B_Standard_B1ms"
816
+ }
817
+
818
+ resource "azurerm_redis_cache" "main" {
819
+ name = "redis-\${local.name_prefix}-\${var.resource_suffix}"
820
+ location = azurerm_resource_group.main.location
821
+ resource_group_name = azurerm_resource_group.main.name
822
+ capacity = 0
823
+ family = "C"
824
+ sku_name = "Basic"
825
+ }
826
+
827
+ resource "azurerm_storage_account" "main" {
828
+ name = "st\${replace(var.resource_suffix, "-", "")}"
829
+ resource_group_name = azurerm_resource_group.main.name
830
+ location = azurerm_resource_group.main.location
831
+ account_tier = "Standard"
832
+ account_replication_type = "LRS"
833
+ }
834
+
835
+ resource "azurerm_storage_container" "documents" {
836
+ name = "documents"
837
+ storage_account_name = azurerm_storage_account.main.name
838
+ container_access_type = "private"
839
+ }
840
+
841
+ resource "azurerm_servicebus_namespace" "main" {
842
+ name = "sb-\${local.name_prefix}-\${var.resource_suffix}"
843
+ location = azurerm_resource_group.main.location
844
+ resource_group_name = azurerm_resource_group.main.name
845
+ sku = "Standard"
846
+ }
847
+
848
+ resource "azurerm_servicebus_queue" "events" {
849
+ name = "events"
850
+ namespace_id = azurerm_servicebus_namespace.main.id
851
+ }
852
+
853
+ resource "azurerm_communication_service" "main" {
854
+ name = "acs-\${local.name_prefix}"
855
+ resource_group_name = azurerm_resource_group.main.name
856
+ data_location = "United States"
857
+ }
858
+
859
+ resource "azurerm_key_vault" "main" {
860
+ name = "kv-\${local.name_prefix}-\${var.resource_suffix}"
861
+ location = azurerm_resource_group.main.location
862
+ resource_group_name = azurerm_resource_group.main.name
863
+ tenant_id = data.azurerm_client_config.current.tenant_id
864
+ sku_name = "standard"
865
+ }
866
+
867
+ data "azurerm_client_config" "current" {}
868
+ `;
869
+ }
870
+ function renderTofuOutputs(plan) {
871
+ return `output "backend_url" {
872
+ value = azurerm_container_app.backend.ingress[0].fqdn
873
+ }
874
+
875
+ ${plan.includeFrontend ? `output "frontend_url" {
876
+ value = azurerm_container_app.frontend.ingress[0].fqdn
877
+ }
878
+ ` : ''}output "container_registry" {
879
+ value = azurerm_container_registry.main.login_server
880
+ }
881
+ `;
882
+ }
883
+ function renderTofuLocalState() {
884
+ return `# Local state is the V1 default for first-use simplicity.
885
+ # Teams can replace this file with backend.remote.example.tf when adopting shared state.
886
+ `;
887
+ }
888
+ function renderTofuRemoteStateExample() {
889
+ return `# Rename to backend.tf and configure values for shared state.
890
+ # terraform {
891
+ # backend "azurerm" {
892
+ # resource_group_name = "rg-opentofu-state"
893
+ # storage_account_name = "stliftoffstate"
894
+ # container_name = "tfstate"
895
+ # key = "mission-control/liftoff.tfstate"
896
+ # }
897
+ # }
898
+ `;
899
+ }
900
+ function renderTofuReadme(plan) {
901
+ const env = plan.environments[0]?.id ?? 'dev';
902
+ return `# Azure OpenTofu
903
+
904
+ Azure is the complete V1 provider for this Liftoff project.
905
+
906
+ \`\`\`bash
907
+ tofu init
908
+ tofu plan -var-file=environments/${env}.tfvars
909
+ tofu apply -var-file=environments/${env}.tfvars
910
+ tofu output
911
+ \`\`\`
912
+
913
+ Local OpenTofu state is generated by default. Use \`backend.remote.example.tf\` as the starting point for team remote state.
914
+ `;
915
+ }
916
+ function renderTofuTfvars(plan, environment) {
917
+ return `environment = "${environment}"
918
+ location = "${plan.region.slug}"
919
+ resource_suffix = "${plan.safeProjectName.replace(/-/g, '')}${environment}"
920
+ enable_private_networking = ${environment === 'prod' ? 'true' : 'false'}
921
+ `;
922
+ }
923
+ function renderOpenSpecConfig(plan) {
924
+ const frontendRule = plan.includeFrontend ? '\n - Keep frontend code under frontend.' : '';
925
+ return `schema: spec-driven
926
+
927
+ context: |
928
+ Project generated by Mission Control Liftoff.
929
+ GenAI pattern: ${plan.pattern.label}
930
+ Application framework: FastAPI + PydanticAI.
931
+ API developer portal: Scalar.
932
+ Infrastructure: OpenTofu.
933
+ Primary cloud: Azure (${plan.region.slug}).
934
+ Local development: Docker Compose.
935
+ Database: PostgreSQL with Alembic migrations.
936
+ Cache and local messaging: Redis.
937
+ Observability: Langfuse.
938
+ Environments: ${plan.environments.map((environment) => environment.id).join(', ')}.
939
+
940
+ rules:
941
+ specs:
942
+ - Requirements must describe observable product behavior.
943
+ - Cloud behavior must identify environment differences for generated environments.
944
+ design:
945
+ - Use PydanticAI for orchestration logic.
946
+ - Use Pydantic settings models for runtime configuration.
947
+ - Use OpenTofu for infrastructure changes.${frontendRule}
948
+ - Keep backend API code under backend/apis.
949
+ - Keep database artifacts under database.
950
+ tasks:
951
+ - Include local Docker Compose verification.
952
+ - Include OpenTofu validation for generated infrastructure.
953
+ `;
954
+ }
955
+ function renderSeedProposal(plan) {
956
+ return `## Why
957
+
958
+ Bootstrap the generated ${plan.pattern.label} application baseline created by Mission Control Liftoff.
959
+
960
+ ## What Changes
961
+
962
+ - Establish the approved backend, infrastructure, local development, and governance baseline.
963
+ - Capture follow-up product requirements through spec-driven changes.
964
+
965
+ ## Capabilities
966
+
967
+ ### New Capabilities
968
+
969
+ - \`${plan.pattern.id}-application-baseline\`: Generated application baseline for this Liftoff project.
970
+
971
+ ### Modified Capabilities
972
+
973
+ - None.
974
+
975
+ ## Impact
976
+
977
+ - Generated FastAPI/PydanticAI backend, OpenTofu infrastructure, Docker Compose local development, and governance files.
978
+ `;
979
+ }
980
+ function renderSeedDesign(plan) {
981
+ return `## Context
982
+
983
+ This project was generated with Liftoff using ${plan.pattern.label}, Azure, OpenTofu, and ${plan.specWorkflow.label}.
984
+
985
+ ## Goals / Non-Goals
986
+
987
+ **Goals:**
988
+
989
+ - Keep the generated baseline aligned to the approved Mission Control stack.
990
+
991
+ **Non-Goals:**
992
+
993
+ - Define domain-specific product behavior in the bootstrap change.
994
+
995
+ ## Decisions
996
+
997
+ - Use FastAPI and PydanticAI for backend APIs and orchestration.
998
+ - Use OpenTofu for Azure infrastructure.
999
+ - Use Docker Compose for local development.
1000
+
1001
+ ## Risks / Trade-offs
1002
+
1003
+ - The baseline contains placeholders that product-specific changes should replace.
1004
+ `;
1005
+ }
1006
+ function renderSeedTasks() {
1007
+ return `## 1. Bootstrap Review
1008
+
1009
+ - [ ] 1.1 Review generated baseline and replace placeholders with domain-specific requirements.
1010
+ - [ ] 1.2 Validate local Docker Compose startup.
1011
+ - [ ] 1.3 Validate OpenTofu plan for the first target environment.
1012
+ `;
1013
+ }
1014
+ function renderSpecKitConstitution(plan) {
1015
+ return `# Mission Control Liftoff Constitution
1016
+
1017
+ ## Principle 1: Approved Application Stack
1018
+ Generated backend services MUST use FastAPI, PydanticAI, Pydantic configuration models, and Scalar for API documentation.
1019
+
1020
+ ## Principle 2: Standard Project Layout
1021
+ Backend APIs live under backend/apis. Database artifacts live under database.${plan.includeFrontend ? ' Frontend code lives under frontend.' : ''}
1022
+
1023
+ ## Principle 3: Infrastructure As Code
1024
+ Cloud infrastructure MUST be defined with OpenTofu. Azure is the supported V1 provider.
1025
+
1026
+ ## Principle 4: Local Development Parity
1027
+ Projects MUST include Docker Compose for local development with PostgreSQL, Redis, local blob storage, and local messaging behavior.
1028
+
1029
+ ## Principle 5: Observability And Operations
1030
+ LLM workflows MUST include Langfuse tracing hooks and environment-specific configuration for ${plan.environments.map((environment) => environment.id).join(', ')}.
1031
+ `;
1032
+ }
1033
+ function renderSpecKitSpecTemplate() {
1034
+ return `# Feature Specification
1035
+
1036
+ ## User Scenarios
1037
+
1038
+ ## Requirements
1039
+
1040
+ ## Success Criteria
1041
+ `;
1042
+ }
1043
+ function renderSpecKitPlanTemplate() {
1044
+ return `# Implementation Plan
1045
+
1046
+ ## Technical Context
1047
+
1048
+ ## Constitution Check
1049
+
1050
+ ## Tasks
1051
+ `;
1052
+ }
1053
+ function renderFrontendPackage(plan) {
1054
+ return JSON.stringify({
1055
+ name: `${plan.safeProjectName}-frontend`,
1056
+ version: '0.1.0',
1057
+ private: true,
1058
+ type: 'module',
1059
+ scripts: {
1060
+ dev: 'vite',
1061
+ build: 'vite build',
1062
+ preview: 'vite preview'
1063
+ },
1064
+ dependencies: {
1065
+ '@vitejs/plugin-vue': '^5.0.5',
1066
+ vite: '^5.3.1',
1067
+ vue: '^3.4.29',
1068
+ tailwindcss: '^3.4.4',
1069
+ autoprefixer: '^10.4.19',
1070
+ postcss: '^8.4.38'
1071
+ }
1072
+ }, null, 2);
1073
+ }
1074
+ function renderFrontendIndex(plan) {
1075
+ return `<div id="app"></div><script type="module" src="/src/main.ts"></script><title>${plan.projectName}</title>`;
1076
+ }
1077
+ function renderFrontendMain() {
1078
+ return `import { createApp } from 'vue';
1079
+ import App from './App.vue';
1080
+ import './styles.css';
1081
+
1082
+ createApp(App).mount('#app');
1083
+ `;
1084
+ }
1085
+ function renderFrontendApp(plan) {
1086
+ return `<script setup lang="ts">
1087
+ const title = '${plan.projectName}';
1088
+ const starter = '${plan.frontendStarter}';
1089
+ const pattern = '${plan.pattern.label}';
1090
+ </script>
1091
+
1092
+ <template>
1093
+ <main class="min-h-screen bg-slate-50 text-slate-950">
1094
+ <section class="mx-auto flex min-h-screen w-full max-w-5xl flex-col gap-6 px-6 py-10">
1095
+ <header>
1096
+ <p class="text-sm font-semibold uppercase tracking-wide text-emerald-700">Mission Control Liftoff</p>
1097
+ <h1 class="mt-2 text-3xl font-bold">{{ title }}</h1>
1098
+ <p class="mt-2 text-slate-600">{{ pattern }} starter</p>
1099
+ </header>
1100
+ <section class="rounded-lg border border-slate-200 bg-white p-6 shadow-sm">
1101
+ <h2 class="text-xl font-semibold">{{ starter }}</h2>
1102
+ <textarea class="mt-4 min-h-40 w-full rounded-md border border-slate-300 p-3" placeholder="Start with your application-specific input." />
1103
+ <button class="mt-4 rounded-md bg-emerald-700 px-4 py-2 font-semibold text-white">Run</button>
1104
+ </section>
1105
+ </section>
1106
+ </main>
1107
+ </template>
1108
+ `;
1109
+ }
1110
+ function renderFrontendStyles() {
1111
+ return `@tailwind base;
1112
+ @tailwind components;
1113
+ @tailwind utilities;
1114
+ `;
1115
+ }
1116
+ function renderFrontendViteConfig() {
1117
+ return `import { defineConfig } from 'vite';
1118
+ import vue from '@vitejs/plugin-vue';
1119
+
1120
+ export default defineConfig({ plugins: [vue()] });
1121
+ `;
1122
+ }
1123
+ function renderFrontendTailwindConfig() {
1124
+ return `import type { Config } from 'tailwindcss';
1125
+
1126
+ export default {
1127
+ content: ['./index.html', './src/**/*.{vue,ts}'],
1128
+ theme: { extend: {} },
1129
+ plugins: []
1130
+ } satisfies Config;
1131
+ `;
1132
+ }
1133
+ function renderFrontendDockerfile() {
1134
+ return `FROM node:20-alpine AS build
1135
+ WORKDIR /app
1136
+ COPY package.json package-lock.json* ./
1137
+ RUN npm install
1138
+ COPY . .
1139
+ RUN npm run build
1140
+
1141
+ FROM nginx:1.27-alpine
1142
+ COPY --from=build /app/dist /usr/share/nginx/html
1143
+ `;
1144
+ }
1145
+ function ensureTrailingNewline(content) {
1146
+ return content.endsWith('\n') ? content : `${content}\n`;
1147
+ }
1148
+ //# sourceMappingURL=templates.js.map