popeye-cli 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/README.md +240 -32
  2. package/cheatsheet.md +407 -0
  3. package/dist/cli/commands/db.d.ts +10 -0
  4. package/dist/cli/commands/db.d.ts.map +1 -0
  5. package/dist/cli/commands/db.js +240 -0
  6. package/dist/cli/commands/db.js.map +1 -0
  7. package/dist/cli/commands/doctor.d.ts +18 -0
  8. package/dist/cli/commands/doctor.d.ts.map +1 -0
  9. package/dist/cli/commands/doctor.js +255 -0
  10. package/dist/cli/commands/doctor.js.map +1 -0
  11. package/dist/cli/commands/index.d.ts +2 -0
  12. package/dist/cli/commands/index.d.ts.map +1 -1
  13. package/dist/cli/commands/index.js +2 -0
  14. package/dist/cli/commands/index.js.map +1 -1
  15. package/dist/cli/index.d.ts.map +1 -1
  16. package/dist/cli/index.js +3 -1
  17. package/dist/cli/index.js.map +1 -1
  18. package/dist/cli/interactive.d.ts.map +1 -1
  19. package/dist/cli/interactive.js +96 -0
  20. package/dist/cli/interactive.js.map +1 -1
  21. package/dist/generators/admin-wizard.d.ts +25 -0
  22. package/dist/generators/admin-wizard.d.ts.map +1 -0
  23. package/dist/generators/admin-wizard.js +123 -0
  24. package/dist/generators/admin-wizard.js.map +1 -0
  25. package/dist/generators/all.d.ts.map +1 -1
  26. package/dist/generators/all.js +10 -3
  27. package/dist/generators/all.js.map +1 -1
  28. package/dist/generators/database.d.ts +58 -0
  29. package/dist/generators/database.d.ts.map +1 -0
  30. package/dist/generators/database.js +229 -0
  31. package/dist/generators/database.js.map +1 -0
  32. package/dist/generators/fullstack.d.ts.map +1 -1
  33. package/dist/generators/fullstack.js +23 -7
  34. package/dist/generators/fullstack.js.map +1 -1
  35. package/dist/generators/index.d.ts +2 -0
  36. package/dist/generators/index.d.ts.map +1 -1
  37. package/dist/generators/index.js +2 -0
  38. package/dist/generators/index.js.map +1 -1
  39. package/dist/generators/templates/admin-wizard-python.d.ts +32 -0
  40. package/dist/generators/templates/admin-wizard-python.d.ts.map +1 -0
  41. package/dist/generators/templates/admin-wizard-python.js +425 -0
  42. package/dist/generators/templates/admin-wizard-python.js.map +1 -0
  43. package/dist/generators/templates/admin-wizard-react.d.ts +48 -0
  44. package/dist/generators/templates/admin-wizard-react.d.ts.map +1 -0
  45. package/dist/generators/templates/admin-wizard-react.js +554 -0
  46. package/dist/generators/templates/admin-wizard-react.js.map +1 -0
  47. package/dist/generators/templates/database-docker.d.ts +23 -0
  48. package/dist/generators/templates/database-docker.d.ts.map +1 -0
  49. package/dist/generators/templates/database-docker.js +221 -0
  50. package/dist/generators/templates/database-docker.js.map +1 -0
  51. package/dist/generators/templates/database-python.d.ts +54 -0
  52. package/dist/generators/templates/database-python.d.ts.map +1 -0
  53. package/dist/generators/templates/database-python.js +723 -0
  54. package/dist/generators/templates/database-python.js.map +1 -0
  55. package/dist/generators/templates/database-typescript.d.ts +34 -0
  56. package/dist/generators/templates/database-typescript.d.ts.map +1 -0
  57. package/dist/generators/templates/database-typescript.js +232 -0
  58. package/dist/generators/templates/database-typescript.js.map +1 -0
  59. package/dist/generators/templates/fullstack.d.ts.map +1 -1
  60. package/dist/generators/templates/fullstack.js +29 -0
  61. package/dist/generators/templates/fullstack.js.map +1 -1
  62. package/dist/generators/templates/index.d.ts +5 -0
  63. package/dist/generators/templates/index.d.ts.map +1 -1
  64. package/dist/generators/templates/index.js +5 -0
  65. package/dist/generators/templates/index.js.map +1 -1
  66. package/dist/state/index.d.ts +10 -0
  67. package/dist/state/index.d.ts.map +1 -1
  68. package/dist/state/index.js +22 -0
  69. package/dist/state/index.js.map +1 -1
  70. package/dist/types/consensus.d.ts +3 -0
  71. package/dist/types/consensus.d.ts.map +1 -1
  72. package/dist/types/consensus.js +1 -0
  73. package/dist/types/consensus.js.map +1 -1
  74. package/dist/types/database-runtime.d.ts +86 -0
  75. package/dist/types/database-runtime.d.ts.map +1 -0
  76. package/dist/types/database-runtime.js +61 -0
  77. package/dist/types/database-runtime.js.map +1 -0
  78. package/dist/types/database.d.ts +85 -0
  79. package/dist/types/database.d.ts.map +1 -0
  80. package/dist/types/database.js +71 -0
  81. package/dist/types/database.js.map +1 -0
  82. package/dist/types/index.d.ts +3 -0
  83. package/dist/types/index.d.ts.map +1 -1
  84. package/dist/types/index.js +6 -0
  85. package/dist/types/index.js.map +1 -1
  86. package/dist/types/tester.d.ts +138 -0
  87. package/dist/types/tester.d.ts.map +1 -0
  88. package/dist/types/tester.js +110 -0
  89. package/dist/types/tester.js.map +1 -0
  90. package/dist/types/workflow.d.ts +166 -0
  91. package/dist/types/workflow.d.ts.map +1 -1
  92. package/dist/types/workflow.js +14 -0
  93. package/dist/types/workflow.js.map +1 -1
  94. package/dist/workflow/db-setup-runner.d.ts +63 -0
  95. package/dist/workflow/db-setup-runner.d.ts.map +1 -0
  96. package/dist/workflow/db-setup-runner.js +336 -0
  97. package/dist/workflow/db-setup-runner.js.map +1 -0
  98. package/dist/workflow/db-state-machine.d.ts +30 -0
  99. package/dist/workflow/db-state-machine.d.ts.map +1 -0
  100. package/dist/workflow/db-state-machine.js +51 -0
  101. package/dist/workflow/db-state-machine.js.map +1 -0
  102. package/dist/workflow/execution-mode.js +2 -2
  103. package/dist/workflow/execution-mode.js.map +1 -1
  104. package/dist/workflow/index.d.ts +3 -0
  105. package/dist/workflow/index.d.ts.map +1 -1
  106. package/dist/workflow/index.js +3 -0
  107. package/dist/workflow/index.js.map +1 -1
  108. package/dist/workflow/task-workflow.d.ts +5 -0
  109. package/dist/workflow/task-workflow.d.ts.map +1 -1
  110. package/dist/workflow/task-workflow.js +172 -6
  111. package/dist/workflow/task-workflow.js.map +1 -1
  112. package/dist/workflow/tester.d.ts +120 -0
  113. package/dist/workflow/tester.d.ts.map +1 -0
  114. package/dist/workflow/tester.js +589 -0
  115. package/dist/workflow/tester.js.map +1 -0
  116. package/dist/workflow/workflow-logger.d.ts +1 -1
  117. package/dist/workflow/workflow-logger.d.ts.map +1 -1
  118. package/dist/workflow/workflow-logger.js.map +1 -1
  119. package/package.json +1 -1
  120. package/src/cli/commands/db.ts +281 -0
  121. package/src/cli/commands/doctor.ts +273 -0
  122. package/src/cli/commands/index.ts +2 -0
  123. package/src/cli/index.ts +4 -0
  124. package/src/cli/interactive.ts +102 -0
  125. package/src/generators/admin-wizard.ts +146 -0
  126. package/src/generators/all.ts +10 -3
  127. package/src/generators/database.ts +286 -0
  128. package/src/generators/fullstack.ts +26 -9
  129. package/src/generators/index.ts +12 -0
  130. package/src/generators/templates/admin-wizard-python.ts +431 -0
  131. package/src/generators/templates/admin-wizard-react.ts +560 -0
  132. package/src/generators/templates/database-docker.ts +227 -0
  133. package/src/generators/templates/database-python.ts +734 -0
  134. package/src/generators/templates/database-typescript.ts +238 -0
  135. package/src/generators/templates/fullstack.ts +29 -0
  136. package/src/generators/templates/index.ts +5 -0
  137. package/src/state/index.ts +29 -0
  138. package/src/types/consensus.ts +3 -0
  139. package/src/types/database-runtime.ts +69 -0
  140. package/src/types/database.ts +84 -0
  141. package/src/types/index.ts +50 -0
  142. package/src/types/tester.ts +136 -0
  143. package/src/types/workflow.ts +31 -0
  144. package/src/workflow/db-setup-runner.ts +391 -0
  145. package/src/workflow/db-state-machine.ts +58 -0
  146. package/src/workflow/execution-mode.ts +2 -2
  147. package/src/workflow/index.ts +3 -0
  148. package/src/workflow/task-workflow.ts +227 -5
  149. package/src/workflow/tester.ts +723 -0
  150. package/src/workflow/workflow-logger.ts +2 -0
  151. package/tests/generators/admin-wizard-orchestrator.test.ts +64 -0
  152. package/tests/generators/admin-wizard-templates.test.ts +366 -0
  153. package/tests/generators/cross-phase-integration.test.ts +383 -0
  154. package/tests/generators/database.test.ts +456 -0
  155. package/tests/generators/fe-be-db-integration.test.ts +613 -0
  156. package/tests/types/database-runtime.test.ts +158 -0
  157. package/tests/types/database.test.ts +187 -0
  158. package/tests/types/tester.test.ts +174 -0
  159. package/tests/workflow/db-setup-runner.test.ts +211 -0
  160. package/tests/workflow/db-state-machine.test.ts +117 -0
  161. package/tests/workflow/tester.test.ts +401 -0
