tryassay 0.20.3 → 0.21.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.
Files changed (31) hide show
  1. package/dist/api/server.d.ts +4 -0
  2. package/dist/api/server.js +247 -5
  3. package/dist/api/server.js.map +1 -1
  4. package/dist/cli.js +3 -1
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commands/create.d.ts +2 -0
  7. package/dist/commands/create.js +74 -0
  8. package/dist/commands/create.js.map +1 -1
  9. package/dist/runtime/__tests__/check-loop.test.js +2 -2
  10. package/dist/runtime/__tests__/cross-verification-checks.test.d.ts +8 -0
  11. package/dist/runtime/__tests__/cross-verification-checks.test.js +365 -0
  12. package/dist/runtime/__tests__/cross-verification-checks.test.js.map +1 -0
  13. package/dist/runtime/agents/planner-agent.d.ts +18 -1
  14. package/dist/runtime/agents/planner-agent.js +162 -0
  15. package/dist/runtime/agents/planner-agent.js.map +1 -1
  16. package/dist/runtime/app-create-orchestrator.d.ts +46 -1
  17. package/dist/runtime/app-create-orchestrator.js +691 -7
  18. package/dist/runtime/app-create-orchestrator.js.map +1 -1
  19. package/dist/runtime/check-catalog.js +53 -0
  20. package/dist/runtime/check-catalog.js.map +1 -1
  21. package/dist/runtime/fs-helpers.d.ts +2 -0
  22. package/dist/runtime/fs-helpers.js +14 -0
  23. package/dist/runtime/fs-helpers.js.map +1 -1
  24. package/dist/runtime/functional-tester.d.ts +47 -0
  25. package/dist/runtime/functional-tester.js +775 -0
  26. package/dist/runtime/functional-tester.js.map +1 -0
  27. package/dist/runtime/plan-refiner.d.ts +14 -0
  28. package/dist/runtime/plan-refiner.js +160 -0
  29. package/dist/runtime/plan-refiner.js.map +1 -0
  30. package/dist/runtime/types.d.ts +126 -1
  31. package/package.json +1 -1
@@ -7,6 +7,7 @@ import { EventEmitter } from 'node:events';
7
7
  import { mkdir, writeFile, readFile, readdir, stat } from 'node:fs/promises';
8
8
  import { join, dirname } from 'node:path';
9
9
  import { randomUUID } from 'node:crypto';
10
+ import { execSync } from 'node:child_process';
10
11
  import { PlannerAgent } from './agents/planner-agent.js';
11
12
  import { CodeAgent } from './agents/code-agent.js';
12
13
  import { MessageBus } from './message-bus.js';
@@ -17,6 +18,8 @@ import { IntegrationVerifier } from './integration-verifier.js';
17
18
  import { CheckLoop } from './check-loop.js';
18
19
  import { CheckStore } from './check-store.js';
19
20
  import { FailureClassifier } from './failure-classifier.js';
21
+ import { PlanRefiner } from './plan-refiner.js';
22
+ import { FunctionalTester } from './functional-tester.js';
20
23
  // ── Default safety policy ───────────────────────────────────
