thachvd-kit 1.0.3 → 1.0.5

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.
package/README.md CHANGED
@@ -17,6 +17,7 @@ npx github:thachvd/thachvd-kit init
17
17
 
18
18
  The interactive wizard will first ask whether this is a new project or an existing one.
19
19
  If it is an existing project, it can scan the current codebase to prefill `PROJECT_CONTEXT.md`, or let you enter everything manually.
20
+ The scan mode uses evidence-based detectors and shows why a framework was inferred, so you can quickly confirm or correct it.
20
21
 
21
22
  ```bash
22
23
  thachvd-kit init --scan
package/bin/cli.js CHANGED
@@ -11,18 +11,19 @@ const targetDir = process.cwd();
11
11
 
12
12
  // --- Skill Resolver ---
13
13
 
14
- const LANG_SKILLS = {
15
- javascript: ["nodejs-best-practices", "api-design"],
16
- typescript: ["nodejs-best-practices", "api-design"],
17
- python: ["python-patterns"],
18
- go: ["golang-patterns", "golang-testing"],
19
- php: ["laravel-patterns", "laravel-security", "laravel-tdd"],
20
- rust: ["rust-pro"],
21
- java: ["api-design"],
22
- swift: ["mobile-design"],
23
- kotlin: ["mobile-design"],
24
- csharp: ["api-design"],
25
- };
14
+ const LANG_SKILLS = {
15
+ javascript: ["nodejs-best-practices", "api-design"],
16
+ typescript: ["nodejs-best-practices", "api-design"],
17
+ python: ["python-patterns"],
18
+ go: ["golang-patterns", "golang-testing"],
19
+ php: ["api-design"],
20
+ rust: ["rust-pro"],
21
+ java: ["api-design"],
22
+ ruby: ["api-design"],
23
+ swift: ["mobile-design"],
24
+ kotlin: ["mobile-design"],
25
+ csharp: ["api-design"],
26
+ };
26
27
 
27
28
  const FRAMEWORK_SKILLS = {
28
29
  vue: ["frontend-design", "tailwind-patterns"],
@@ -30,14 +31,25 @@ const FRAMEWORK_SKILLS = {
30
31
  next: ["nextjs-react-expert", "frontend-design"],
31
32
  nuxt: ["frontend-design"],
32
33
  svelte: ["frontend-design"],
33
- laravel: ["laravel-patterns", "laravel-security", "laravel-tdd"],
34
- express: ["nodejs-best-practices", "api-design"],
35
- fastapi: ["python-patterns", "api-design"],
36
- django: ["python-patterns", "api-design"],
37
- gin: ["golang-patterns", "golang-testing"],
38
- fiber: ["golang-patterns", "golang-testing"],
39
- nestjs: ["nodejs-best-practices", "api-design"],
34
+ angular: ["frontend-design"],
35
+ laravel: ["laravel-patterns", "laravel-security", "laravel-tdd"],
36
+ cakephp: ["api-design"],
37
+ symfony: ["api-design"],
38
+ codeigniter: ["api-design"],
39
+ express: ["nodejs-best-practices", "api-design"],
40
+ fastapi: ["python-patterns", "api-design"],
41
+ django: ["python-patterns", "api-design"],
42
+ flask: ["python-patterns", "api-design"],
43
+ gin: ["golang-patterns", "golang-testing"],
44
+ fiber: ["golang-patterns", "golang-testing"],
45
+ echo: ["golang-patterns", "golang-testing"],
46
+ nestjs: ["nodejs-best-practices", "api-design"],
47
+ rails: ["api-design"],
48
+ spring: ["api-design"],
49
+ aspnet: ["api-design"],
40
50
  tauri: ["rust-pro", "desktop-design"],
51
+ axum: ["rust-pro", "api-design"],
52
+ actix: ["rust-pro", "api-design"],
41
53
  flutter: ["mobile-design"],
42
54
  "react-native": ["mobile-design"],
43
55
  };
@@ -80,16 +92,21 @@ function resolveSkills(data) {
80
92
 
81
93
  const FRAMEWORK_CHOICES = [
82
94
  'vue', 'react', 'next', 'nuxt', 'svelte', 'angular',
83
- 'laravel', 'express', 'fastapi', 'django', 'gin', 'fiber', 'nestjs',
84
- 'tauri', 'flutter', 'react-native'
95
+ 'laravel', 'cakephp', 'symfony', 'codeigniter',
96
+ 'express', 'fastapi', 'django', 'flask', 'gin', 'fiber', 'echo', 'nestjs',
97
+ 'rails', 'spring', 'aspnet',
98
+ 'tauri', 'axum', 'actix', 'flutter', 'react-native'
85
99
  ];
86
100
 
87
- const LANGUAGE_CHOICES = ['typescript', 'javascript', 'python', 'go', 'php', 'rust', 'java', 'swift', 'kotlin', 'csharp', 'cpp'];
101
+ const LANGUAGE_CHOICES = ['typescript', 'javascript', 'python', 'go', 'php', 'rust', 'java', 'ruby', 'swift', 'kotlin', 'csharp', 'cpp'];
88
102
  const PROJECT_TYPE_CHOICES = ['web-app', 'api', 'mobile', 'desktop', 'cli', 'fullstack'];
89
103
  const DATABASE_CHOICES = ['postgres', 'mysql', 'sqlite', 'mongodb', 'redis', 'dynamodb', 'none'];
90
104
  const PACKAGE_MANAGER_CHOICES = ['npm', 'pnpm', 'yarn', 'bun', 'pip', 'uv', 'go mod', 'cargo', 'composer', 'auto-detect'];
91
105
  const TEST_FRAMEWORK_CHOICES = ['jest', 'vitest', 'pytest', 'go test', 'phpunit', 'pest', 'rspec', 'junit', 'auto-detect'];
92
106
  const CLOUD_CHOICES = ['aws', 'gcp', 'azure', 'vercel', 'fly', 'railway', 'none'];
107
+ const FRONTEND_FRAMEWORKS = ['vue', 'react', 'next', 'nuxt', 'svelte', 'angular'];
108
+ const BACKEND_FRAMEWORKS = ['laravel', 'cakephp', 'symfony', 'codeigniter', 'express', 'fastapi', 'django', 'flask', 'gin', 'fiber', 'echo', 'nestjs', 'rails', 'spring', 'aspnet', 'axum', 'actix'];
109
+ const MOBILE_DESKTOP_FRAMEWORKS = ['tauri', 'flutter', 'react-native'];
93
110
 
94
111
  function printHelp() {
95
112
  console.log(`
@@ -160,74 +177,453 @@ function uniq(values) {
160
177
  return Array.from(new Set(values.filter(Boolean)));
161
178
  }
162
179
 