@@ -7,6 +7,10 @@ import { z } from 'zod';
7
7
  import { OutputLanguageSchema } from './project.js';
8
8
  import type { OutputLanguage, OpenAIModel } from './project.js';
9
9
  import type { ConsensusIteration } from './consensus.js';
10
+ import type { TestPlanOutput } from './tester.js';
11
+ import { TestPlanOutputSchema, TestVerdictSchema } from './tester.js';
12
+ import type { DbConfig } from './database.js';
13
+ import { DbConfigSchema } from './database.js';
10
14
 
11
15
  /**
12
16
  * Workflow phases
@@ -68,6 +72,17 @@ export interface Task {
68
72
 
69
73
  // App target (which app this task affects)
70
74
  appTarget?: 'frontend' | 'backend' | 'unified';
75
+
76
+ // Tester (QA) tracking
77
+ qaTestPlanText?: string; // Approved test plan (markdown, for humans)
78
+ qaTestPlanParsed?: TestPlanOutput; // Parsed structured plan (for machine flow)
79
+ qaTestPlanScore?: number; // Consensus score (0-100)
80
+ qaTestPlanIterations?: number; // Iterations to reach consensus
81
+ qaTestPlanApproved?: boolean; // Whether consensus was reached
82
+ qaTestPlanDoc?: string; // Path to docs/qa/test-plans/...
83
+ qaVerdict?: 'PASS' | 'PASS_WITH_NOTES' | 'FAIL';
84
+ qaReviewNotes?: string; // Tester's review notes
85
+ qaReviewDoc?: string; // Path to docs/qa/test-runs/...
71
86
  }
72
87
 
73
88
  /**
@@ -107,6 +122,16 @@ export const TaskSchema = z.object({
107
122
  backendConsensus: AppConsensusTrackingSchema.optional(),
108
123
  unifiedConsensus: AppConsensusTrackingSchema.optional(),
109
124
  appTarget: z.enum(['frontend', 'backend', 'unified']).optional(),
125
+ // Tester (QA) tracking
126
+ qaTestPlanText: z.string().optional(),
127
+ qaTestPlanParsed: TestPlanOutputSchema.optional(),
128
+ qaTestPlanScore: z.number().optional(),
129
+ qaTestPlanIterations: z.number().optional(),
130
+ qaTestPlanApproved: z.boolean().optional(),
131
+ qaTestPlanDoc: z.string().optional(),
132
+ qaVerdict: TestVerdictSchema.optional(),
133
+ qaReviewNotes: z.string().optional(),
134
+ qaReviewDoc: z.string().optional(),
110
135
  });
111
136
 
112
137
  /**
@@ -205,6 +230,10 @@ export interface ProjectState {
205
230
  strategyError?: string;
206
231
  /** Absolute paths to discovered source documentation files */