21
24
  const DEFAULT_SAFETY = {
22
25
  formalOverridesConsensus: true,
@@ -35,11 +38,32 @@ export class AppCreateOrchestrator extends EventEmitter {
35
38
  options;
36
39
  auditTrail = [];
37
40
  startTime = 0;
41
+ planSummaryCache = null;
42
+ approvalResolve = null;
43
+ answersResolve = null;
38
44
  constructor(description, options) {
39
45
  super();
40
46
  this.description = description;
41
47
  this.options = options;
42
48
  }
49
+ /** Get the current plan summary (available after planning phase). */
50
+ getPlanSummary() {
51
+ return this.planSummaryCache;
52
+ }
53
+ /** Approve or reject the plan. Resolves the blocked pipeline. */
54
+ approvePlan(result) {
55
+ if (this.approvalResolve) {
56
+ this.approvalResolve(result);
57
+ this.approvalResolve = null;
58
+ }
59
+ }
60
+ /** Submit answers to plan questions. Resolves the waiting questioning loop. */
61
+ submitAnswers(answers) {
62
+ if (this.answersResolve) {
63
+ this.answersResolve(answers);
64
+ this.answersResolve = null;
65
+ }
66
+ }
43
67
  /** Run the full app creation pipeline. */
44
68
  async run() {
45
69
  this.startTime = Date.now();
@@ -49,19 +73,94 @@ export class AppCreateOrchestrator extends EventEmitter {
49
73
  await mkdir(projectPath, { recursive: true });
50
74
  await mkdir(join(projectPath, '.assay'), { recursive: true });
51
75
  this.audit('task_status_change', { phase: 'initializing', projectPath });
52
- // Phase: planning
76
+ // Phase: plan questioning (refine requirements BEFORE planning)
77
+ let refinedRequirements = '';
78
+ if (!this.options.skipPlanQuestions && !this.options.autoApprove) {
79
+ const messageBus = new MessageBus();
80
+ const planner = new PlannerAgent(messageBus, { model: this.options.models?.planner });
81
+ const maxRounds = 3;
82
+ let currentConfidence = 0;
83
+ const allAnswers = [];
84
+ for (let round = 1; round <= maxRounds; round++) {
85
+ // Generate questions from the user prompt (no plan yet)
86
+ let questions = [];
87
+ try {
88
+ questions = await planner.generateQuestions(this.description.description);
89
+ }
90
+ catch (err) {
91
+ this.audit('task_status_change', {
92
+ phase: 'plan_questioning',
93
+ round,
94
+ error: err instanceof Error ? err.message : String(err),
95
+ reason: 'question_generation_failed',
96
+ });
97
+ break; // Skip questioning, proceed to planning
98
+ }
99
+ if (questions.length === 0) {
100
+ this.audit('task_status_change', {
101
+ phase: 'plan_questioning',
102
+ round,
103
+ questionsGenerated: 0,
104
+ reason: 'no_questions',
105
+ });
106
+ break;
107
+ }
108
+ // Emit questions to frontend
109
+ this.emitPhase({
110
+ phase: 'plan_questioning',
111
+ questions,
112
+ round,
113
+ confidence: currentConfidence,
114
+ });
115
+ this.audit('task_status_change', {
116
+ phase: 'plan_questioning',
117
+ round,
118
+ questionsGenerated: questions.length,
119
+ });
120
+ // Wait for answers from frontend
121
+ const answers = await new Promise((resolve) => {
122
+ this.answersResolve = resolve;
123
+ });
124
+ allAnswers.push(...answers);
125
+ // Update confidence based on answers received
126
+ this.emitPhase({ phase: 'plan_refining', round });
127
+ currentConfidence = Math.min(0.5 + (allAnswers.length * 0.15), 1.0);
128
+ this.audit('task_status_change', {
129
+ phase: 'plan_refining',
130
+ round,
131
+ confidence: currentConfidence,
132
+ answersReceived: answers.length,
133
+ });
134
+ if (currentConfidence >= 0.95) {
135
+ break;
136
+ }
137
+ }
138
+ // Build refined requirements string from all answers
139
+ if (allAnswers.length > 0) {
140
+ refinedRequirements = '\n\nREFINED REQUIREMENTS (from user selections):\n' +
141
+ allAnswers.map(a => `- ${a.questionId}: ${a.selectedOptions.join(', ')}${a.customText ? ` (${a.customText})` : ''}`).join('\n');
142
+ }
143
+ }
144
+ // Phase: planning (now with refined requirements)
53
145
  this.emitPhase({ phase: 'planning' });
54
- const plan = await this.runPlanningPhase(projectPath);
146
+ // Enrich the description with user's answers before planning
147
+ if (refinedRequirements) {
148
+ this.description = {
149
+ ...this.description,
150
+ description: this.description.description + refinedRequirements,
151
+ };
152
+ }
153
+ let plan = await this.runPlanningPhase(projectPath);
55
154
  if (!plan) {
56
155
  this.emitPhase({ phase: 'failed', reason: 'Planning failed after retries' });
57
- return this.makeResult('failed', projectPath, null, [], null, null, null);
156
+ return this.makeResult('failed', projectPath, null, [], null, null, null, null);
58
157
  }
59
158
  // Phase: verifying_plan
60
159
  this.emitPhase({ phase: 'verifying_plan' });
61
160
  const planValid = this.verifyPlanStructure(plan);
62
161
  if (!planValid) {
63
162
  this.emitPhase({ phase: 'failed', reason: 'Plan verification failed — invalid structure' });
64
- return this.makeResult('failed', projectPath, plan, [], null, null, null);
163
+ return this.makeResult('failed', projectPath, plan, [], null, null, null, null);
65
164
  }
66
165
  this.audit('verification_completed', {
67
166
  phase: 'verifying_plan',
@@ -69,6 +168,57 @@ export class AppCreateOrchestrator extends EventEmitter {
69
168
  schema: plan.schema.length,
70
169
  apiRoutes: plan.apiRoutes.length,
71
170
  });
171
+ // Phase: requirements_refining (Plan Approval Gate)
172
+ {
173
+ this.emitPhase({ phase: 'requirements_refining' });
174
+ const refiner = new PlanRefiner(this.description);
175
+ const summary = refiner.generateSummary(plan);
176
+ this.planSummaryCache = summary;
177
+ if (this.options.autoApprove) {
178
+ // Auto-approve: validate heuristics, only pause if validation fails
179
+ const validation = refiner.validatePlan(plan);
180
+ this.audit('task_status_change', {
181
+ phase: 'requirements_refining',
182
+ mode: 'auto_approve',
183
+ valid: validation.valid,
184
+ warnings: validation.warnings,
185
+ });
186
+ if (!validation.valid) {
187
+ // Validation failed — fall through to interactive approval
188
+ this.emitPhase({ phase: 'awaiting_approval', planSummary: summary });
189
+ const approvalResult = await new Promise((resolve) => {
190
+ this.approvalResolve = resolve;
191
+ });
192
+ if (approvalResult.decision === 'rejected') {
193
+ this.emitPhase({ phase: 'failed', reason: approvalResult.reason });
194
+ return this.makeResult('failed', projectPath, plan, [], null, null, null, null);
195
+ }
196
+ if (approvalResult.decision === 'approved_with_modifications') {
197
+ plan = refiner.applyModifications(plan, approvalResult.modifications);
198
+ }
199
+ }
200
+ // Auto-approve with valid heuristics — proceed
201
+ }
202
+ else {
203
+ // Interactive mode: wait for human approval
204
+ this.emitPhase({ phase: 'awaiting_approval', planSummary: summary });
205
+ const approvalResult = await new Promise((resolve) => {
206
+ this.approvalResolve = resolve;
207
+ });
208
+ this.audit('task_status_change', {
209
+ phase: 'requirements_refining',
210
+ mode: 'interactive',
211
+ decision: approvalResult.decision,
212
+ });
213
+ if (approvalResult.decision === 'rejected') {
214
+ this.emitPhase({ phase: 'failed', reason: approvalResult.reason });
215
+ return this.makeResult('failed', projectPath, plan, [], null, null, null, null);
216
+ }
217
+ if (approvalResult.decision === 'approved_with_modifications') {
218
+ plan = refiner.applyModifications(plan, approvalResult.modifications);
219
+ }
220
+ }
221
+ }
72
222
  // Write plan to disk
73
223
  await writeFile(join(projectPath, '.assay', 'architecture-plan.json'), JSON.stringify(plan, null, 2), 'utf-8');
74
224
  // Phase: scaffolding
@@ -227,6 +377,36 @@ export class AppCreateOrchestrator extends EventEmitter {
227
377
  }
228
378
  }
229
379
  }
380
+ // Phase: functional testing (HTTP-level tests with repair loop)
381
+ let functionalTestResult = null;
382
+ if (!this.options.skipFunctionalTesting && !this.options.skipBuildVerification) {
383
+ const maxFunctionalAttempts = this.options.maxBuildRepairAttempts ?? 3;
384
+ this.emitPhase({ phase: 'functional_testing', attempt: 1, maxAttempts: maxFunctionalAttempts });
385
+ const functionalTester = new FunctionalTester(projectPath, plan, {
386
+ maxRepairAttempts: maxFunctionalAttempts,
387
+ codeModel: this.options.models?.code,
388
+ framework: this.description.techStack.framework,
389
+ });
390
+ functionalTestResult = await functionalTester.test();
391
+ // Emit repair phases if repairs happened
392
+ for (const repair of functionalTestResult.repairAttempts) {
393
+ this.emitPhase({
394
+ phase: 'functional_repairing',
395
+ attempt: repair.attempt,
396
+ maxAttempts: maxFunctionalAttempts,
397
+ failureCount: repair.failures.length,
398
+ });
399
+ }
400
+ this.audit('verification_completed', {
401
+ phase: 'functional_testing',
402
+ status: functionalTestResult.status,
403
+ passedCount: functionalTestResult.passedCount,
404
+ failedCount: functionalTestResult.failedCount,
405
+ repairAttempts: functionalTestResult.repairAttempts.length,
406
+ serverPort: functionalTestResult.serverPort,
407
+ totalDurationMs: functionalTestResult.totalDurationMs,
408
+ });
409
+ }
230
410
  // Phase: cross-verifying
231
411
  this.emitPhase({ phase: 'cross_verifying' });
232
412
  const crossVerification = await this.runCrossVerification(projectPath, plan);
@@ -241,9 +421,10 @@ export class AppCreateOrchestrator extends EventEmitter {
241
421
  const anyCompleted = featureResults.some(r => r.status === 'completed');
242
422
  const buildOk = !buildVerification || buildVerification.status === 'pass' || buildVerification.status === 'repaired' || buildVerification.status === 'skipped';
243
423
  const integrationOk = !integrationVerification || integrationVerification.verdict === 'PASS';
244
- const finalStatus = allCompleted && buildOk && integrationOk ? 'completed' : anyCompleted ? 'partial' : 'failed';
424
+ const functionalOk = !functionalTestResult || functionalTestResult.status === 'pass' || functionalTestResult.status === 'repaired' || functionalTestResult.status === 'skipped';
425
+ const finalStatus = allCompleted && buildOk && integrationOk && functionalOk ? 'completed' : anyCompleted ? 'partial' : 'failed';
245
426
  this.emitPhase({ phase: 'completed' });
246
- return this.makeResult(finalStatus, projectPath, plan, featureResults, buildVerification, integrationVerification, crossVerification);
427
+ return this.makeResult(finalStatus, projectPath, plan, featureResults, buildVerification, integrationVerification, crossVerification, functionalTestResult);
247
428
  }
248
429
  // ── Planning Phase ─────────────────────────────────────────
249
430
  async runPlanningPhase(projectPath) {
@@ -385,6 +566,7 @@ Include a .env.local file with placeholder values for all required environment v
385
566
  try {
386
567
  await mkdir(dirname(absPath), { recursive: true });
387
568
  await writeFile(absPath, artifact.content, 'utf-8');
569
+ this.audit('code_generated', { path: artifact.path, language: artifact.language || this.inferLanguage(artifact.path), content: artifact.content });
388
570
  }
389
571
  catch {
390
572
  // Best-effort write
@@ -431,6 +613,7 @@ Include a .env.local file with placeholder values for all required environment v
431
613
  await mkdir(dirname(absPath), { recursive: true });
432
614
  await writeFile(absPath, handoff.artifact.content, 'utf-8');
433
615
  filesCreated.push(path);
616
+ this.audit('code_generated', { path, language: handoff.artifact.language || this.inferLanguage(path), content: handoff.artifact.content });
434
617
  }
435
618
  catch {
436
619
  // Best-effort write
@@ -516,6 +699,27 @@ Include a .env.local file with placeholder values for all required environment v
516
699
  });
517
700
  }
518
701
  }
702
+ // Check 6: Link integrity — href targets in generated code resolve to actual pages
703
+ const linkChecks = await this.checkLinkIntegrity(projectPath, plan);
704
+ checks.push(...linkChecks);
705
+ // Check 7: Import paths resolve to actual files
706
+ const importChecks = await this.checkImportResolution(projectPath);
707
+ checks.push(...importChecks);
708
+ // Check 8: Package dependencies complete
709
+ const pkgChecks = await this.checkPackageDepsComplete(projectPath);
710
+ checks.push(...pkgChecks);
711
+ // Check 9: No self-fetch in server components
712
+ const selfFetchChecks = await this.checkNoSelfFetch(projectPath);
713
+ checks.push(...selfFetchChecks);
714
+ // Check 10: TypeScript compiles
715
+ const tsChecks = await this.checkTypescriptCompiles(projectPath);
716
+ checks.push(...tsChecks);
717
+ // Check 11: Environment variables complete
718
+ const envChecks = await this.checkEnvVarsComplete(projectPath);
719
+ checks.push(...envChecks);
720
+ // Check 12: Asset references valid
721
+ const assetChecks = await this.checkAssetRefsValid(projectPath);
722
+ checks.push(...assetChecks);
519
723
  const passedCount = checks.filter(c => c.verdict === 'PASS').length;
520
724
  const failedCount = checks.filter(c => c.verdict === 'FAIL').length;
521
725
  return {
@@ -599,6 +803,7 @@ Include a .env.local file with placeholder values for all required environment v
599
803
  await mkdir(dirname(absPath), { recursive: true });
600
804
  await writeFile(absPath, artifact.content, 'utf-8');
601
805
  filesCreated.push(artifact.path);
806
+ this.audit('code_generated', { path: artifact.path, language: artifact.language || this.inferLanguage(artifact.path), content: artifact.content });
602
807
  }
603
808
  catch {
604
809
  // Best-effort write
@@ -892,7 +1097,7 @@ ${importNote}`;
892
1097
  };
893
1098
  this.emit('progress', progress);
894
1099
  }
895
- makeResult(status, projectPath, plan, featureResults, buildVerification, integrationVerification, crossVerification) {
1100
+ makeResult(status, projectPath, plan, featureResults, buildVerification, integrationVerification, crossVerification, functionalTestResult) {
896
1101
  return {
897
1102
  status,
898
1103
  projectPath,
@@ -901,6 +1106,7 @@ ${importNote}`;
901
1106
  buildVerification,
902
1107
  integrationVerification,
903
1108
  crossVerification,
1109
+ functionalTestResult,
904
1110
  totalDurationMs: Date.now() - this.startTime,
905
1111
  auditTrail: this.auditTrail,
906
1112
  };
@@ -917,6 +1123,11 @@ ${importNote}`;
917
1123
  this.auditTrail.push(entry);
918
1124
  this.emit('audit', entry);
919
1125
  }
1126
+ inferLanguage(filePath) {
1127
+ const ext = filePath.split('.').pop()?.toLowerCase() || '';
1128
+ const map = { ts: 'typescript', tsx: 'typescript', js: 'javascript', jsx: 'javascript', json: 'json', css: 'css', html: 'html', md: 'markdown', sql: 'sql', py: 'python' };
1129
+ return map[ext] || 'text';
1130
+ }
920
1131
  slugify(name) {
921
1132
  return name
922
1133
  .toLowerCase()
@@ -1015,5 +1226,478 @@ The renderer process communicates with main via contextBridge APIs exposed in sr
1015
1226
  }
1016
1227
  return false;
1017
1228
  }
1229
+ // ── Link Integrity Check ──────────────────────────────────
1230
+ /**
1231
+ * Scan generated page files for href/Link targets and verify
1232
+ * each internal route has a corresponding page file.
1233
+ * Catches hallucinated links like href="/categories" when
1234
+ * no /categories/page.tsx exists.
1235
+ */
1236
+ async checkLinkIntegrity(projectPath, plan) {
1237
+ const checks = [];
1238
+ const framework = this.description.techStack.framework.toLowerCase();
1239
+ // Build set of known routes from the plan
1240
+ const knownRoutes = new Set();
1241
+ for (const page of plan.pages) {
1242
+ knownRoutes.add(page.path);
1243
+ }
1244
+ // Collect all page files to scan
1245
+ const pageFiles = [];
1246
+ for (const page of plan.pages) {
1247
+ const filePath = this.pageToFilePath(page.path, page.component, this.description.techStack.framework);
1248
+ pageFiles.push(join(projectPath, filePath));
1249
+ }
1250
+ // Also scan layout files which often contain nav links
1251
+ const layoutCandidates = framework.includes('next')
1252
+ ? ['app/layout.tsx', 'app/layout.jsx', 'app/page.tsx', 'app/page.jsx',
1253
+ 'components/nav.tsx', 'components/navbar.tsx', 'components/header.tsx',
1254
+ 'components/sidebar.tsx', 'components/layout.tsx',
1255
+ 'src/components/nav.tsx', 'src/components/navbar.tsx', 'src/components/header.tsx']
1256
+ : ['src/App.tsx', 'src/App.jsx', 'src/components/Nav.tsx', 'src/components/Navbar.tsx',
1257
+ 'src/components/Header.tsx', 'src/components/Sidebar.tsx', 'src/components/Layout.tsx'];
1258
+ for (const candidate of layoutCandidates) {
1259
+ pageFiles.push(join(projectPath, candidate));
1260
+ }
1261
+ // Regex patterns to extract internal href targets
1262
+ // Matches: href="/path", href='/path', to="/path", to='/path'
1263
+ const hrefPattern = /(?:href|to)=["'](\/([\w\-/]*)?)["']/g;
1264
+ // Also match Next.js Link component: <Link href="/path">
1265
+ const linkPattern = /<Link[^>]*\s+href=["'](\/([\w\-/]*)?)["']/g;
1266
+ const seenLinks = new Set();
1267
+ for (const filePath of pageFiles) {
1268
+ let content;
1269
+ try {
1270
+ content = await readFile(filePath, 'utf-8');
1271
+ }
1272
+ catch {
1273
+ continue; // File doesn't exist, skip
1274
+ }
1275
+ // Extract all href targets
1276
+ for (const pattern of [hrefPattern, linkPattern]) {
1277
+ pattern.lastIndex = 0; // Reset regex state
1278
+ let match;
1279
+ while ((match = pattern.exec(content)) !== null) {
1280
+ const target = match[1];
1281
+ if (!target || target === '/' || target === '#')
1282
+ continue;
1283
+ // Skip external links, anchors, api routes, auth routes
1284
+ if (target.startsWith('/api/') || target.startsWith('/auth/'))
1285
+ continue;
1286
+ if (target.includes('://') || target.startsWith('mailto:'))
1287
+ continue;
1288
+ // Normalize: strip trailing slash
1289
+ const normalized = target.endsWith('/') ? target.slice(0, -1) : target;
1290
+ if (seenLinks.has(normalized))
1291
+ continue;
1292
+ seenLinks.add(normalized);
1293
+ // Check if this route has a page file
1294
+ const expectedPath = this.pageToFilePath(normalized, '', this.description.techStack.framework);
1295
+ const exists = await this.fileExists(join(projectPath, expectedPath));
1296
+ // Also check if it might be a dynamic route (e.g., /categories/[slug])
1297
+ // by looking for [param] directories
1298
+ let dynamicMatch = false;
1299
+ if (!exists && framework.includes('next')) {
1300
+ // For /categories, check if app/categories/page.tsx OR app/categories/[slug]/page.tsx exists
1301
+ // The link might be to a listing page that was never created
1302
+ const segments = normalized.split('/').filter(Boolean);
1303
+ if (segments.length === 1) {
1304
+ // Single-segment route like /categories — check if only a dynamic child exists
1305
+ try {
1306
+ const dirPath = join(projectPath, 'app', segments[0]);
1307
+ const entries = await readdir(dirPath);
1308
+ const hasDynamicChild = entries.some(e => e.startsWith('['));
1309
+ if (hasDynamicChild && !entries.includes('page.tsx') && !entries.includes('page.jsx')) {
1310
+ // Directory exists with dynamic routes but no index page — this IS the bug
1311
+ dynamicMatch = false; // Intentionally false: missing index page is a real failure
1312
+ }
1313
+ }
1314
+ catch {
1315
+ // Directory doesn't exist at all
1316
+ }
1317
+ }
1318
+ }
1319
+ const passed = exists || dynamicMatch;
1320
+ const sourceFile = filePath.replace(projectPath + '/', '');
1321
+ checks.push({
1322
+ type: 'link_integrity',
1323
+ description: `Link href="${normalized}" in ${sourceFile} resolves to a page`,
1324
+ verdict: passed ? 'PASS' : 'FAIL',
1325
+ evidence: passed
1326
+ ? `Page exists: ${expectedPath}`
1327
+ : `No page at ${expectedPath} — link target has no route handler`,
1328
+ });
1329
+ }
1330
+ }
1331
+ }
1332
+ return checks;
1333
+ }
1334
+ /**
1335
+ * Check A1: Verify all import paths resolve to actual files.
1336
+ * Catches: broken relative imports, unresolvable @/ aliases.
1337
+ */
1338
+ async checkImportResolution(projectPath) {
1339
+ const checks = [];
1340
+ const sourceFiles = await this.collectFiles(projectPath, ['.ts', '.tsx', '.js', '.jsx']);
1341
+ // Read tsconfig paths for alias resolution
1342
+ const { readTsConfigPaths } = await import('./fs-helpers.js');
1343
+ const paths = await readTsConfigPaths(projectPath);
1344
+ for (const filePath of sourceFiles) {
1345
+ let content;
1346
+ try {
1347
+ content = await readFile(filePath, 'utf-8');
1348
+ }
1349
+ catch {
1350
+ continue;
1351
+ }
1352
+ const importRegex = /(?:import\s+(?:[\s\S]*?)\s+from\s+|import\s+|require\s*\(\s*)['"]([^'"]+)['"]/g;
1353
+ let match;
1354
+ while ((match = importRegex.exec(content)) !== null) {
1355
+ const importPath = match[1];
1356
+ // Skip external packages (no . or @ prefix, or @scoped packages)
1357
+ if (!importPath.startsWith('.') && !importPath.startsWith('@/') && !importPath.startsWith('~/'))
1358
+ continue;
1359
+ // Skip @scoped npm packages like @supabase/ssr
1360
+ if (importPath.startsWith('@') && !importPath.startsWith('@/'))
1361
+ continue;
1362
+ const relFile = filePath.replace(projectPath + '/', '');
1363
+ if (importPath.startsWith('.')) {
1364
+ // Relative import — use resolveImport from fs-helpers
1365
+ const { resolveImport } = await import('./fs-helpers.js');
1366
+ const resolved = resolveImport(filePath, importPath, sourceFiles);
1367
+ if (!resolved) {
1368
+ checks.push({
1369
+ type: 'import_path_valid',
1370
+ description: `Import '${importPath}' in ${relFile} resolves to file`,
1371
+ verdict: 'FAIL',
1372
+ evidence: `Import '${importPath}' in ${relFile} does not resolve to any file`,
1373
+ });
1374
+ }
1375
+ }
1376
+ else if (importPath.startsWith('@/') || importPath.startsWith('~/')) {
1377
+ // Alias import — resolve via tsconfig paths
1378
+ let resolved = false;
1379
+ const prefix = importPath.startsWith('@/') ? '@/*' : '~/*';
1380
+ const aliasTargets = paths[prefix];
1381
+ if (aliasTargets && aliasTargets.length > 0) {
1382
+ const suffix = importPath.slice(2); // strip @/ or ~/
1383
+ for (const target of aliasTargets) {
1384
+ const targetDir = target.replace('*', '');
1385
+ const base = join(projectPath, targetDir, suffix);
1386
+ const extensions = ['', '.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.tsx', '/index.js', '/index.jsx'];
1387
+ for (const ext of extensions) {
1388
+ try {
1389
+ await stat(base + ext);
1390
+ resolved = true;
1391
+ break;
1392
+ }
1393
+ catch {
1394
+ // continue
1395
+ }
1396
+ }
1397
+ if (resolved)
1398
+ break;
1399
+ }
1400
+ }
1401
+ if (!resolved) {
1402
+ checks.push({
1403
+ type: 'import_path_valid',
1404
+ description: `Import '${importPath}' in ${relFile} resolves via tsconfig alias`,
1405
+ verdict: 'FAIL',
1406
+ evidence: `Import '${importPath}' in ${relFile} — alias not configured or target missing`,
1407
+ });
1408
+ }
1409
+ }
1410
+ }
1411
+ }
1412
+ return checks;
1413
+ }
1414
+ /**
1415
+ * Check A2: Verify all imported packages exist in package.json.
1416
+ * Also detects invalid alias entries (e.g. "@/lib": "latest").
1417
+ */
1418
+ async checkPackageDepsComplete(projectPath) {
1419
+ const checks = [];
1420
+ const pkgPath = join(projectPath, 'package.json');
1421
+ let pkg;
1422
+ try {
1423
+ pkg = JSON.parse(await readFile(pkgPath, 'utf-8'));
1424
+ }
1425
+ catch {
1426
+ return checks; // No package.json
1427
+ }
1428
+ const deps = (pkg.dependencies ?? {});
1429
+ const devDeps = (pkg.devDependencies ?? {});
1430
+ const allDeps = new Set([...Object.keys(deps), ...Object.keys(devDeps)]);
1431
+ // Check for invalid alias entries in package.json
1432
+ for (const key of allDeps) {
1433
+ if (key.startsWith('@/') || key.startsWith('./') || key.startsWith('~/')) {
1434
+ checks.push({
1435
+ type: 'package_deps_complete',
1436
+ description: `Package "${key}" is a valid npm package name`,
1437
+ verdict: 'FAIL',
1438
+ evidence: `"${key}" in package.json is a path alias, not a valid npm package`,
1439
+ });
1440
+ }
1441
+ }
1442
+ // Collect imports from source files
1443
+ const sourceFiles = await this.collectFiles(projectPath, ['.ts', '.tsx', '.js', '.jsx']);
1444
+ const importedPackages = new Set();
1445
+ for (const filePath of sourceFiles) {
1446
+ try {
1447
+ const content = await readFile(filePath, 'utf-8');
1448
+ const importRegex = /(?:import\s+.*?\s+from\s+['"]|import\s+['"]|require\s*\(\s*['"])([^./~][^'"]*)['"]/g;
1449
+ let match;
1450
+ while ((match = importRegex.exec(content)) !== null) {
1451
+ const raw = match[1];
1452
+ // Skip @/ aliases — they're not packages
1453
+ if (raw.startsWith('@/'))
1454
+ continue;
1455
+ const pkgName = raw.startsWith('@')
1456
+ ? raw.split('/').slice(0, 2).join('/')
1457
+ : raw.split('/')[0];
1458
+ importedPackages.add(pkgName);
1459
+ }
1460
+ }
1461
+ catch {
1462
+ continue;
1463
+ }
1464
+ }
1465
+ const builtins = new Set([
1466
+ 'node:events', 'node:fs', 'node:path', 'node:crypto', 'node:url', 'node:http', 'node:https',
1467
+ 'node:stream', 'node:util', 'node:os', 'node:child_process', 'node:buffer', 'node:assert',
1468
+ 'events', 'fs', 'path', 'crypto', 'url', 'http', 'https', 'stream', 'util', 'os',
1469
+ 'child_process', 'buffer', 'assert', 'fs/promises',
1470
+ 'react', 'react-dom', 'react-dom/client', 'next', 'electron',
1471
+ ]);
1472
+ for (const importedPkg of importedPackages) {
1473
+ if (builtins.has(importedPkg))
1474
+ continue;
1475
+ if (allDeps.has(importedPkg))
1476
+ continue;
1477
+ if (importedPkg.startsWith('next/'))
1478
+ continue;
1479
+ checks.push({
1480
+ type: 'package_deps_complete',
1481
+ description: `Package "${importedPkg}" is listed in package.json`,
1482
+ verdict: 'FAIL',
1483
+ evidence: `"${importedPkg}" is imported but not in package.json dependencies`,
1484
+ });
1485
+ }
1486
+ return checks;
1487
+ }
1488
+ /**
1489
+ * Check A3: Detect server components that fetch their own API routes (deadlock risk).
1490
+ * Only applies to Next.js apps.
1491
+ */
1492
+ async checkNoSelfFetch(projectPath) {
1493
+ const checks = [];
1494
+ const framework = this.description.techStack.framework.toLowerCase();
1495
+ if (!framework.includes('next'))
1496
+ return checks;
1497
+ const appDir = join(projectPath, 'app');
1498
+ let appFiles;
1499
+ try {
1500
+ appFiles = await this.collectFiles(appDir, ['.ts', '.tsx', '.js', '.jsx']);
1501
+ }
1502
+ catch {
1503
+ return checks;
1504
+ }
1505
+ const selfFetchPattern = /fetch\s*\(\s*['"`](?:\/api\/|https?:\/\/localhost)/;
1506
+ for (const filePath of appFiles) {
1507
+ let content;
1508
+ try {
1509
+ content = await readFile(filePath, 'utf-8');
1510
+ }
1511
+ catch {
1512
+ continue;
1513
+ }
1514
+ // Client components can self-fetch — skip
1515
+ if (content.includes("'use client'") || content.includes('"use client"'))
1516
+ continue;
1517
+ if (selfFetchPattern.test(content)) {
1518
+ const relFile = filePath.replace(projectPath + '/', '');
1519
+ checks.push({
1520
+ type: 'no_self_fetch',
1521
+ description: `Server component ${relFile} does not self-fetch API routes`,
1522
+ verdict: 'FAIL',
1523
+ evidence: `${relFile} is a server component that fetches /api/ or localhost — this causes a deadlock in Next.js`,
1524
+ });
1525
+ }
1526
+ }
1527
+ return checks;
1528
+ }
1529
+ /**
1530
+ * Check A4: Verify TypeScript compiles without errors.
1531
+ * Uses `npx tsc --noEmit` with 60s timeout.
1532
+ */
1533
+ async checkTypescriptCompiles(projectPath) {
1534
+ const tsconfigPath = join(projectPath, 'tsconfig.json');
1535
+ try {
1536
+ await stat(tsconfigPath);
1537
+ }
1538
+ catch {
1539
+ return []; // No tsconfig — skip
1540
+ }
1541
+ try {
1542
+ execSync('npx tsc --noEmit --pretty false', {
1543
+ cwd: projectPath,
1544
+ timeout: 60_000,
1545
+ stdio: ['pipe', 'pipe', 'pipe'],
1546
+ });
1547
+ return [{
1548
+ type: 'typescript_compiles',
1549
+ description: 'TypeScript compiles without errors',
1550
+ verdict: 'PASS',
1551
+ evidence: 'npx tsc --noEmit exited with code 0',
1552
+ }];
1553
+ }
1554
+ catch (err) {
1555
+ const stderr = err?.stderr?.toString() ?? '';
1556
+ const stdout = err?.stdout?.toString() ?? '';
1557
+ const output = stderr || stdout;
1558
+ const errorLines = output.split('\n').filter(l => l.includes('error TS'));
1559
+ return [{
1560
+ type: 'typescript_compiles',
1561
+ description: 'TypeScript compiles without errors',
1562
+ verdict: 'FAIL',
1563
+ evidence: errorLines.length > 0
1564
+ ? `tsc --noEmit failed with ${errorLines.length} error(s): ${errorLines.slice(0, 5).join('; ')}${errorLines.length > 5 ? ` ... and ${errorLines.length - 5} more` : ''}`
1565
+ : `tsc --noEmit failed: ${output.slice(0, 500)}`,
1566
+ }];
1567
+ }
1568
+ }
1569
+ /**
1570
+ * Check A5: Verify all referenced environment variables are defined in .env files.
1571
+ */
1572
+ async checkEnvVarsComplete(projectPath) {
1573
+ const checks = [];
1574
+ const sourceFiles = await this.collectFiles(projectPath, ['.ts', '.tsx', '.js', '.jsx']);
1575
+ // Standard exclusions — always available at runtime
1576
+ const standardVars = new Set([
1577
+ 'NODE_ENV', 'PORT', 'HOME', 'PATH', 'PWD', 'USER', 'CI',
1578
+ 'HOSTNAME', 'VERCEL', 'NEXT_RUNTIME', 'NODE_PATH', 'NODE_OPTIONS',
1579
+ 'VERCEL_URL', 'NEXT_PUBLIC_VERCEL_URL', 'NEXT_PUBLIC_VERCEL_ENV',
1580
+ ]);
1581
+ // Collect all referenced env vars
1582
+ const referencedVars = new Set();
1583
+ const processEnvPattern = /process\.env\.([A-Z_][A-Z0-9_]+)/g;
1584
+ const importMetaPattern = /import\.meta\.env\.([A-Z_][A-Z0-9_]+)/g;
1585
+ for (const filePath of sourceFiles) {
1586
+ try {
1587
+ const content = await readFile(filePath, 'utf-8');
1588
+ let match;
1589
+ while ((match = processEnvPattern.exec(content)) !== null) {
1590
+ referencedVars.add(match[1]);
1591
+ }
1592
+ processEnvPattern.lastIndex = 0;
1593
+ while ((match = importMetaPattern.exec(content)) !== null) {
1594
+ referencedVars.add(match[1]);
1595
+ }
1596
+ importMetaPattern.lastIndex = 0;
1597
+ }
1598
+ catch {
1599
+ continue;
1600
+ }
1601
+ }
1602
+ // Collect defined env vars from all .env* files
1603
+ const definedVars = new Set();
1604
+ try {
1605
+ const entries = await readdir(projectPath);
1606
+ for (const entry of entries) {
1607
+ if (entry.startsWith('.env')) {
1608
+ try {
1609
+ const content = await readFile(join(projectPath, entry), 'utf-8');
1610
+ for (const line of content.split('\n')) {
1611
+ const trimmed = line.trim();
1612
+ if (trimmed && !trimmed.startsWith('#')) {
1613
+ const eqIdx = trimmed.indexOf('=');
1614
+ if (eqIdx > 0) {
1615
+ definedVars.add(trimmed.slice(0, eqIdx).trim());
1616
+ }
1617
+ }
1618
+ }
1619
+ }
1620
+ catch {
1621
+ continue;
1622
+ }
1623
+ }
1624
+ }
1625
+ }
1626
+ catch {
1627
+ // Can't read directory
1628
+ }
1629
+ for (const varName of referencedVars) {
1630
+ if (standardVars.has(varName))
1631
+ continue;
1632
+ if (definedVars.has(varName))
1633
+ continue;
1634
+ checks.push({
1635
+ type: 'env_vars_complete',
1636
+ description: `Environment variable ${varName} is defined in .env file`,
1637
+ verdict: 'FAIL',
1638
+ evidence: `${varName} is referenced in source code but not defined in any .env* file`,
1639
+ });
1640
+ }
1641
+ return checks;
1642
+ }
1643
+ /**
1644
+ * Check A6: Verify asset references (images, CSS, icons) point to existing files.
1645
+ */
1646
+ async checkAssetRefsValid(projectPath) {
1647
+ const checks = [];
1648
+ const framework = this.description.techStack.framework.toLowerCase();
1649
+ // Determine public asset directory
1650
+ let publicDir;
1651
+ if (framework.includes('electron')) {
1652
+ publicDir = join(projectPath, 'src', 'renderer', 'assets');
1653
+ }
1654
+ else {
1655
+ publicDir = join(projectPath, 'public');
1656
+ }
1657
+ const sourceFiles = await this.collectFiles(projectPath, ['.ts', '.tsx', '.js', '.jsx', '.html', '.css']);
1658
+ // Patterns for asset references
1659
+ const assetPatterns = [
1660
+ /(?:src|href)=["'](\/[^"']+\.(?:png|jpg|jpeg|gif|svg|webp|ico|css))["']/g,
1661
+ /url\(["']?(\/[^"')]+\.(?:png|jpg|jpeg|gif|svg|webp|ico|woff2?|ttf|eot))["']?\)/g,
1662
+ ];
1663
+ const checkedPaths = new Set();
1664
+ for (const filePath of sourceFiles) {
1665
+ let content;
1666
+ try {
1667
+ content = await readFile(filePath, 'utf-8');
1668
+ }
1669
+ catch {
1670
+ continue;
1671
+ }
1672
+ for (const pattern of assetPatterns) {
1673
+ pattern.lastIndex = 0;
1674
+ let match;
1675
+ while ((match = pattern.exec(content)) !== null) {
1676
+ const assetPath = match[1];
1677
+ // Skip external URLs and data URIs
1678
+ if (assetPath.includes('://') || assetPath.startsWith('//') || assetPath.startsWith('data:'))
1679
+ continue;
1680
+ if (checkedPaths.has(assetPath))
1681
+ continue;
1682
+ checkedPaths.add(assetPath);
1683
+ // Resolve against public directory (strip leading /)
1684
+ const resolved = join(publicDir, assetPath.slice(1));
1685
+ try {
1686
+ await stat(resolved);
1687
+ }
1688
+ catch {
1689
+ const relFile = filePath.replace(projectPath + '/', '');
1690
+ checks.push({
1691
+ type: 'asset_refs_valid',
1692
+ description: `Asset ${assetPath} referenced in ${relFile} exists`,
1693
+ verdict: 'FAIL',
1694
+ evidence: `${assetPath} referenced in ${relFile} not found at ${resolved.replace(projectPath + '/', '')}`,
1695
+ });
1696
+ }
1697
+ }
1698
+ }
1699
+ }
1700
+ return checks;
1701
+ }
1018
1702
  }
1019
1703
  //# sourceMappingURL=app-create-orchestrator.js.map