180
+ function evidenceIf(condition, label, weight = 1) {
181
+ return condition ? { label, weight } : null;
182
+ }
183
+
184
+ function buildDetection(id, evidence, meta = {}) {
185
+ const hits = evidence.filter(Boolean);
186
+ const score = hits.reduce((sum, hit) => sum + hit.weight, 0);
187
+ return {
188
+ id,
189
+ score,
190
+ evidence: hits.map(hit => hit.label),
191
+ ...meta
192
+ };
193
+ }
194
+
195
+ function confidenceLabel(score) {
196
+ if (score >= 6) return 'high';
197
+ if (score >= 3) return 'medium';
198
+ return 'low';
199
+ }
200
+
201
+ function hasDependency(depNames, ...names) {
202
+ return names.some(name => depNames.has(name));
203
+ }
204
+
205
+ function hasComposerDependency(composerDepNames, ...names) {
206
+ return names.some(name => composerDepNames.has(name));
207
+ }
208
+
209
+ function hasRootFile(rootEntries, ...filenames) {
210
+ return filenames.some(name => rootEntries.has(name));
211
+ }
212
+
213
+ function textMatches(text, pattern) {
214
+ return Boolean(text && pattern.test(text));
215
+ }
216
+
217
+ const LANGUAGE_DETECTORS = [
218
+ {
219
+ id: 'typescript',
220
+ detect: ctx => buildDetection('typescript', [
221
+ evidenceIf(ctx.fileExists('tsconfig.json'), 'found tsconfig.json', 4),
222
+ evidenceIf(hasDependency(ctx.depNames, 'typescript', 'ts-node', 'tsx'), 'package.json -> typescript toolchain', 2)
223
+ ])
224
+ },
225
+ {
226
+ id: 'javascript',
227
+ detect: ctx => buildDetection('javascript', [
228
+ evidenceIf(ctx.fileExists('package.json'), 'found package.json', 2),
229
+ evidenceIf(ctx.depNames.size > 0, 'package.json -> JavaScript dependency graph', 1)
230
+ ])
231
+ },
232
+ {
233
+ id: 'python',
234
+ detect: ctx => buildDetection('python', [
235
+ evidenceIf(ctx.fileExists('requirements.txt'), 'found requirements.txt', 3),
236
+ evidenceIf(ctx.fileExists('pyproject.toml'), 'found pyproject.toml', 2),
237
+ evidenceIf(textMatches(ctx.pyprojectText, /\[tool\.(poetry|uv|hatch|pdm)\]/i), 'pyproject.toml -> Python tool section', 2)
238
+ ])
239
+ },
240
+ {
241
+ id: 'go',
242
+ detect: ctx => buildDetection('go', [
243
+ evidenceIf(ctx.fileExists('go.mod'), 'found go.mod', 4)
244
+ ])
245
+ },
246
+ {
247
+ id: 'php',
248
+ detect: ctx => buildDetection('php', [
249
+ evidenceIf(ctx.fileExists('composer.json'), 'found composer.json', 4),
250
+ evidenceIf(ctx.composerDepNames.size > 0, 'composer.json -> PHP dependency graph', 1)
251
+ ])
252
+ },
253
+ {
254
+ id: 'rust',
255
+ detect: ctx => buildDetection('rust', [
256
+ evidenceIf(ctx.fileExists('Cargo.toml'), 'found Cargo.toml', 4),
257
+ evidenceIf(ctx.fileExists('src-tauri', 'Cargo.toml'), 'found src-tauri/Cargo.toml', 5)
258
+ ])
259
+ },
260
+ {
261
+ id: 'java',
262
+ detect: ctx => buildDetection('java', [
263
+ evidenceIf(ctx.fileExists('pom.xml'), 'found pom.xml', 4),
264
+ evidenceIf(ctx.fileExists('build.gradle') || ctx.fileExists('build.gradle.kts'), 'found Gradle build file', 3)
265
+ ])
266
+ },
267
+ {
268
+ id: 'ruby',
269
+ detect: ctx => buildDetection('ruby', [
270
+ evidenceIf(ctx.fileExists('Gemfile'), 'found Gemfile', 4),
271
+ evidenceIf(ctx.fileExists('config.ru'), 'found config.ru', 1)
272
+ ])
273
+ },
274
+ {
275
+ id: 'csharp',
276
+ detect: ctx => buildDetection('csharp', [
277
+ evidenceIf(ctx.rootEntries.some(name => name.endsWith('.sln') || name.endsWith('.csproj')), 'found .sln/.csproj', 4),
278
+ evidenceIf(ctx.fileExists('Program.cs'), 'found Program.cs', 1)
279
+ ])
280
+ }
281
+ ];
282
+
283
+ const FRAMEWORK_DETECTORS = [
284
+ {
285
+ id: 'next',
286
+ kind: 'frontend',
287
+ projectType: 'web-app',
288
+ detect: ctx => buildDetection('next', [
289
+ evidenceIf(hasDependency(ctx.depNames, 'next'), 'package.json -> next', 4),
290
+ evidenceIf(ctx.fileExists('next.config.js') || ctx.fileExists('next.config.mjs') || ctx.fileExists('next.config.ts'), 'found next.config.*', 3),
291
+ evidenceIf(ctx.fileExists('app') || ctx.fileExists('pages'), 'found app/ or pages/', 1)
292
+ ])
293
+ },
294
+ {
295
+ id: 'react',
296
+ kind: 'frontend',
297
+ projectType: 'web-app',
298
+ detect: ctx => buildDetection('react', [
299
+ evidenceIf(hasDependency(ctx.depNames, 'react'), 'package.json -> react', 4),
300
+ evidenceIf(hasDependency(ctx.depNames, 'react-dom'), 'package.json -> react-dom', 2)
301
+ ])
302
+ },
303
+ {
304
+ id: 'vue',
305
+ kind: 'frontend',
306
+ projectType: 'web-app',
307
+ detect: ctx => buildDetection('vue', [
308
+ evidenceIf(hasDependency(ctx.depNames, 'vue'), 'package.json -> vue', 4)
309
+ ])
310
+ },
311
+ {
312
+ id: 'nuxt',
313
+ kind: 'frontend',
314
+ projectType: 'web-app',
315
+ detect: ctx => buildDetection('nuxt', [
316
+ evidenceIf(hasDependency(ctx.depNames, 'nuxt'), 'package.json -> nuxt', 4),
317
+ evidenceIf(ctx.fileExists('nuxt.config.ts') || ctx.fileExists('nuxt.config.js'), 'found nuxt.config.*', 3)
318
+ ])
319
+ },
320
+ {
321
+ id: 'svelte',
322
+ kind: 'frontend',
323
+ projectType: 'web-app',
324
+ detect: ctx => buildDetection('svelte', [
325
+ evidenceIf(hasDependency(ctx.depNames, 'svelte', '@sveltejs/kit'), 'package.json -> svelte', 4)
326
+ ])
327
+ },
328
+ {
329
+ id: 'angular',
330
+ kind: 'frontend',
331
+ projectType: 'web-app',
332
+ detect: ctx => buildDetection('angular', [
333
+ evidenceIf(hasDependency(ctx.depNames, '@angular/core'), 'package.json -> @angular/core', 4),
334
+ evidenceIf(ctx.fileExists('angular.json'), 'found angular.json', 3)
335
+ ])
336
+ },
337
+ {
338
+ id: 'express',
339
+ kind: 'backend',
340
+ language: 'javascript',
341
+ projectType: 'api',
342
+ detect: ctx => buildDetection('express', [
343
+ evidenceIf(hasDependency(ctx.depNames, 'express'), 'package.json -> express', 4)
344
+ ])
345
+ },
346
+ {
347
+ id: 'nestjs',
348
+ kind: 'backend',
349
+ language: 'javascript',
350
+ projectType: 'api',
351
+ detect: ctx => buildDetection('nestjs', [
352
+ evidenceIf(hasDependency(ctx.depNames, '@nestjs/core'), 'package.json -> @nestjs/core', 4)
353
+ ])
354
+ },
355
+ {
356
+ id: 'fastapi',
357
+ kind: 'backend',
358
+ language: 'python',
359
+ projectType: 'api',
360
+ detect: ctx => buildDetection('fastapi', [
361
+ evidenceIf(textMatches(ctx.requirementsText, /\bfastapi\b/i) || textMatches(ctx.pyprojectText, /\bfastapi\b/i), 'Python deps -> fastapi', 4)
362
+ ])
363
+ },
364
+ {
365
+ id: 'django',
366
+ kind: 'backend',
367
+ language: 'python',
368
+ projectType: 'api',
369
+ detect: ctx => buildDetection('django', [
370
+ evidenceIf(textMatches(ctx.requirementsText, /\bdjango\b/i) || textMatches(ctx.pyprojectText, /\bdjango\b/i), 'Python deps -> django', 4),
371
+ evidenceIf(ctx.fileExists('manage.py'), 'found manage.py', 2)
372
+ ])
373
+ },
374
+ {
375
+ id: 'flask',
376
+ kind: 'backend',
377
+ language: 'python',
378
+ projectType: 'api',
379
+ detect: ctx => buildDetection('flask', [
380
+ evidenceIf(textMatches(ctx.requirementsText, /\bflask\b/i) || textMatches(ctx.pyprojectText, /\bflask\b/i), 'Python deps -> flask', 4)
381
+ ])
382
+ },
383
+ {
384
+ id: 'laravel',
385
+ kind: 'backend',
386
+ language: 'php',
387
+ projectType: 'api',
388
+ detect: ctx => buildDetection('laravel', [
389
+ evidenceIf(hasComposerDependency(ctx.composerDepNames, 'laravel/framework'), 'composer.json -> laravel/framework', 4),
390
+ evidenceIf(ctx.fileExists('artisan'), 'found artisan', 3)
391
+ ])
392
+ },
393
+ {
394
+ id: 'cakephp',
395
+ kind: 'backend',
396
+ language: 'php',
397
+ projectType: 'api',
398
+ detect: ctx => buildDetection('cakephp', [
399
+ evidenceIf(hasComposerDependency(ctx.composerDepNames, 'cakephp/cakephp'), 'composer.json -> cakephp/cakephp', 4),
400
+ evidenceIf(ctx.fileExists('bin', 'cake'), 'found bin/cake', 3),
401
+ evidenceIf(ctx.fileExists('config', 'app.php'), 'found config/app.php', 2),
402
+ evidenceIf(ctx.fileExists('app', 'Config', 'core.php'), 'found app/Config/core.php (CakePHP legacy)', 3),
403
+ evidenceIf(ctx.fileExists('app', 'Controller', 'AppController.php'), 'found app/Controller/AppController.php', 2),
404
+ evidenceIf(ctx.fileExists('app', 'Model', 'AppModel.php'), 'found app/Model/AppModel.php', 2),
405
+ evidenceIf(ctx.fileExists('lib', 'Cake'), 'found lib/Cake', 3),
406
+ evidenceIf(ctx.fileExists('cake', 'console', 'cake'), 'found cake/console/cake', 3)
407
+ ])
408
+ },
409
+ {
410
+ id: 'symfony',
411
+ kind: 'backend',
412
+ language: 'php',
413
+ projectType: 'api',
414
+ detect: ctx => buildDetection('symfony', [
415
+ evidenceIf(hasComposerDependency(ctx.composerDepNames, 'symfony/framework-bundle'), 'composer.json -> symfony/framework-bundle', 4),
416
+ evidenceIf(ctx.fileExists('bin', 'console'), 'found bin/console', 2),
417
+ evidenceIf(ctx.fileExists('config', 'bundles.php'), 'found config/bundles.php', 1)
418
+ ])
419
+ },
420
+ {
421
+ id: 'codeigniter',
422
+ kind: 'backend',
423
+ language: 'php',
424
+ projectType: 'api',
425
+ detect: ctx => buildDetection('codeigniter', [
426
+ evidenceIf(hasComposerDependency(ctx.composerDepNames, 'codeigniter4/framework'), 'composer.json -> codeigniter4/framework', 4),
427
+ evidenceIf(ctx.fileExists('spark'), 'found spark', 2),
428
+ evidenceIf(ctx.fileExists('app', 'Config', 'App.php'), 'found app/Config/App.php', 1)
429
+ ])
430
+ },
431
+ {
432
+ id: 'gin',
433
+ kind: 'backend',
434
+ language: 'go',
435
+ projectType: 'api',
436
+ detect: ctx => buildDetection('gin', [
437
+ evidenceIf(textMatches(ctx.goModText, /\bgithub\.com\/gin-gonic\/gin\b/i), 'go.mod -> github.com/gin-gonic/gin', 4)
438
+ ])
439
+ },
440
+ {
441
+ id: 'fiber',
442
+ kind: 'backend',
443
+ language: 'go',
444
+ projectType: 'api',
445
+ detect: ctx => buildDetection('fiber', [
446
+ evidenceIf(textMatches(ctx.goModText, /\bgithub\.com\/gofiber\/fiber\b/i), 'go.mod -> github.com/gofiber/fiber', 4)
447
+ ])
448
+ },
449
+ {
450
+ id: 'echo',
451
+ kind: 'backend',
452
+ language: 'go',
453
+ projectType: 'api',
454
+ detect: ctx => buildDetection('echo', [
455
+ evidenceIf(textMatches(ctx.goModText, /\bgithub\.com\/labstack\/echo\b/i), 'go.mod -> github.com/labstack/echo', 4)
456
+ ])
457
+ },
458
+ {
459
+ id: 'rails',
460
+ kind: 'backend',
461
+ language: 'ruby',
462
+ projectType: 'api',
463
+ detect: ctx => buildDetection('rails', [
464
+ evidenceIf(textMatches(ctx.gemfileText, /^\s*gem\s+["']rails["']/im), 'Gemfile -> rails', 4),
465
+ evidenceIf(ctx.fileExists('bin', 'rails'), 'found bin/rails', 2)
466
+ ])
467
+ },
468
+ {
469
+ id: 'spring',
470
+ kind: 'backend',
471
+ language: 'java',
472
+ projectType: 'api',
473
+ detect: ctx => buildDetection('spring', [
474
+ evidenceIf(textMatches(ctx.pomText, /spring-boot/i) || textMatches(ctx.gradleText, /spring-boot/i), 'Java build -> spring-boot', 4)
475
+ ])
476
+ },
477
+ {
478
+ id: 'aspnet',
479
+ kind: 'backend',
480
+ language: 'csharp',
481
+ projectType: 'api',
482
+ detect: ctx => buildDetection('aspnet', [
483
+ evidenceIf(textMatches(ctx.csprojText, /Microsoft\.NET\.Sdk\.Web/i), '.csproj -> Microsoft.NET.Sdk.Web', 4),
484
+ evidenceIf(textMatches(ctx.programCsText, /WebApplication\.CreateBuilder/i), 'Program.cs -> WebApplication.CreateBuilder', 2)
485
+ ])
486
+ },
487
+ {
488
+ id: 'tauri',
489
+ kind: 'desktop',
490
+ language: 'rust',
491
+ projectType: 'desktop',
492
+ detect: ctx => buildDetection('tauri', [
493
+ evidenceIf(hasDependency(ctx.depNames, '@tauri-apps/api'), 'package.json -> @tauri-apps/api', 4),
494
+ evidenceIf(ctx.fileExists('src-tauri', 'tauri.conf.json') || ctx.fileExists('src-tauri', 'tauri.conf.json5'), 'found src-tauri/tauri.conf.*', 4),
495
+ evidenceIf(ctx.fileExists('src-tauri', 'Cargo.toml'), 'found src-tauri/Cargo.toml', 2)
496
+ ])
497
+ },
498
+ {
499
+ id: 'axum',
500
+ kind: 'backend',
501
+ language: 'rust',
502
+ projectType: 'api',
503
+ detect: ctx => buildDetection('axum', [
504
+ evidenceIf(textMatches(ctx.cargoText, /\baxum\b/i), 'Cargo.toml -> axum', 4)
505
+ ])
506
+ },
507
+ {
508
+ id: 'actix',
509
+ kind: 'backend',
510
+ language: 'rust',
511
+ projectType: 'api',
512
+ detect: ctx => buildDetection('actix', [
513
+ evidenceIf(textMatches(ctx.cargoText, /\bactix-web\b/i), 'Cargo.toml -> actix-web', 4)
514
+ ])
515
+ },
516
+ {
517
+ id: 'flutter',
518
+ kind: 'mobile',
519
+ projectType: 'mobile',
520
+ detect: ctx => buildDetection('flutter', [
521
+ evidenceIf(textMatches(ctx.pubspecText, /^\s*flutter\s*:/im), 'pubspec.yaml -> flutter', 4)
522
+ ])
523
+ },
524
+ {
525
+ id: 'react-native',
526
+ kind: 'mobile',
527
+ projectType: 'mobile',
528
+ detect: ctx => buildDetection('react-native', [
529
+ evidenceIf(hasDependency(ctx.depNames, 'react-native', 'expo'), 'package.json -> react-native/expo', 4)
530
+ ])
531
+ }
532
+ ];
533
+
163
534
  function scanProject() {
164
535
  const pkg = readJsonIfExists('package.json') || {};
536
+ const composer = readJsonIfExists('composer.json') || {};
537
+ const rootEntries = fs.existsSync(targetDir) ? fs.readdirSync(targetDir) : [];
538
+ const rootCsproj = rootEntries.find(name => name.endsWith('.csproj')) || '';
165
539
  const deps = {
166
540
  ...(pkg.dependencies || {}),
167
541
  ...(pkg.devDependencies || {}),
168
542
  ...(pkg.peerDependencies || {})
169
543
  };
170
544
  const depNames = new Set(Object.keys(deps));
171
- const frameworks = [];
545
+ const composerDeps = {
546
+ ...(composer.require || {}),
547
+ ...(composer['require-dev'] || {})
548
+ };
549
+ const composerDepNames = new Set(Object.keys(composerDeps));
550
+ const ctx = {
551
+ pkg,
552
+ composer,
553
+ depNames,
554
+ composerDepNames,
555
+ rootEntries,
556
+ fileExists,
557
+ requirementsText: readTextIfExists('requirements.txt'),
558
+ pyprojectText: readTextIfExists('pyproject.toml'),
559
+ goModText: readTextIfExists('go.mod'),
560
+ pubspecText: readTextIfExists('pubspec.yaml'),
561
+ gemfileText: readTextIfExists('Gemfile'),
562
+ pomText: readTextIfExists('pom.xml'),
563
+ gradleText: `${readTextIfExists('build.gradle')}\n${readTextIfExists('build.gradle.kts')}`,
564
+ cargoText: `${readTextIfExists('Cargo.toml')}\n${readTextIfExists('src-tauri', 'Cargo.toml')}`,
565
+ csprojText: rootCsproj ? readTextIfExists(rootCsproj) : '',
566
+ programCsText: readTextIfExists('Program.cs')
567
+ };
568
+
569
+ const languageDetections = LANGUAGE_DETECTORS
570
+ .map(detector => detector.detect(ctx))
571
+ .filter(result => result.score >= 2)
572
+ .sort((a, b) => b.score - a.score);
573
+
574
+ const frameworkDetails = FRAMEWORK_DETECTORS
575
+ .map(detector => {
576
+ const result = detector.detect(ctx);
577
+ if (result.score < 3) return null;
578
+ return {
579
+ ...result,
580
+ kind: detector.kind,
581
+ language: detector.language,
582
+ projectType: detector.projectType,
583
+ confidence: confidenceLabel(result.score)
584
+ };
585
+ })
586
+ .filter(Boolean)
587
+ .sort((a, b) => b.score - a.score);
588
+
589
+ const frameworkLanguageScores = frameworkDetails.reduce((acc, item) => {
590
+ if (!item.language) return acc;
591
+ acc[item.language] = (acc[item.language] || 0) + item.score;
592
+ return acc;
593
+ }, {});
594
+ const primaryLanguage = Object.keys(frameworkLanguageScores).sort((a, b) => frameworkLanguageScores[b] - frameworkLanguageScores[a])[0]
595
+ || languageDetections[0]?.id
596
+ || 'typescript';
597
+ const frameworks = uniq(frameworkDetails.map(item => item.id)).filter(name => FRAMEWORK_CHOICES.includes(name));
172
598
  const infrastructure = [];
173
- let primaryLanguage = '';
174
599
  let projectType = '';
175
600
  let database = 'none';
176
601
  let testFramework = 'auto-detect';
177
602
  let cloud = 'none';
178
603
  let ciCd = 'none';
179
604
 
180
- const addFramework = (name, condition) => {
181
- if (condition) frameworks.push(name);
182
- };
605
+ if (frameworkDetails.some(item => item.projectType === 'desktop')) projectType = 'desktop';
606
+ else if (frameworkDetails.some(item => item.projectType === 'mobile')) projectType = 'mobile';
607
+ else if (frameworkDetails.some(item => item.kind === 'frontend') && frameworkDetails.some(item => item.kind === 'backend')) projectType = 'fullstack';
608
+ else if (frameworkDetails.some(item => item.kind === 'backend')) projectType = 'api';
609
+ else if (frameworkDetails.some(item => item.kind === 'frontend')) projectType = 'web-app';
610
+ else if (fileExists('package.json') && pkg.bin) projectType = 'cli';
611
+ else if (['php', 'python', 'go', 'java', 'ruby', 'csharp', 'rust'].includes(primaryLanguage)) projectType = 'api';
183
612
 
184
- addFramework('next', depNames.has('next'));
185
- addFramework('react', depNames.has('react'));
186
- addFramework('vue', depNames.has('vue'));
187
- addFramework('nuxt', depNames.has('nuxt'));
188
- addFramework('svelte', depNames.has('svelte') || depNames.has('@sveltejs/kit'));
189
- addFramework('angular', depNames.has('@angular/core'));
190
- addFramework('express', depNames.has('express'));
191
- addFramework('nestjs', depNames.has('@nestjs/core'));
192
- addFramework('fastapi', fileExists('requirements.txt') && /fastapi/i.test(readTextIfExists('requirements.txt')));
193
- addFramework('django', fileExists('requirements.txt') && /django/i.test(readTextIfExists('requirements.txt')));
194
- addFramework('tauri', depNames.has('@tauri-apps/api') || fileExists('src-tauri', 'tauri.conf.json') || fileExists('src-tauri', 'tauri.conf.json5'));
195
- addFramework('flutter', fileExists('pubspec.yaml') && /flutter:/i.test(readTextIfExists('pubspec.yaml')));
196
- addFramework('react-native', depNames.has('react-native') || depNames.has('expo'));
197
- addFramework('laravel', fileExists('artisan') || depNames.has('laravel'));
198
- addFramework('gin', fileExists('go.mod') && /\bgithub\.com\/gin-gonic\/gin\b/i.test(readTextIfExists('go.mod')));
199
- addFramework('fiber', fileExists('go.mod') && /\bgithub\.com\/gofiber\/fiber\b/i.test(readTextIfExists('go.mod')));
200
-
201
- if (fileExists('tsconfig.json') || depNames.has('typescript')) primaryLanguage = 'typescript';
202
- else if (fileExists('Cargo.toml') || fileExists('src-tauri', 'Cargo.toml')) primaryLanguage = 'rust';
203
- else if (fileExists('go.mod')) primaryLanguage = 'go';
204
- else if (fileExists('requirements.txt') || fileExists('pyproject.toml')) primaryLanguage = 'python';
205
- else if (fileExists('composer.json')) primaryLanguage = 'php';
206
- else if (depNames.size > 0 || fileExists('package.json')) primaryLanguage = 'javascript';
207
-
208
- if (frameworks.includes('tauri')) projectType = 'desktop';
209
- else if (frameworks.includes('flutter') || frameworks.includes('react-native')) projectType = 'mobile';
210
- else if (frameworks.some(f => ['express', 'fastapi', 'django', 'laravel', 'gin', 'fiber', 'nestjs'].includes(f))) {
211
- projectType = frameworks.some(f => ['react', 'next', 'vue', 'nuxt', 'svelte', 'angular'].includes(f)) ? 'fullstack' : 'api';
212
- } else if (frameworks.some(f => ['react', 'next', 'vue', 'nuxt', 'svelte', 'angular'].includes(f))) {
213
- projectType = 'web-app';
214
- } else if (fileExists('package.json') && pkg.bin) {
215
- projectType = 'cli';
216
- }
613
+ if (hasDependency(depNames, 'pg', 'postgres', '@prisma/client') || /postgres/i.test(JSON.stringify(pkg))) database = 'postgres';
614
+ else if (hasDependency(depNames, 'mysql', 'mysql2')) database = 'mysql';
615
+ else if (hasDependency(depNames, 'sqlite3', 'better-sqlite3', 'rusqlite')) database = 'sqlite';
616
+ else if (hasDependency(depNames, 'mongodb', 'mongoose')) database = 'mongodb';
617
+ else if (hasDependency(depNames, 'redis', 'ioredis')) database = 'redis';
618
+ else if (hasDependency(depNames, '@aws-sdk/client-dynamodb')) database = 'dynamodb';
217
619
 
218
- if (depNames.has('pg') || depNames.has('postgres') || depNames.has('@prisma/client') || /postgres/i.test(JSON.stringify(pkg))) database = 'postgres';
219
- else if (depNames.has('mysql') || depNames.has('mysql2')) database = 'mysql';
220
- else if (depNames.has('sqlite3') || depNames.has('better-sqlite3') || depNames.has('rusqlite')) database = 'sqlite';
221
- else if (depNames.has('mongodb') || depNames.has('mongoose')) database = 'mongodb';
222
- else if (depNames.has('redis') || depNames.has('ioredis')) database = 'redis';
223
- else if (depNames.has('@aws-sdk/client-dynamodb')) database = 'dynamodb';
224
-
225
- if (depNames.has('vitest')) testFramework = 'vitest';
226
- else if (depNames.has('jest')) testFramework = 'jest';
227
- else if (depNames.has('pytest') || /pytest/i.test(readTextIfExists('requirements.txt'))) testFramework = 'pytest';
228
- else if (depNames.has('phpunit')) testFramework = 'phpunit';
229
- else if (depNames.has('pest')) testFramework = 'pest';
620
+ if (hasDependency(depNames, 'vitest')) testFramework = 'vitest';
621
+ else if (hasDependency(depNames, 'jest')) testFramework = 'jest';
622
+ else if (hasDependency(depNames, 'pytest') || textMatches(ctx.requirementsText, /\bpytest\b/i) || textMatches(ctx.pyprojectText, /\bpytest\b/i)) testFramework = 'pytest';
623
+ else if (hasComposerDependency(composerDepNames, 'phpunit/phpunit')) testFramework = 'phpunit';
624
+ else if (hasComposerDependency(composerDepNames, 'pestphp/pest')) testFramework = 'pest';
230
625
  else if (fileExists('go.mod')) testFramework = 'go test';
626
+ else if (textMatches(ctx.gemfileText, /^\s*gem\s+["']rspec["']/im)) testFramework = 'rspec';
231
627
 
232
628
  if (fileExists('Dockerfile') || fileExists('docker-compose.yml') || fileExists('docker-compose.yaml')) {
233
629
  infrastructure.push('docker');
@@ -249,12 +645,19 @@ function scanProject() {
249
645
  else if (fileExists('.circleci', 'config.yml')) ciCd = 'circleci';
250
646
  else if (fileExists('Jenkinsfile')) ciCd = 'jenkins';
251
647
 
648
+ const scanEvidence = [
649
+ ...(languageDetections[0] ? [`language:${languageDetections[0].id} => ${languageDetections[0].evidence.join('; ')}`] : []),
650
+ ...frameworkDetails.map(item => `${item.id} [${item.confidence}] => ${item.evidence.join('; ')}`)
651
+ ];
652
+
252
653
  return {
253
654
  project_name: path.basename(targetDir),
254
- description: pkg.description || 'A software project',
655
+ description: pkg.description || composer.description || 'A software project',
255
656
  project_type: PROJECT_TYPE_CHOICES.includes(projectType) ? projectType : 'web-app',
256
657
  primary_language: LANGUAGE_CHOICES.includes(primaryLanguage) ? primaryLanguage : 'typescript',
257
- frameworks: uniq(frameworks),
658
+ frameworks,
659
+ framework_details: frameworkDetails,
660
+ scan_evidence: scanEvidence,
258
661
  database: DATABASE_CHOICES.includes(database) ? database : 'none',
259
662
  package_manager: PACKAGE_MANAGER_CHOICES.includes(detectPackageManager()) ? detectPackageManager() : 'auto-detect',
260
663
  uses_docker: infrastructure.includes('docker'),
@@ -277,7 +680,7 @@ function resolveAgentTable(data) {
277
680
  const lang = (data.primary_language || "").toLowerCase();
278
681
 
279
682
  // Frontend
280
- const frontendFws = frameworks.filter(f => ["vue", "react", "next", "nuxt", "svelte", "angular"].includes(f));
683
+ const frontendFws = frameworks.filter(f => FRONTEND_FRAMEWORKS.includes(f));
281
684
  if (frontendFws.length > 0) {
282
685
  let feSkills = [];
283
686
  frontendFws.forEach(fw => {
@@ -288,14 +691,14 @@ function resolveAgentTable(data) {
288
691
 
289
692
  // Backend
290
693
  let beSkills = [...(LANG_SKILLS[lang] || ["api-design"])];
291
- const backendFws = frameworks.filter(f => ["laravel", "express", "fastapi", "django", "gin", "fiber", "nestjs"].includes(f));
694
+ const backendFws = frameworks.filter(f => BACKEND_FRAMEWORKS.includes(f));
292
695
  backendFws.forEach(fw => {
293
696
  beSkills.push(...(FRAMEWORK_SKILLS[fw] || []));
294
697
  });
295
698
  rows.push(["Backend API", "backend-specialist", Array.from(new Set(beSkills)).slice(0, 3).join(", ")]);
296
699
 
297
700
  // Mobile/Desktop
298
- const mobileFws = frameworks.filter(f => ["tauri", "flutter", "react-native"].includes(f));
701
+ const mobileFws = frameworks.filter(f => MOBILE_DESKTOP_FRAMEWORKS.includes(f));
299
702
  if (mobileFws.length > 0) {
300
703
  let mobileSkills = [];
301
704
  mobileFws.forEach(fw => {
@@ -318,12 +721,25 @@ function resolveAgentTable(data) {
318
721
 
319
722
  // --- File Generators ---
320
723
 
321
- function generateProjectContext(data) {
322
- const agentTable = resolveAgentTable(data);
323
- const autoSkills = resolveSkills(data);
324
-
325
- const agentRows = agentTable.map(([task, agent, skills]) => `| ${task} | \`${agent}\` | ${skills} |`).join('\n');
326
- const autoSkillsList = autoSkills.map(s => `- ${s}`).join('\n');
724
+ function generateProjectContext(data) {
725
+ const agentTable = resolveAgentTable(data);
726
+ const autoSkills = resolveSkills(data);
727
+ const scanEvidenceBlock = (data.scan_evidence || []).length > 0
728
+ ? `
729
+ ---
730
+
731
+ ## Scan Evidence
732
+
733
+ These detections were inferred from the current codebase during \`init --scan\`:
734
+
735
+ \`\`\`text
736
+ ${(data.scan_evidence || []).map(line => '- ' + line).join('\n')}
737
+ \`\`\`
738
+ `
739
+ : '';
740
+
741
+ const agentRows = agentTable.map(([task, agent, skills]) => `| ${task} | \`${agent}\` | ${skills} |`).join('\n');
742
+ const autoSkillsList = autoSkills.map(s => `- ${s}`).join('\n');
327
743
 
328
744
  return `# PROJECT_CONTEXT.md
329
745
  # Generated by thachvd-kit init wizard on ${new Date().toISOString().split('T')[0]}
@@ -347,11 +763,13 @@ stack:
347
763
  infrastructure: [${(data.infrastructure || []).join(', ')}]
348
764
  package_manager: ${data.package_manager || 'auto-detect'}
349
765
  test_framework: ${data.test_framework || 'auto-detect'}
350
- \`\`\`
351
-
352
- ---
353
-
354
- ## Agent Routing
766
+ \`\`\`
767
+
768
+ ${scanEvidenceBlock}
769
+
770
+ ---
771
+
772
+ ## Agent Routing
355
773
 
356
774
  | Task Type | Agent | Primary Skills |
357
775
  |-----------|-------|----------------|
@@ -510,6 +928,11 @@ async function main() {
510
928
  if (useScan) {
511
929
  console.log(`${pc.bold(' Scan result:')} ${scanned.primary_language} | ${scanned.frameworks.join(', ') || 'no framework'}`);
512
930
  console.log(` ${pc.bold('Detected infra:')} ${scanned.infrastructure.join(', ') || 'none'}\n`);
931
+ if ((scanned.scan_evidence || []).length > 0) {
932
+ console.log(` ${pc.bold('Evidence:')}`);
933
+ scanned.scan_evidence.forEach(line => console.log(` · ${line}`));
934
+ console.log('');
935
+ }
513
936
  }
514
937
 
515
938
  const { project_name, description, project_type } = await askQuestions([
@@ -578,6 +1001,8 @@ async function main() {
578
1001
  data.package_manager = data.package_manager || scanned?.package_manager || 'auto-detect';
579
1002
  data.test_framework = data.test_framework || scanned?.test_framework || 'auto-detect';
580
1003
  data.max_file_lines = data.max_file_lines || scanned?.max_file_lines || '300';
1004
+ data.framework_details = scanned?.framework_details || [];
1005
+ data.scan_evidence = scanned?.scan_evidence || [];
581
1006
  data.infrastructure = [];
582
1007
  if (data.uses_docker) data.infrastructure.push('docker');
583
1008
  if (data.cloud !== 'none') data.infrastructure.push(data.cloud);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thachvd-kit",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Universal Agent Kit for Antigravity, Claude Code, and OpenCode",
5
5
  "bin": {
6
6
  "thachvd-kit": "./bin/cli.js"
package/scripts/init.py CHANGED
@@ -13,6 +13,7 @@ import sys
13
13
  import os
14
14
  import argparse
15
15
  import json
16
+ import re
16
17
  from pathlib import Path
17
18
  from datetime import datetime
18
19
 
@@ -35,12 +36,13 @@ def ask(prompt, default="", choices=None):
35
36
  value = input(f" {bold('→')} {prompt}{display_default}: ").strip()
36
37
  return value if value else default
37
38
 
38
- def ask_multi(prompt, choices, default=None):
39
- """Prompt for comma-separated selection from choices."""
40
- print(f"\n {cyan('Available:')} {', '.join(f'{i+1}) {c}' for i, c in enumerate(choices))}")
41
- raw = input(f" {bold('→')} {prompt} (numbers or names, comma-separated) [{dim(default or 'none')}]: ").strip()
42
- if not raw:
43
- return default or []
39
+ def ask_multi(prompt, choices, default=None):
40
+ """Prompt for comma-separated selection from choices."""
41
+ display_default = ", ".join(default) if isinstance(default, list) else (default or "none")
42
+ print(f"\n {cyan('Available:')} {', '.join(f'{i+1}) {c}' for i, c in enumerate(choices))}")
43
+ raw = input(f" {bold('→')} {prompt} (numbers or names, comma-separated) [{dim(display_default)}]: ").strip()
44
+ if not raw:
45
+ return default or []
44
46
  selected = []
45
47
  for part in raw.replace(",", " ").split():
46
48
  part = part.strip()
@@ -71,32 +73,45 @@ def section(title):
71
73
 
72
74
  # ─── Skill resolver ──────────────────────────────────────────────────────────
73
75
 
74
- LANG_SKILLS = {
75
- "typescript": ["nodejs-best-practices", "api-design"],
76
- "javascript": ["nodejs-best-practices", "api-design"],
77
- "python": ["python-patterns"],
78
- "go": ["golang-patterns", "golang-testing"],
79
- "php": ["laravel-patterns", "laravel-security", "laravel-tdd"],
80
- "rust": ["rust-pro"],
81
- "java": ["api-design"],
82
- "swift": ["mobile-design"],
83
- "kotlin": ["mobile-design"],
84
- "csharp": ["api-design"],
85
- }
76
+ LANG_SKILLS = {
77
+ "typescript": ["nodejs-best-practices", "api-design"],
78
+ "javascript": ["nodejs-best-practices", "api-design"],
79
+ "python": ["python-patterns"],
80
+ "go": ["golang-patterns", "golang-testing"],
81
+ "php": ["api-design"],
82
+ "rust": ["rust-pro"],
83
+ "java": ["api-design"],
84
+ "ruby": ["api-design"],
85
+ "swift": ["mobile-design"],
86
+ "kotlin": ["mobile-design"],
87
+ "csharp": ["api-design"],
88
+ }
86
89
 
87
90
  FRAMEWORK_SKILLS = {
88
91
  "vue": ["frontend-design", "tailwind-patterns"],
89
92
  "react": ["react-frontend", "frontend-design"],
90
93
  "next": ["nextjs-react-expert", "frontend-design"],
91
- "nuxt": ["frontend-design"],
92
- "svelte": ["frontend-design"],
93
- "laravel": ["laravel-patterns", "laravel-security", "laravel-tdd"],
94
- "express": ["nodejs-best-practices", "api-design"],
95
- "fastapi": ["python-patterns", "api-design"],
96
- "django": ["python-patterns", "api-design"],
97
- "gin": ["golang-patterns", "golang-testing"],
98
- "fiber": ["golang-patterns", "golang-testing"],
94
+ "nuxt": ["frontend-design"],
95
+ "svelte": ["frontend-design"],
96
+ "angular": ["frontend-design"],
97
+ "laravel": ["laravel-patterns", "laravel-security", "laravel-tdd"],
98
+ "cakephp": ["api-design"],
99
+ "symfony": ["api-design"],
100
+ "codeigniter": ["api-design"],
101
+ "express": ["nodejs-best-practices", "api-design"],
102
+ "fastapi": ["python-patterns", "api-design"],
103
+ "django": ["python-patterns", "api-design"],
104
+ "flask": ["python-patterns", "api-design"],
105
+ "gin": ["golang-patterns", "golang-testing"],
106
+ "fiber": ["golang-patterns", "golang-testing"],
107
+ "echo": ["golang-patterns", "golang-testing"],
108
+ "nestjs": ["nodejs-best-practices", "api-design"],
99
109
  "tauri": ["rust-pro", "desktop-design"],
110
+ "axum": ["rust-pro", "api-design"],
111
+ "actix": ["rust-pro", "api-design"],
112
+ "rails": ["api-design"],
113
+ "spring": ["api-design"],
114
+ "aspnet": ["api-design"],
100
115
  "flutter": ["mobile-design"],
101
116
  "react-native": ["mobile-design"],
102
117
  }
@@ -128,14 +143,19 @@ def resolve_skills(data):
128
143
  return sorted(skills)
129
144
 
130
145
  FRAMEWORKS_ALL = ["vue", "react", "next", "nuxt", "svelte", "angular",
131
- "laravel", "express", "fastapi", "django", "gin", "fiber", "nestjs",
132
- "tauri", "flutter", "react-native"]
133
- LANGUAGES_ALL = ["typescript", "javascript", "python", "go", "php", "rust", "java", "swift", "kotlin", "csharp", "c", "cpp"]
146
+ "laravel", "cakephp", "symfony", "codeigniter",
147
+ "express", "fastapi", "django", "flask", "gin", "fiber", "echo", "nestjs",
148
+ "rails", "spring", "aspnet",
149
+ "tauri", "axum", "actix", "flutter", "react-native"]
150
+ LANGUAGES_ALL = ["typescript", "javascript", "python", "go", "php", "rust", "java", "ruby", "swift", "kotlin", "csharp", "c", "cpp"]
134
151
  PROJECT_TYPES_ALL = ["web-app", "api", "mobile", "desktop", "cli", "library", "fullstack"]
135
152
  DATABASES_ALL = ["postgres", "mysql", "sqlite", "mongodb", "redis", "dynamodb", "supabase", "prisma", "none"]
136
153
  PACKAGE_MANAGERS_ALL = ["npm", "pnpm", "yarn", "bun", "pip", "uv", "poetry", "go mod", "cargo", "composer", "auto-detect"]
137
154
  TEST_FRAMEWORKS_ALL = ["jest", "vitest", "pytest", "go test", "phpunit", "pest", "rspec", "junit", "auto-detect"]
138
155
  CLOUDS_ALL = ["aws", "gcp", "azure", "vercel", "fly", "railway", "digitalocean", "none"]
156
+ FRONTEND_FRAMEWORKS = ["vue", "react", "next", "nuxt", "svelte", "angular"]
157
+ BACKEND_FRAMEWORKS = ["laravel", "cakephp", "symfony", "codeigniter", "express", "fastapi", "django", "flask", "gin", "fiber", "echo", "nestjs", "rails", "spring", "aspnet", "axum", "actix"]
158
+ MOBILE_DESKTOP_FRAMEWORKS = ["tauri", "flutter", "react-native"]
139
159
 
140
160
  def read_text_if_exists(*parts):
141
161
  path = Path.cwd().joinpath(*parts)
@@ -168,75 +188,241 @@ def detect_package_manager():
168
188
  def uniq(values):
169
189
  return list(dict.fromkeys(v for v in values if v))
170
190
 
191
+ def evidence_if(condition, label, weight=1):
192
+ return {"label": label, "weight": weight} if condition else None
193
+
194
+ def build_detection(id, evidence, **meta):
195
+ hits = [hit for hit in evidence if hit]
196
+ score = sum(hit["weight"] for hit in hits)
197
+ return {
198
+ "id": id,
199
+ "score": score,
200
+ "evidence": [hit["label"] for hit in hits],
201
+ **meta,
202
+ }
203
+
204
+ def confidence_label(score):
205
+ if score >= 6: return "high"
206
+ if score >= 3: return "medium"
207
+ return "low"
208
+
209
+ def has_dependency(dep_names, *names):
210
+ return any(name in dep_names for name in names)
211
+
212
+ def has_composer_dependency(composer_dep_names, *names):
213
+ return any(name in composer_dep_names for name in names)
214
+
215
+ def text_matches(text, pattern):
216
+ return bool(text and re.search(pattern, text, re.IGNORECASE | re.MULTILINE))
217
+
171
218
  def scan_project():
172
219
  pkg = read_json_if_exists("package.json") or {}
220
+ composer = read_json_if_exists("composer.json") or {}
221
+ root_entries = [p.name for p in Path.cwd().iterdir()] if Path.cwd().exists() else []
222
+ root_csproj = next((name for name in root_entries if name.endswith(".csproj")), "")
173
223
  deps = {}
174
224
  for key in ("dependencies", "devDependencies", "peerDependencies"):
175
225
  deps.update(pkg.get(key, {}) or {})
176
226
  dep_names = set(deps.keys())
177
- frameworks = []
227
+ composer_deps = {}
228
+ for key in ("require", "require-dev"):
229
+ composer_deps.update(composer.get(key, {}) or {})
230
+ composer_dep_names = set(composer_deps.keys())
231
+ requirements = read_text_if_exists("requirements.txt")
232
+ pyproject = read_text_if_exists("pyproject.toml")
233
+ go_mod = read_text_if_exists("go.mod")
234
+ pubspec = read_text_if_exists("pubspec.yaml")
235
+ gemfile = read_text_if_exists("Gemfile")
236
+ pom = read_text_if_exists("pom.xml")
237
+ gradle = f"{read_text_if_exists('build.gradle')}\n{read_text_if_exists('build.gradle.kts')}"
238
+ cargo = f"{read_text_if_exists('Cargo.toml')}\n{read_text_if_exists('src-tauri/Cargo.toml')}"
239
+ csproj = read_text_if_exists(root_csproj) if root_csproj else ""
240
+ program_cs = read_text_if_exists("Program.cs")
241
+
242
+ language_detections = [
243
+ build_detection("typescript", [
244
+ evidence_if(Path("tsconfig.json").exists(), "found tsconfig.json", 4),
245
+ evidence_if(has_dependency(dep_names, "typescript", "ts-node", "tsx"), "package.json -> typescript toolchain", 2),
246
+ ]),
247
+ build_detection("javascript", [
248
+ evidence_if(Path("package.json").exists(), "found package.json", 2),
249
+ evidence_if(bool(dep_names), "package.json -> JavaScript dependency graph", 1),
250
+ ]),
251
+ build_detection("python", [
252
+ evidence_if(Path("requirements.txt").exists(), "found requirements.txt", 3),
253
+ evidence_if(Path("pyproject.toml").exists(), "found pyproject.toml", 2),
254
+ evidence_if(text_matches(pyproject, r"\[tool\.(poetry|uv|hatch|pdm)\]"), "pyproject.toml -> Python tool section", 2),
255
+ ]),
256
+ build_detection("go", [
257
+ evidence_if(Path("go.mod").exists(), "found go.mod", 4),
258
+ ]),
259
+ build_detection("php", [
260
+ evidence_if(Path("composer.json").exists(), "found composer.json", 4),
261
+ evidence_if(bool(composer_dep_names), "composer.json -> PHP dependency graph", 1),
262
+ ]),
263
+ build_detection("rust", [
264
+ evidence_if(Path("Cargo.toml").exists(), "found Cargo.toml", 4),
265
+ evidence_if(Path("src-tauri/Cargo.toml").exists(), "found src-tauri/Cargo.toml", 5),
266
+ ]),
267
+ build_detection("java", [
268
+ evidence_if(Path("pom.xml").exists(), "found pom.xml", 4),
269
+ evidence_if(Path("build.gradle").exists() or Path("build.gradle.kts").exists(), "found Gradle build file", 3),
270
+ ]),
271
+ build_detection("ruby", [
272
+ evidence_if(Path("Gemfile").exists(), "found Gemfile", 4),
273
+ evidence_if(Path("config.ru").exists(), "found config.ru", 1),
274
+ ]),
275
+ build_detection("csharp", [
276
+ evidence_if(any(name.endswith(".sln") or name.endswith(".csproj") for name in root_entries), "found .sln/.csproj", 4),
277
+ evidence_if(Path("Program.cs").exists(), "found Program.cs", 1),
278
+ ]),
279
+ ]
280
+ language_detections = [item for item in language_detections if item["score"] >= 2]
281
+ language_detections.sort(key=lambda item: item["score"], reverse=True)
282
+
283
+ framework_details = [
284
+ {**build_detection("next", [
285
+ evidence_if(has_dependency(dep_names, "next"), "package.json -> next", 4),
286
+ evidence_if(Path("next.config.js").exists() or Path("next.config.mjs").exists() or Path("next.config.ts").exists(), "found next.config.*", 3),
287
+ evidence_if(Path("app").exists() or Path("pages").exists(), "found app/ or pages/", 1),
288
+ ]), "kind": "frontend", "project_type": "web-app"},
289
+ {**build_detection("react", [
290
+ evidence_if(has_dependency(dep_names, "react"), "package.json -> react", 4),
291
+ evidence_if(has_dependency(dep_names, "react-dom"), "package.json -> react-dom", 2),
292
+ ]), "kind": "frontend", "project_type": "web-app"},
293
+ {**build_detection("vue", [
294
+ evidence_if(has_dependency(dep_names, "vue"), "package.json -> vue", 4),
295
+ ]), "kind": "frontend", "project_type": "web-app"},
296
+ {**build_detection("nuxt", [
297
+ evidence_if(has_dependency(dep_names, "nuxt"), "package.json -> nuxt", 4),
298
+ evidence_if(Path("nuxt.config.ts").exists() or Path("nuxt.config.js").exists(), "found nuxt.config.*", 3),
299
+ ]), "kind": "frontend", "project_type": "web-app"},
300
+ {**build_detection("svelte", [
301
+ evidence_if(has_dependency(dep_names, "svelte", "@sveltejs/kit"), "package.json -> svelte", 4),
302
+ ]), "kind": "frontend", "project_type": "web-app"},
303
+ {**build_detection("angular", [
304
+ evidence_if(has_dependency(dep_names, "@angular/core"), "package.json -> @angular/core", 4),
305
+ evidence_if(Path("angular.json").exists(), "found angular.json", 3),
306
+ ]), "kind": "frontend", "project_type": "web-app"},
307
+ {**build_detection("express", [
308
+ evidence_if(has_dependency(dep_names, "express"), "package.json -> express", 4),
309
+ ]), "kind": "backend", "project_type": "api", "language": "javascript"},
310
+ {**build_detection("nestjs", [
311
+ evidence_if(has_dependency(dep_names, "@nestjs/core"), "package.json -> @nestjs/core", 4),
312
+ ]), "kind": "backend", "project_type": "api", "language": "javascript"},
313
+ {**build_detection("fastapi", [
314
+ evidence_if(text_matches(requirements, r"\bfastapi\b") or text_matches(pyproject, r"\bfastapi\b"), "Python deps -> fastapi", 4),
315
+ ]), "kind": "backend", "project_type": "api", "language": "python"},
316
+ {**build_detection("django", [
317
+ evidence_if(text_matches(requirements, r"\bdjango\b") or text_matches(pyproject, r"\bdjango\b"), "Python deps -> django", 4),
318
+ evidence_if(Path("manage.py").exists(), "found manage.py", 2),
319
+ ]), "kind": "backend", "project_type": "api", "language": "python"},
320
+ {**build_detection("flask", [
321
+ evidence_if(text_matches(requirements, r"\bflask\b") or text_matches(pyproject, r"\bflask\b"), "Python deps -> flask", 4),
322
+ ]), "kind": "backend", "project_type": "api", "language": "python"},
323
+ {**build_detection("laravel", [
324
+ evidence_if(has_composer_dependency(composer_dep_names, "laravel/framework"), "composer.json -> laravel/framework", 4),
325
+ evidence_if(Path("artisan").exists(), "found artisan", 3),
326
+ ]), "kind": "backend", "project_type": "api", "language": "php"},
327
+ {**build_detection("cakephp", [
328
+ evidence_if(has_composer_dependency(composer_dep_names, "cakephp/cakephp"), "composer.json -> cakephp/cakephp", 4),
329
+ evidence_if(Path("bin/cake").exists(), "found bin/cake", 3),
330
+ evidence_if(Path("config/app.php").exists(), "found config/app.php", 2),
331
+ evidence_if(Path("app/Config/core.php").exists(), "found app/Config/core.php (CakePHP legacy)", 3),
332
+ evidence_if(Path("app/Controller/AppController.php").exists(), "found app/Controller/AppController.php", 2),
333
+ evidence_if(Path("app/Model/AppModel.php").exists(), "found app/Model/AppModel.php", 2),
334
+ evidence_if(Path("lib/Cake").exists(), "found lib/Cake", 3),
335
+ evidence_if(Path("cake/console/cake").exists(), "found cake/console/cake", 3),
336
+ ]), "kind": "backend", "project_type": "api", "language": "php"},
337
+ {**build_detection("symfony", [
338
+ evidence_if(has_composer_dependency(composer_dep_names, "symfony/framework-bundle"), "composer.json -> symfony/framework-bundle", 4),
339
+ evidence_if(Path("bin/console").exists(), "found bin/console", 2),
340
+ evidence_if(Path("config/bundles.php").exists(), "found config/bundles.php", 1),
341
+ ]), "kind": "backend", "project_type": "api", "language": "php"},
342
+ {**build_detection("codeigniter", [
343
+ evidence_if(has_composer_dependency(composer_dep_names, "codeigniter4/framework"), "composer.json -> codeigniter4/framework", 4),
344
+ evidence_if(Path("spark").exists(), "found spark", 2),
345
+ evidence_if(Path("app/Config/App.php").exists(), "found app/Config/App.php", 1),
346
+ ]), "kind": "backend", "project_type": "api", "language": "php"},
347
+ {**build_detection("gin", [
348
+ evidence_if(text_matches(go_mod, r"\bgithub\.com\/gin-gonic\/gin\b"), "go.mod -> github.com/gin-gonic/gin", 4),
349
+ ]), "kind": "backend", "project_type": "api", "language": "go"},
350
+ {**build_detection("fiber", [
351
+ evidence_if(text_matches(go_mod, r"\bgithub\.com\/gofiber\/fiber\b"), "go.mod -> github.com/gofiber/fiber", 4),
352
+ ]), "kind": "backend", "project_type": "api", "language": "go"},
353
+ {**build_detection("echo", [
354
+ evidence_if(text_matches(go_mod, r"\bgithub\.com\/labstack\/echo\b"), "go.mod -> github.com/labstack/echo", 4),
355
+ ]), "kind": "backend", "project_type": "api", "language": "go"},
356
+ {**build_detection("rails", [
357
+ evidence_if(text_matches(gemfile, r'^\s*gem\s+["\']rails["\']'), "Gemfile -> rails", 4),
358
+ evidence_if(Path("bin/rails").exists(), "found bin/rails", 2),
359
+ ]), "kind": "backend", "project_type": "api", "language": "ruby"},
360
+ {**build_detection("spring", [
361
+ evidence_if(text_matches(pom, r"spring-boot") or text_matches(gradle, r"spring-boot"), "Java build -> spring-boot", 4),
362
+ ]), "kind": "backend", "project_type": "api", "language": "java"},
363
+ {**build_detection("aspnet", [
364
+ evidence_if(text_matches(csproj, r"Microsoft\.NET\.Sdk\.Web"), ".csproj -> Microsoft.NET.Sdk.Web", 4),
365
+ evidence_if(text_matches(program_cs, r"WebApplication\.CreateBuilder"), "Program.cs -> WebApplication.CreateBuilder", 2),
366
+ ]), "kind": "backend", "project_type": "api", "language": "csharp"},
367
+ {**build_detection("tauri", [
368
+ evidence_if(has_dependency(dep_names, "@tauri-apps/api"), "package.json -> @tauri-apps/api", 4),
369
+ evidence_if(Path("src-tauri/tauri.conf.json").exists() or Path("src-tauri/tauri.conf.json5").exists(), "found src-tauri/tauri.conf.*", 4),
370
+ evidence_if(Path("src-tauri/Cargo.toml").exists(), "found src-tauri/Cargo.toml", 2),
371
+ ]), "kind": "desktop", "project_type": "desktop", "language": "rust"},
372
+ {**build_detection("axum", [
373
+ evidence_if(text_matches(cargo, r"\baxum\b"), "Cargo.toml -> axum", 4),
374
+ ]), "kind": "backend", "project_type": "api", "language": "rust"},
375
+ {**build_detection("actix", [
376
+ evidence_if(text_matches(cargo, r"\bactix-web\b"), "Cargo.toml -> actix-web", 4),
377
+ ]), "kind": "backend", "project_type": "api", "language": "rust"},
378
+ {**build_detection("flutter", [
379
+ evidence_if(text_matches(pubspec, r"^\s*flutter\s*:"), "pubspec.yaml -> flutter", 4),
380
+ ]), "kind": "mobile", "project_type": "mobile"},
381
+ {**build_detection("react-native", [
382
+ evidence_if(has_dependency(dep_names, "react-native", "expo"), "package.json -> react-native/expo", 4),
383
+ ]), "kind": "mobile", "project_type": "mobile"},
384
+ ]
385
+ framework_details = [item for item in framework_details if item["score"] >= 3]
386
+ framework_details.sort(key=lambda item: item["score"], reverse=True)
387
+ for item in framework_details:
388
+ item["confidence"] = confidence_label(item["score"])
389
+
390
+ framework_language_scores = {}
391
+ for item in framework_details:
392
+ if item.get("language"):
393
+ framework_language_scores[item["language"]] = framework_language_scores.get(item["language"], 0) + item["score"]
394
+ primary_language = max(framework_language_scores, key=framework_language_scores.get) if framework_language_scores else (language_detections[0]["id"] if language_detections else "typescript")
395
+ frameworks = [item["id"] for item in framework_details if item["id"] in FRAMEWORKS_ALL]
396
+ frameworks = uniq(frameworks)
178
397
  infrastructure = []
179
- primary_language = ""
180
398
  project_type = ""
181
399
  database = "none"
182
400
  test_framework = "auto-detect"
183
401
  cloud = "none"
184
402
  ci_cd = "none"
185
403
 
186
- def add_framework(name, condition):
187
- if condition:
188
- frameworks.append(name)
189
-
190
- requirements = read_text_if_exists("requirements.txt")
191
- go_mod = read_text_if_exists("go.mod")
192
- pubspec = read_text_if_exists("pubspec.yaml")
193
-
194
- add_framework("next", "next" in dep_names)
195
- add_framework("react", "react" in dep_names)
196
- add_framework("vue", "vue" in dep_names)
197
- add_framework("nuxt", "nuxt" in dep_names)
198
- add_framework("svelte", "svelte" in dep_names or "@sveltejs/kit" in dep_names)
199
- add_framework("angular", "@angular/core" in dep_names)
200
- add_framework("express", "express" in dep_names)
201
- add_framework("nestjs", "@nestjs/core" in dep_names)
202
- add_framework("fastapi", "fastapi" in requirements.lower())
203
- add_framework("django", "django" in requirements.lower())
204
- add_framework("tauri", "@tauri-apps/api" in dep_names or Path("src-tauri/tauri.conf.json").exists() or Path("src-tauri/tauri.conf.json5").exists())
205
- add_framework("flutter", "flutter:" in pubspec.lower())
206
- add_framework("react-native", "react-native" in dep_names or "expo" in dep_names)
207
- add_framework("laravel", Path("artisan").exists())
208
- add_framework("gin", "github.com/gin-gonic/gin" in go_mod.lower())
209
- add_framework("fiber", "github.com/gofiber/fiber" in go_mod.lower())
210
-
211
- if Path("tsconfig.json").exists() or "typescript" in dep_names: primary_language = "typescript"
212
- elif Path("Cargo.toml").exists() or Path("src-tauri/Cargo.toml").exists(): primary_language = "rust"
213
- elif Path("go.mod").exists(): primary_language = "go"
214
- elif Path("requirements.txt").exists() or Path("pyproject.toml").exists(): primary_language = "python"
215
- elif Path("composer.json").exists(): primary_language = "php"
216
- elif dep_names or Path("package.json").exists(): primary_language = "javascript"
217
-
218
- if "tauri" in frameworks: project_type = "desktop"
219
- elif "flutter" in frameworks or "react-native" in frameworks: project_type = "mobile"
220
- elif any(f in frameworks for f in ["express", "fastapi", "django", "laravel", "gin", "fiber", "nestjs"]):
221
- project_type = "fullstack" if any(f in frameworks for f in ["react", "next", "vue", "nuxt", "svelte", "angular"]) else "api"
222
- elif any(f in frameworks for f in ["react", "next", "vue", "nuxt", "svelte", "angular"]):
223
- project_type = "web-app"
224
- elif Path("package.json").exists() and pkg.get("bin"):
225
- project_type = "cli"
226
-
227
- if "pg" in dep_names or "postgres" in dep_names or "@prisma/client" in dep_names or "postgres" in json.dumps(pkg).lower(): database = "postgres"
228
- elif "mysql" in dep_names or "mysql2" in dep_names: database = "mysql"
229
- elif "sqlite3" in dep_names or "better-sqlite3" in dep_names or "rusqlite" in dep_names: database = "sqlite"
230
- elif "mongodb" in dep_names or "mongoose" in dep_names: database = "mongodb"
231
- elif "redis" in dep_names or "ioredis" in dep_names: database = "redis"
232
- elif "@aws-sdk/client-dynamodb" in dep_names: database = "dynamodb"
233
-
234
- if "vitest" in dep_names: test_framework = "vitest"
235
- elif "jest" in dep_names: test_framework = "jest"
236
- elif "pytest" in dep_names or "pytest" in requirements.lower(): test_framework = "pytest"
237
- elif "phpunit" in dep_names: test_framework = "phpunit"
238
- elif "pest" in dep_names: test_framework = "pest"
404
+ if any(item["project_type"] == "desktop" for item in framework_details): project_type = "desktop"
405
+ elif any(item["project_type"] == "mobile" for item in framework_details): project_type = "mobile"
406
+ elif any(item["kind"] == "frontend" for item in framework_details) and any(item["kind"] == "backend" for item in framework_details): project_type = "fullstack"
407
+ elif any(item["kind"] == "backend" for item in framework_details): project_type = "api"
408
+ elif any(item["kind"] == "frontend" for item in framework_details): project_type = "web-app"
409
+ elif Path("package.json").exists() and pkg.get("bin"): project_type = "cli"
410
+ elif primary_language in ["php", "python", "go", "java", "ruby", "csharp", "rust"]: project_type = "api"
411
+
412
+ if has_dependency(dep_names, "pg", "postgres", "@prisma/client") or "postgres" in json.dumps(pkg).lower(): database = "postgres"
413
+ elif has_dependency(dep_names, "mysql", "mysql2"): database = "mysql"
414
+ elif has_dependency(dep_names, "sqlite3", "better-sqlite3", "rusqlite"): database = "sqlite"
415
+ elif has_dependency(dep_names, "mongodb", "mongoose"): database = "mongodb"
416
+ elif has_dependency(dep_names, "redis", "ioredis"): database = "redis"
417
+ elif has_dependency(dep_names, "@aws-sdk/client-dynamodb"): database = "dynamodb"
418
+
419
+ if has_dependency(dep_names, "vitest"): test_framework = "vitest"
420
+ elif has_dependency(dep_names, "jest"): test_framework = "jest"
421
+ elif has_dependency(dep_names, "pytest") or text_matches(requirements, r"\bpytest\b") or text_matches(pyproject, r"\bpytest\b"): test_framework = "pytest"
422
+ elif has_composer_dependency(composer_dep_names, "phpunit/phpunit"): test_framework = "phpunit"
423
+ elif has_composer_dependency(composer_dep_names, "pestphp/pest"): test_framework = "pest"
239
424
  elif Path("go.mod").exists(): test_framework = "go test"
425
+ elif text_matches(gemfile, r'^\s*gem\s+["\']rspec["\']'): test_framework = "rspec"
240
426
 
241
427
  if Path("Dockerfile").exists() or Path("docker-compose.yml").exists() or Path("docker-compose.yaml").exists():
242
428
  infrastructure.append("docker")
@@ -256,12 +442,19 @@ def scan_project():
256
442
  elif Path(".circleci/config.yml").exists(): ci_cd = "circleci"
257
443
  elif Path("Jenkinsfile").exists(): ci_cd = "jenkins"
258
444
 
445
+ scan_evidence = []
446
+ if language_detections:
447
+ scan_evidence.append(f"language:{language_detections[0]['id']} => {'; '.join(language_detections[0]['evidence'])}")
448
+ scan_evidence.extend(f"{item['id']} [{item['confidence']}] => {'; '.join(item['evidence'])}" for item in framework_details)
449
+
259
450
  return {
260
451
  "project_name": Path.cwd().name,
261
- "description": pkg.get("description", "A software project"),
452
+ "description": pkg.get("description") or composer.get("description", "A software project"),
262
453
  "project_type": project_type if project_type in PROJECT_TYPES_ALL else "web-app",
263
454
  "primary_language": primary_language if primary_language in LANGUAGES_ALL else "typescript",
264
- "frameworks": uniq(frameworks),
455
+ "frameworks": frameworks,
456
+ "framework_details": framework_details,
457
+ "scan_evidence": scan_evidence,
265
458
  "database": database if database in DATABASES_ALL else "none",
266
459
  "package_manager": detect_package_manager(),
267
460
  "uses_docker": "docker" in infrastructure,
@@ -283,7 +476,7 @@ def resolve_agent_table(data):
283
476
  lang = data.get("primary_language", "").lower()
284
477
 
285
478
  # Determine frontend agent
286
- frontend_fws = [f for f in frameworks if f in ["vue", "react", "next", "nuxt", "svelte", "angular"]]
479
+ frontend_fws = [f for f in frameworks if f in FRONTEND_FRAMEWORKS]
287
480
  if frontend_fws:
288
481
  fe_skills = []
289
482
  for fw in frontend_fws:
@@ -292,13 +485,13 @@ def resolve_agent_table(data):
292
485
 
293
486
  # Backend
294
487
  be_skills = list(LANG_SKILLS.get(lang, ["api-design"]))
295
- backend_fws = [f for f in frameworks if f in ["laravel", "express", "fastapi", "django", "gin", "fiber"]]
488
+ backend_fws = [f for f in frameworks if f in BACKEND_FRAMEWORKS]
296
489
  for fw in backend_fws:
297
490
  be_skills.extend(FRAMEWORK_SKILLS.get(fw, []))
298
491
  rows.append(("Backend API", "backend-specialist", ", ".join(set(be_skills[:3]))))
299
492
 
300
493
  # Mobile/Desktop
301
- mobile_fws = [f for f in frameworks if f in ["tauri", "flutter", "react-native"]]
494
+ mobile_fws = [f for f in frameworks if f in MOBILE_DESKTOP_FRAMEWORKS]
302
495
  if mobile_fws:
303
496
  mobile_skills = []
304
497
  for fw in mobile_fws:
@@ -316,20 +509,35 @@ def resolve_agent_table(data):
316
509
 
317
510
  # ─── Generators ──────────────────────────────────────────────────────────────
318
511
 
319
- def generate_project_context(data):
320
- frameworks = data.get("frameworks", [])
321
- infrastructure = data.get("infrastructure", [])
322
- agent_table = resolve_agent_table(data)
323
- auto_skills = resolve_skills(data)
512
+ def generate_project_context(data):
513
+ frameworks = data.get("frameworks", [])
514
+ infrastructure = data.get("infrastructure", [])
515
+ agent_table = resolve_agent_table(data)
516
+ auto_skills = resolve_skills(data)
517
+ scan_evidence = data.get("scan_evidence", [])
324
518
 
325
519
  agent_rows = "\n".join(
326
520
  f"| {task} | `{agent}` | {skills} |"
327
521
  for task, agent, skills in agent_table
328
522
  )
329
523
 
330
- return f"""# PROJECT_CONTEXT.md
331
- # Generated by thachvd-kit init wizard on {datetime.now().strftime('%Y-%m-%d')}
332
- # Edit freely — this is your project's brain.
524
+ scan_evidence_block = ""
525
+ if scan_evidence:
526
+ scan_evidence_block = f"""
527
+ ---
528
+
529
+ ## Scan Evidence
530
+
531
+ These detections were inferred from the current codebase during `init --scan`:
532
+
533
+ ```
534
+ {chr(10).join('- ' + line for line in scan_evidence)}
535
+ ```
536
+ """
537
+
538
+ return f"""# PROJECT_CONTEXT.md
539
+ # Generated by thachvd-kit init wizard on {datetime.now().strftime('%Y-%m-%d')}
540
+ # Edit freely — this is your project's brain.
333
541
 
334
542
  ## Project
335
543
 
@@ -349,9 +557,11 @@ stack:
349
557
  infrastructure: [{', '.join(infrastructure)}]
350
558
  package_manager: {data.get('package_manager', 'auto-detect')}
351
559
  test_framework: {data.get('test_framework', 'auto-detect')}
352
- ```
353
-
354
- ---
560
+ ```
561
+
562
+ {scan_evidence_block}
563
+
564
+ ---
355
565
 
356
566
  ## Agent Routing
357
567
 
@@ -469,6 +679,11 @@ def main():
469
679
  print(f" {dim('Scan mode: detecting stack from current project files...')}")
470
680
  print(f" {bold('Scan result:')} {scanned['primary_language']} | {', '.join(scanned['frameworks']) or 'no framework'}")
471
681
  print(f" {bold('Detected infra:')} {', '.join(scanned['infrastructure']) or 'none'}\n")
682
+ if scanned.get("scan_evidence"):
683
+ print(f" {bold('Evidence:')}")
684
+ for line in scanned["scan_evidence"]:
685
+ print(f" · {line}")
686
+ print()
472
687
  elif not args.yes:
473
688
  print(f" {dim('Manual mode: asking for project context explicitly.')}\n")
474
689
 
@@ -493,7 +708,7 @@ def main():
493
708
  default=(scanned or {}).get("primary_language", "typescript"),
494
709
  choices=LANGUAGES_ALL)
495
710
 
496
- frameworks = ask_multi("Frameworks (select all that apply)", FRAMEWORKS_ALL, default=", ".join((scanned or {}).get("frameworks", [])) or None)
711
+ frameworks = ask_multi("Frameworks (select all that apply)", FRAMEWORKS_ALL, default=(scanned or {}).get("frameworks", None))
497
712
  data["frameworks"] = frameworks or []
498
713
 
499
714
  data["database"] = ask("Database / ORM", default=(scanned or {}).get("database", "postgres"), choices=DATABASES_ALL)
@@ -547,6 +762,8 @@ def main():
547
762
  data["test_framework"] = data.get("test_framework", (scanned or {}).get("test_framework", "auto-detect"))
548
763
  data["max_file_lines"] = data.get("max_file_lines", (scanned or {}).get("max_file_lines", "300"))
549
764
  data["code_style"] = data.get("code_style", (scanned or {}).get("code_style", "project-standard"))
765
+ data["framework_details"] = data.get("framework_details", (scanned or {}).get("framework_details", []))
766
+ data["scan_evidence"] = data.get("scan_evidence", (scanned or {}).get("scan_evidence", []))
550
767
  data["architecture_notes"] = data.get("architecture_notes", (scanned or {}).get("architecture_notes", "<!-- Add notes -->"))
551
768
  data["current_phase"] = data.get("current_phase", (scanned or {}).get("current_phase", "Initial setup"))
552
769
  data["next_phase"] = data.get("next_phase", (scanned or {}).get("next_phase", "—"))