207
232
  sourceDocPaths?: string[];
233
+ /** Whether QA Tester skill is active (default: true for new projects, undefined/false for existing) */
234
+ qaEnabled?: boolean;
235
+ /** Database configuration tracking (workspace projects only) */
236
+ dbConfig?: DbConfig;
208
237
  }
209
238
 
210
239
  /**
@@ -250,6 +279,8 @@ export const ProjectStateSchema = z.object({
250
279
  websiteStrategy: z.string().optional(),
251
280
  strategyError: z.string().optional(),
252
281
  sourceDocPaths: z.array(z.string()).optional(),
282
+ qaEnabled: z.boolean().optional(),
283
+ dbConfig: DbConfigSchema.optional(),
253
284
  });
254
285
 
255
286
  /**
@@ -0,0 +1,391 @@
1
+ /**
2
+ * Database setup pipeline runner
3
+ * Executes sequential steps to configure, migrate, and verify a database
4
+ */
5
+
6
+ import { promises as fs } from 'node:fs';
7
+ import path from 'node:path';
8
+ import { exec } from 'node:child_process';
9
+ import { promisify } from 'node:util';
10
+ import type { DbStatus, DbSetupStep } from '../types/database.js';
11
+ import type { SetupStepResult, SetupResult } from '../types/database-runtime.js';
12
+ import { transitionDbStatus } from './db-state-machine.js';
13
+
14
+ const execAsync = promisify(exec);
15
+
16
+ /**
17
+ * Options for the setup pipeline
18
+ */
19
+ export interface SetupPipelineOptions {
20
+ /** Skip seed step */
21
+ skipSeed?: boolean;
22
+ /** Callback for step progress */
23
+ onStep?: (step: DbSetupStep, status: 'start' | 'success' | 'fail', message: string) => void;
24
+ }
25
+
26
+ /**
27
+ * Read and parse a .env file for key=value pairs
28
+ *
29
+ * @param envPath - Path to .env file
30
+ * @returns Map of environment variable key-value pairs
31
+ */
32
+ export async function readEnvFile(envPath: string): Promise<Record<string, string>> {
33
+ const result: Record<string, string> = {};
34
+ try {
35
+ const content = await fs.readFile(envPath, 'utf-8');
36
+ for (const line of content.split('\n')) {
37
+ const trimmed = line.trim();
38
+ if (!trimmed || trimmed.startsWith('#')) continue;
39
+ const eqIndex = trimmed.indexOf('=');
40
+ if (eqIndex === -1) continue;
41
+ const key = trimmed.slice(0, eqIndex).trim();
42
+ let value = trimmed.slice(eqIndex + 1).trim();
43
+ // Strip surrounding quotes
44
+ if ((value.startsWith('"') && value.endsWith('"')) ||
45
+ (value.startsWith("'") && value.endsWith("'"))) {
46
+ value = value.slice(1, -1);
47
+ }
48
+ result[key] = value;
49
+ }
50
+ } catch {
51
+ // File doesn't exist or isn't readable
52
+ }
53
+ return result;
54
+ }
55
+
56
+ /**
57
+ * Scan migration files for prerequisite extension comments
58
+ * Looks for lines like: # popeye:requires_extension=vector
59
+ *
60
+ * @param migrationsDir - Path to migrations/versions/ directory
61
+ * @returns Array of required extension names
62
+ */
63
+ export async function parseMigrationPrereqs(migrationsDir: string): Promise<string[]> {
64
+ const extensions: string[] = [];
65
+ const versionsDir = path.join(migrationsDir, 'versions');
66
+
67
+ try {
68
+ const files = await fs.readdir(versionsDir);
69
+ for (const file of files) {
70
+ if (!file.endsWith('.py')) continue;
71
+ const content = await fs.readFile(path.join(versionsDir, file), 'utf-8');
72
+ const matches = content.matchAll(/# popeye:requires_extension=(\w+)/g);
73
+ for (const match of matches) {
74
+ if (!extensions.includes(match[1])) {
75
+ extensions.push(match[1]);
76
+ }
77
+ }
78
+ }
79
+ } catch {
80
+ // Directory doesn't exist or isn't readable
81
+ }
82
+
83
+ return extensions;
84
+ }
85
+
86
+ /**
87
+ * Derive the snake_case package name from the project state
88
+ *
89
+ * @param projectDir - Project root directory
90
+ * @returns Python package name
91
+ */
92
+ export async function getPackageName(projectDir: string): Promise<string> {
93
+ try {
94
+ const statePath = path.join(projectDir, '.popeye', 'state.json');
95
+ const content = await fs.readFile(statePath, 'utf-8');
96
+ const state = JSON.parse(content);
97
+ const name: string = state.name || 'project';
98
+ return name.toLowerCase().replace(/-/g, '_').replace(/[^a-z0-9_]/g, '');
99
+ } catch {
100
+ return 'backend';
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Resolve the backend directory path
106
+ *
107
+ * @param projectDir - Project root directory
108
+ * @returns Absolute path to apps/backend
109
+ */
110
+ export function resolveBackendDir(projectDir: string): string {
111
+ return path.join(projectDir, 'apps', 'backend');
112
+ }
113
+
114
+ /**
115
+ * Execute a single pipeline step and track timing
116
+ */
117
+ async function executeStep(
118
+ step: DbSetupStep,
119
+ fn: () => Promise<string>,
120
+ options?: SetupPipelineOptions
121
+ ): Promise<SetupStepResult> {
122
+ options?.onStep?.(step, 'start', `Starting ${step}...`);
123
+ const start = Date.now();
124
+
125
+ try {
126
+ const message = await fn();
127
+ const durationMs = Date.now() - start;
128
+ options?.onStep?.(step, 'success', message);
129
+ return { step, success: true, message, durationMs };
130
+ } catch (err) {
131
+ const durationMs = Date.now() - start;
132
+ const error = err instanceof Error ? err.message : String(err);
133
+ options?.onStep?.(step, 'fail', error);
134
+ return { step, success: false, message: `Step failed: ${step}`, durationMs, error };
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Step 1: Check database connection
140
+ */
141
+ async function checkConnection(backendDir: string): Promise<string> {
142
+ const env = await readEnvFile(path.join(backendDir, '.env'));
143
+ const dbUrl = env['DATABASE_URL'] || '';
144
+
145
+ if (!dbUrl) {
146
+ throw new Error(
147
+ 'DATABASE_URL not found in apps/backend/.env. ' +
148
+ 'Run "popeye db configure" to set it up.'
149
+ );
150
+ }
151
+
152
+ // Test connectivity using Python asyncpg
153
+ const cmd = `cd "${backendDir}" && python3 -c "
154
+ import asyncio, asyncpg, os
155
+ async def check():
156
+ conn = await asyncpg.connect('${dbUrl.replace(/'/g, "\\'")}')
157
+ await conn.execute('SELECT 1')
158
+ await conn.close()
159
+ asyncio.run(check())
160
+ "`;
161
+
162
+ try {
163
+ await execAsync(cmd, { timeout: 15000 });
164
+ return 'Database connection verified successfully';
165
+ } catch (err) {
166
+ const msg = err instanceof Error ? err.message : String(err);
167
+ throw new Error(`Database connection failed: ${msg}`);
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Step 2: Ensure required extensions
173
+ */
174
+ async function ensureExtensions(backendDir: string): Promise<string> {
175
+ const migrationsDir = path.join(backendDir, 'migrations');
176
+ const extensions = await parseMigrationPrereqs(migrationsDir);
177
+
178
+ if (extensions.length === 0) {
179
+ return 'No prerequisite extensions required';
180
+ }
181
+
182
+ const env = await readEnvFile(path.join(backendDir, '.env'));
183
+ const dbUrl = env['DATABASE_URL'] || '';
184
+
185
+ for (const ext of extensions) {
186
+ const cmd = `cd "${backendDir}" && python3 -c "
187
+ import asyncio, asyncpg
188
+ async def ensure():
189
+ conn = await asyncpg.connect('${dbUrl.replace(/'/g, "\\'")}')
190
+ await conn.execute('CREATE EXTENSION IF NOT EXISTS ${ext}')
191
+ await conn.close()
192
+ asyncio.run(ensure())
193
+ "`;
194
+
195
+ try {
196
+ await execAsync(cmd, { timeout: 15000 });
197
+ } catch (err) {
198
+ const msg = err instanceof Error ? err.message : String(err);
199
+ throw new Error(`Failed to create extension '${ext}': ${msg}`);
200
+ }
201
+ }
202
+
203
+ return `Extensions verified: ${extensions.join(', ')}`;
204
+ }
205
+
206
+ /**
207
+ * Step 3: Apply Alembic migrations
208
+ */
209
+ async function applyMigrations(backendDir: string): Promise<string> {
210
+ const cmd = `cd "${backendDir}" && alembic upgrade head 2>&1`;
211
+
212
+ try {
213
+ const { stdout } = await execAsync(cmd, { timeout: 60000 });
214
+ // Count applied migrations from output
215
+ const appliedMatches = stdout.match(/Running upgrade/g);
216
+ const count = appliedMatches ? appliedMatches.length : 0;
217
+ return count > 0
218
+ ? `Applied ${count} migration(s) successfully`
219
+ : 'All migrations already up to date';
220
+ } catch (err) {
221
+ const msg = err instanceof Error ? err.message : String(err);
222
+ throw new Error(`Migration failed: ${msg}`);
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Step 4: Run seed script (if exists)
228
+ */
229
+ async function seedMinimal(backendDir: string): Promise<string> {
230
+ const seedPaths = [
231
+ path.join(backendDir, 'scripts', 'seed.py'),
232
+ path.join(backendDir, 'seed.py'),
233
+ ];
234
+
235
+ let seedPath: string | null = null;
236
+ for (const p of seedPaths) {
237
+ try {
238
+ await fs.access(p);
239
+ seedPath = p;
240
+ break;
241
+ } catch {
242
+ // Try next
243
+ }
244
+ }
245
+
246
+ if (!seedPath) {
247
+ return 'No seed script found (skipped)';
248
+ }
249
+
250
+ const cmd = `cd "${backendDir}" && python3 "${seedPath}" 2>&1`;
251
+
252
+ try {
253
+ await execAsync(cmd, { timeout: 30000 });
254
+ return 'Seed script executed successfully';
255
+ } catch (err) {
256
+ const msg = err instanceof Error ? err.message : String(err);
257
+ throw new Error(`Seed script failed: ${msg}`);
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Step 5: Run readiness tests
263
+ */
264
+ async function runReadinessTests(backendDir: string): Promise<string> {
265
+ const env = await readEnvFile(path.join(backendDir, '.env'));
266
+ const dbUrl = env['DATABASE_URL'] || '';
267
+
268
+ const cmd = `cd "${backendDir}" && python3 -c "
269
+ import asyncio, asyncpg
270
+ async def check():
271
+ conn = await asyncpg.connect('${dbUrl.replace(/'/g, "\\'")}')
272
+ # Verify connectivity
273
+ await conn.execute('SELECT 1')
274
+ # Verify alembic_version table exists and has a version
275
+ row = await conn.fetchrow('SELECT version_num FROM alembic_version LIMIT 1')
276
+ if row is None:
277
+ raise Exception('No migration version found in alembic_version table')
278
+ await conn.close()
279
+ return row['version_num']
280
+ asyncio.run(check())
281
+ "`;
282
+
283
+ try {
284
+ await execAsync(cmd, { timeout: 15000 });
285
+ return 'Readiness tests passed: connectivity and migration version verified';
286
+ } catch (err) {
287
+ const msg = err instanceof Error ? err.message : String(err);
288
+ throw new Error(`Readiness tests failed: ${msg}`);
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Run the complete database setup pipeline
294
+ *
295
+ * Steps execute sequentially. Pipeline stops on first failure.
296
+ *
297
+ * @param projectDir - Project root directory
298
+ * @param options - Pipeline options
299
+ * @returns Full pipeline result
300
+ */
301
+ export async function runDbSetupPipeline(
302
+ projectDir: string,
303
+ options: SetupPipelineOptions = {}
304
+ ): Promise<SetupResult> {
305
+ const backendDir = resolveBackendDir(projectDir);
306
+ const steps: SetupStepResult[] = [];
307
+ const pipelineStart = Date.now();
308
+
309
+ // Step 1: Check connection
310
+ const connResult = await executeStep('check_connection', () => checkConnection(backendDir), options);
311
+ steps.push(connResult);
312
+ if (!connResult.success) {
313
+ return buildResult(steps, pipelineStart, 'error');
314
+ }
315
+
316
+ // Step 2: Ensure extensions
317
+ const extResult = await executeStep('ensure_extensions', () => ensureExtensions(backendDir), options);
318
+ steps.push(extResult);
319
+ if (!extResult.success) {
320
+ return buildResult(steps, pipelineStart, 'error');
321
+ }
322
+
323
+ // Step 3: Apply migrations
324
+ const migResult = await executeStep('apply_migrations', () => applyMigrations(backendDir), options);
325
+ steps.push(migResult);
326
+ if (!migResult.success) {
327
+ return buildResult(steps, pipelineStart, 'error');
328
+ }
329
+
330
+ // Step 4: Seed (optional)
331
+ if (!options.skipSeed) {
332
+ const seedResult = await executeStep('seed_minimal', () => seedMinimal(backendDir), options);
333
+ steps.push(seedResult);
334
+ if (!seedResult.success) {
335
+ return buildResult(steps, pipelineStart, 'error');
336
+ }
337
+ }
338
+
339
+ // Step 5: Readiness tests
340
+ const readyResult = await executeStep('readiness_tests', () => runReadinessTests(backendDir), options);
341
+ steps.push(readyResult);
342
+ if (!readyResult.success) {
343
+ return buildResult(steps, pipelineStart, 'error');
344
+ }
345
+
346
+ // Step 6: Mark ready (always succeeds if we got here)
347
+ const markResult = await executeStep('mark_ready', async () => {
348
+ return 'Database marked as ready';
349
+ }, options);
350
+ steps.push(markResult);
351
+
352
+ return buildResult(steps, pipelineStart, 'ready');
353
+ }
354
+
355
+ /**
356
+ * Build a SetupResult from accumulated steps
357
+ */
358
+ function buildResult(
359
+ steps: SetupStepResult[],
360
+ pipelineStart: number,
361
+ finalStatus: DbStatus
362
+ ): SetupResult {
363
+ const totalDurationMs = Date.now() - pipelineStart;
364
+ const success = finalStatus === 'ready';
365
+ const failedStep = steps.find((s) => !s.success);
366
+
367
+ return {
368
+ success,
369
+ steps,
370
+ totalDurationMs,
371
+ finalStatus,
372
+ error: failedStep?.error,
373
+ };
374
+ }
375
+
376
+ /**
377
+ * Compute the new DB status after a pipeline run and validate the transition
378
+ *
379
+ * @param currentStatus - Current DB status from state
380
+ * @param pipelineResult - Result of the pipeline run
381
+ * @returns New DB status after validated transition
382
+ */
383
+ export function computePostPipelineStatus(
384
+ currentStatus: DbStatus,
385
+ pipelineResult: SetupResult
386
+ ): DbStatus {
387
+ // Transition to 'applying' first
388
+ const applying = transitionDbStatus(currentStatus, 'applying');
389
+ // Then transition to final status
390
+ return transitionDbStatus(applying, pipelineResult.finalStatus);
391
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Database lifecycle state machine
3
+ * Enforces valid transitions between DB status states
4
+ */
5
+
6
+ import type { DbStatus } from '../types/database.js';
7
+
8
+ /**
9
+ * Valid state transitions map
10
+ * Each key maps to an array of valid target states
11
+ */
12
+ const VALID_TRANSITIONS: Record<DbStatus, DbStatus[]> = {
13
+ unconfigured: ['configured'],
14
+ configured: ['applying', 'unconfigured'],
15
+ applying: ['ready', 'error'],
16
+ ready: ['configured', 'unconfigured'],
17
+ error: ['configured', 'unconfigured'],
18
+ };
19
+
20
+ /**
21
+ * Check if a transition from current to target is valid
22
+ *
23
+ * @param current - Current DB status
24
+ * @param target - Target DB status
25
+ * @returns True if the transition is allowed
26
+ */
27
+ export function canTransition(current: DbStatus, target: DbStatus): boolean {
28
+ const allowed = VALID_TRANSITIONS[current];
29
+ return allowed !== undefined && allowed.includes(target);
30
+ }
31
+
32
+ /**
33
+ * Validate and execute a state transition
34
+ *
35
+ * @param current - Current DB status
36
+ * @param target - Desired target status
37
+ * @returns The new status
38
+ * @throws Error if the transition is invalid
39
+ */
40
+ export function transitionDbStatus(current: DbStatus, target: DbStatus): DbStatus {
41
+ if (!canTransition(current, target)) {
42
+ throw new Error(
43
+ `Invalid DB status transition: '${current}' -> '${target}'. ` +
44
+ `Allowed transitions from '${current}': [${getAvailableTransitions(current).join(', ')}]`
45
+ );
46
+ }
47
+ return target;
48
+ }
49
+
50
+ /**
51
+ * Get the list of valid next states from the current status
52
+ *
53
+ * @param current - Current DB status
54
+ * @returns Array of valid target states
55
+ */
56
+ export function getAvailableTransitions(current: DbStatus): DbStatus[] {
57
+ return VALID_TRANSITIONS[current] || [];
58
+ }
@@ -865,10 +865,10 @@ ${task.name}
865
865
  ## Description
866
866
  ${task.description || task.name}
867
867
 
868
- ${task.testPlan ? `## Test Requirements\n${task.testPlan}\n` : ''}
868
+ ${task.qaTestPlanText ? `## QA Test Plan\nImplement tests according to this approved test plan:\n${task.qaTestPlanText}\n` : ''}
869
869
 
870
870
  Please implement this task completely. After implementing:
871
- 1. Create appropriate tests if needed
871
+ 1. Implement tests as specified in the QA Test Plan above
872
872
  2. Ensure code follows best practices
873
873
  3. Document any complex logic
874
874
  `.trim();
@@ -48,12 +48,15 @@ export * from './project-structure.js';
48
48
  export * from './separation-guard.js';
49
49
  export * from './seo-tests.js';
50
50
  export * from './task-workflow.js';
51
+ export * from './tester.js';
51
52
  export * from './milestone-workflow.js';
52
53
  export * from './plan-storage.js';
53
54
  export * from './workspace-manager.js';
54
55
  export * from './website-updater.js';
55
56
  export * from './website-strategy.js';
56
57
  export * from './overview.js';
58
+ export * from './db-state-machine.js';
59
+ export * from './db-setup-runner.js';
57
60
 
58
61
  /**
59
62
  * Workflow options