tryassay 0.21.0 → 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.
@@ -1,5 +1,5 @@
1
1
  import { EventEmitter } from 'node:events';
2
- import type { AppDescription, AppCreateOptions, AppCreateResult, PlanSummary, PlanApprovalResult } from './types.js';
2
+ import type { AppDescription, AppCreateOptions, AppCreateResult, PlanSummary, PlanApprovalResult, PlanAnswer } from './types.js';
3
3
  export declare class AppCreateOrchestrator extends EventEmitter {
4
4
  private description;
5
5
  private options;
@@ -7,11 +7,14 @@ export declare class AppCreateOrchestrator extends EventEmitter {
7
7
  private startTime;
8
8
  private planSummaryCache;
9
9
  private approvalResolve;
10
+ private answersResolve;
10
11
  constructor(description: AppDescription, options: AppCreateOptions);
11
12
  /** Get the current plan summary (available after planning phase). */
12
13
  getPlanSummary(): PlanSummary | null;
13
14
  /** Approve or reject the plan. Resolves the blocked pipeline. */
14
15
  approvePlan(result: PlanApprovalResult): void;
16
+ /** Submit answers to plan questions. Resolves the waiting questioning loop. */
17
+ submitAnswers(answers: PlanAnswer[]): void;
15
18
  /** Run the full app creation pipeline. */
16
19
  run(): Promise<AppCreateResult>;
17
20
  private runPlanningPhase;
@@ -68,4 +71,39 @@ export declare class AppCreateOrchestrator extends EventEmitter {
68
71
  private pageToFilePath;
69
72
  private fileExists;
70
73
  private searchForSchemaEntity;
74
+ /**
75
+ * Scan generated page files for href/Link targets and verify
76
+ * each internal route has a corresponding page file.
77
+ * Catches hallucinated links like href="/categories" when
78
+ * no /categories/page.tsx exists.
79
+ */
80
+ private checkLinkIntegrity;
81
+ /**
82
+ * Check A1: Verify all import paths resolve to actual files.
83
+ * Catches: broken relative imports, unresolvable @/ aliases.
84
+ */
85
+ private checkImportResolution;
86
+ /**
87
+ * Check A2: Verify all imported packages exist in package.json.
88
+ * Also detects invalid alias entries (e.g. "@/lib": "latest").
89
+ */
90
+ private checkPackageDepsComplete;
91
+ /**
92
+ * Check A3: Detect server components that fetch their own API routes (deadlock risk).
93
+ * Only applies to Next.js apps.
94
+ */
95
+ private checkNoSelfFetch;
96
+ /**
97
+ * Check A4: Verify TypeScript compiles without errors.
98
+ * Uses `npx tsc --noEmit` with 60s timeout.
99
+ */
100
+ private checkTypescriptCompiles;
101
+ /**
102
+ * Check A5: Verify all referenced environment variables are defined in .env files.
103
+ */
104
+ private checkEnvVarsComplete;
105
+ /**
106
+ * Check A6: Verify asset references (images, CSS, icons) point to existing files.
107
+ */
108
+ private checkAssetRefsValid;
71
109
  }
@@ -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';
@@ -39,6 +40,7 @@ export class AppCreateOrchestrator extends EventEmitter {
39
40
  startTime = 0;
40
41
  planSummaryCache = null;
41
42
  approvalResolve = null;
43
+ answersResolve = null;
42
44
  constructor(description, options) {
43
45
  super();
44
46
  this.description = description;
@@ -55,6 +57,13 @@ export class AppCreateOrchestrator extends EventEmitter {
55
57
  this.approvalResolve = null;
56
58
  }
57
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
+ }
58
67
  /** Run the full app creation pipeline. */
59
68
  async run() {
60
69
  this.startTime = Date.now();
@@ -64,8 +73,83 @@ export class AppCreateOrchestrator extends EventEmitter {
64
73
  await mkdir(projectPath, { recursive: true });
65
74
  await mkdir(join(projectPath, '.assay'), { recursive: true });
66
75
  this.audit('task_status_change', { phase: 'initializing', projectPath });
67
- // 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)
68
145
  this.emitPhase({ phase: 'planning' });
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
+ }
69
153
  let plan = await this.runPlanningPhase(projectPath);
70
154
  if (!plan) {
71
155
  this.emitPhase({ phase: 'failed', reason: 'Planning failed after retries' });
@@ -615,6 +699,27 @@ Include a .env.local file with placeholder values for all required environment v
615
699
  });
616
700
  }
617
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);
618
723
  const passedCount = checks.filter(c => c.verdict === 'PASS').length;
619
724
  const failedCount = checks.filter(c => c.verdict === 'FAIL').length;
620
725
  return {
@@ -1121,5 +1226,478 @@ The renderer process communicates with main via contextBridge APIs exposed in sr
1121
1226
  }
1122
1227
  return false;
1123
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
+ }
1124
1702
  }
1125
1703
  //# sourceMappingURL=app-create-orchestrator.js.map