@lakindu_perera/toren 1.0.9 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1629 @@
1
+ /**
2
+ * @fileoverview Toren — Technology Stack Detector (v1.1.0)
3
+ *
4
+ * Detects the technology stack of a project using pre-collected scan context.
5
+ * Does NOT re-scan the repository or re-read package.json.
6
+ *
7
+ * Design contract:
8
+ * - Consumes only the data already collected by scan() and passed in context.
9
+ * - Deterministic: same input always produces the same ordered output.
10
+ * - Confidence model:
11
+ * dependency / devDependency → +0.60 (strong)
12
+ * config file → +0.25 (strong supporting)
13
+ * file / directory evidence → +0.15 (supporting)
14
+ * script keyword → +0.15 (supporting)
15
+ * manifest → +0.15 (supporting)
16
+ * - Confidence is capped at 1.0.
17
+ * - Technologies below the CONFIDENCE_THRESHOLD (0.60) are suppressed.
18
+ * - When multiple rules match the same technology, evidence is merged and
19
+ * confidence is combined (capped), never producing duplicate entries.
20
+ * - Output is ordered by: category rank → confidence desc → name asc.
21
+ *
22
+ * Frontend technologies detected (Step 4):
23
+ * React, Next.js, Vue, Nuxt, Angular, Svelte, SvelteKit, Astro, Remix
24
+ *
25
+ * Styling technologies detected (Step 4):
26
+ * Tailwind CSS, Sass, Less, Styled Components, Emotion, Bootstrap,
27
+ * Material UI, Chakra UI
28
+ *
29
+ * Backend technologies detected (Step 5):
30
+ * Node.js: Express, Fastify, NestJS, Koa, Hapi
31
+ * Python: Django, Flask, FastAPI
32
+ * Java: Spring Boot
33
+ * PHP: Laravel
34
+ * Ruby: Rails
35
+ *
36
+ * Database technologies detected (Step 5):
37
+ * PostgreSQL, MySQL, MariaDB, SQLite, MongoDB, Redis
38
+ *
39
+ * ORM / data-access tools detected (Step 5):
40
+ * Prisma, Drizzle, TypeORM, Sequelize, Mongoose, Knex,
41
+ * SQLAlchemy, Hibernate, Django ORM, Eloquent, Active Record
42
+ *
43
+ * Testing frameworks detected (Step 6):
44
+ * Jest, Vitest, Mocha, Playwright, Cypress, Testing Library,
45
+ * Pytest, JUnit
46
+ *
47
+ * Build tools detected (Step 6):
48
+ * Vite, Webpack, Rollup, esbuild
49
+ *
50
+ * Quality tools detected (Step 6):
51
+ * ESLint, Prettier, Biome, Stylelint
52
+ *
53
+ * Container tools detected (Step 6):
54
+ * Docker, Docker Compose
55
+ *
56
+ * Deployment tools detected (Step 6):
57
+ * Vercel, Netlify, Fly.io, Railway, Render,
58
+ * Serverless Framework, AWS SAM
59
+ *
60
+ * False-positive / safety guards:
61
+ * - Directory names alone never trigger framework detection.
62
+ * - .env / secret files are NEVER read or scanned.
63
+ * - Database URLs / credentials are NEVER used as evidence.
64
+ * - ORM is never guessed from database type; each needs its own evidence.
65
+ * - Plain CSS files are never reported as a technology framework.
66
+ *
67
+ * @module detectors/technology-stack-detector
68
+ */
69
+ // ---------------------------------------------------------------------------
70
+ // Constants
71
+ // ---------------------------------------------------------------------------
72
+ /** Minimum confidence required to include a technology in the output. */
73
+ const CONFIDENCE_THRESHOLD = 0.60;
74
+ /** Confidence contribution per evidence kind. */
75
+ const SCORE = {
76
+ dependency: 0.60,
77
+ devDependency: 0.60,
78
+ peerDependency: 0.60,
79
+ config: 0.25,
80
+ file: 0.15,
81
+ directory: 0.15,
82
+ script: 0.15,
83
+ manifest: 0.60,
84
+ };
85
+ /**
86
+ * Canonical ordering for technology categories.
87
+ * Lower index = listed first in the output.
88
+ */
89
+ const CATEGORY_ORDER = [
90
+ 'language',
91
+ 'runtime',
92
+ 'package-manager',
93
+ 'frontend',
94
+ 'backend',
95
+ 'styling',
96
+ 'database',
97
+ 'orm',
98
+ 'testing',
99
+ 'build',
100
+ 'quality',
101
+ 'container',
102
+ 'deployment',
103
+ 'other',
104
+ ];
105
+ // ---------------------------------------------------------------------------
106
+ // Rule registry
107
+ // ---------------------------------------------------------------------------
108
+ /**
109
+ * All detection rules, evaluated in order.
110
+ * Add new rules here — the engine handles deduplication automatically.
111
+ *
112
+ * Keep this list small for v1.1.0 (proof-of-concept set).
113
+ * Future steps will add comprehensive rules per category.
114
+ */
115
+ const TECH_RULES = [
116
+ // ── TypeScript ────────────────────────────────────────────────────────────
117
+ {
118
+ tech: 'TypeScript',
119
+ category: 'language',
120
+ evidenceType: 'devDependency',
121
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['typescript'] ? 'typescript' : null,
122
+ },
123
+ {
124
+ tech: 'TypeScript',
125
+ category: 'language',
126
+ evidenceType: 'dependency',
127
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['typescript'] ? 'typescript' : null,
128
+ },
129
+ {
130
+ tech: 'TypeScript',
131
+ category: 'language',
132
+ evidenceType: 'config',
133
+ match: ({ configs }) => configs.includes('tsconfig.json') ? 'tsconfig.json' : null,
134
+ },
135
+ {
136
+ tech: 'TypeScript',
137
+ category: 'language',
138
+ evidenceType: 'file',
139
+ match: ({ flatFiles }) => flatFiles.some(f => /\.(ts|tsx)$/.test(f) && !f.endsWith('.d.ts'))
140
+ ? '*.ts / *.tsx source files'
141
+ : null,
142
+ },
143
+ // ── JavaScript ───────────────────────────────────────────────────────────
144
+ {
145
+ tech: 'JavaScript',
146
+ category: 'language',
147
+ evidenceType: 'file',
148
+ match: ({ flatFiles }) => flatFiles.some(f => /\.(js|jsx|mjs|cjs)$/.test(f))
149
+ ? '*.js / *.jsx source files'
150
+ : null,
151
+ },
152
+ {
153
+ tech: 'JavaScript',
154
+ category: 'language',
155
+ evidenceType: 'config',
156
+ match: ({ configs }) => configs.includes('jsconfig.json') ? 'jsconfig.json' : null,
157
+ },
158
+ {
159
+ tech: 'JavaScript',
160
+ category: 'language',
161
+ evidenceType: 'manifest',
162
+ match: ({ flatFiles }) => flatFiles.includes('package.json') ? 'package.json' : null,
163
+ },
164
+ // ==========================================================================
165
+ // FRONTEND FRAMEWORKS
166
+ // ==========================================================================
167
+ // ── React ─────────────────────────────────────────────────────────────────
168
+ // False-positive guard: a directory named "react/" alone must NOT trigger this.
169
+ // Evidence must come from package.json deps or explicit .jsx/.tsx files.
170
+ {
171
+ tech: 'React',
172
+ category: 'frontend',
173
+ evidenceType: 'dependency',
174
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['react'] ? 'react' : null,
175
+ },
176
+ {
177
+ tech: 'React',
178
+ category: 'frontend',
179
+ evidenceType: 'devDependency',
180
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['react'] ? 'react' : null,
181
+ },
182
+ {
183
+ // *.jsx / *.tsx files are supporting evidence only (0.15 alone < threshold).
184
+ tech: 'React',
185
+ category: 'frontend',
186
+ evidenceType: 'file',
187
+ match: ({ flatFiles }) => flatFiles.some(f => /\.(jsx|tsx)$/.test(f)) ? '*.jsx / *.tsx files' : null,
188
+ },
189
+ // ── Next.js ───────────────────────────────────────────────────────────────
190
+ // False-positive guards:
191
+ // - A directory named "next/" alone must NOT trigger detection.
192
+ // - README mentions are not used as evidence (no text-scanning rules).
193
+ // - app/ or pages/ directories are only supporting evidence (< threshold alone).
194
+ {
195
+ tech: 'Next.js',
196
+ category: 'frontend',
197
+ evidenceType: 'dependency',
198
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['next'] ? 'next' : null,
199
+ },
200
+ {
201
+ tech: 'Next.js',
202
+ category: 'frontend',
203
+ evidenceType: 'devDependency',
204
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['next'] ? 'next' : null,
205
+ },
206
+ {
207
+ tech: 'Next.js',
208
+ category: 'frontend',
209
+ evidenceType: 'config',
210
+ match: ({ configs }) => {
211
+ const found = configs.find(c => /^next\.config\.[a-zA-Z0-9]+$/.test(c));
212
+ return found ?? null;
213
+ },
214
+ },
215
+ {
216
+ // app/ or pages/ are supporting evidence only — not standalone triggers.
217
+ tech: 'Next.js',
218
+ category: 'frontend',
219
+ evidenceType: 'directory',
220
+ match: ({ flatFiles }) => flatFiles.some(f => f.startsWith('app/') || f.startsWith('pages/'))
221
+ ? 'app/ or pages/ directory'
222
+ : null,
223
+ },
224
+ // ── Vue ───────────────────────────────────────────────────────────────────
225
+ {
226
+ tech: 'Vue',
227
+ category: 'frontend',
228
+ evidenceType: 'dependency',
229
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['vue'] ? 'vue' : null,
230
+ },
231
+ {
232
+ tech: 'Vue',
233
+ category: 'frontend',
234
+ evidenceType: 'devDependency',
235
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['vue'] ? 'vue' : null,
236
+ },
237
+ {
238
+ // *.vue SFC files — supporting evidence only (0.15 alone < threshold).
239
+ tech: 'Vue',
240
+ category: 'frontend',
241
+ evidenceType: 'file',
242
+ match: ({ flatFiles }) => flatFiles.some(f => f.endsWith('.vue')) ? '*.vue single-file components' : null,
243
+ },
244
+ // ── Nuxt ──────────────────────────────────────────────────────────────────
245
+ {
246
+ tech: 'Nuxt',
247
+ category: 'frontend',
248
+ evidenceType: 'dependency',
249
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['nuxt'] ? 'nuxt' : null,
250
+ },
251
+ {
252
+ tech: 'Nuxt',
253
+ category: 'frontend',
254
+ evidenceType: 'devDependency',
255
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['nuxt'] ? 'nuxt' : null,
256
+ },
257
+ {
258
+ tech: 'Nuxt',
259
+ category: 'frontend',
260
+ evidenceType: 'config',
261
+ match: ({ configs }) => {
262
+ const found = configs.find(c => /^nuxt\.config\.[a-zA-Z0-9]+$/.test(c));
263
+ return found ?? null;
264
+ },
265
+ },
266
+ // ── Angular ───────────────────────────────────────────────────────────────
267
+ {
268
+ tech: 'Angular',
269
+ category: 'frontend',
270
+ evidenceType: 'dependency',
271
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['@angular/core'] ? '@angular/core' : null,
272
+ },
273
+ {
274
+ tech: 'Angular',
275
+ category: 'frontend',
276
+ evidenceType: 'devDependency',
277
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['@angular/core'] ? '@angular/core' : null,
278
+ },
279
+ {
280
+ tech: 'Angular',
281
+ category: 'frontend',
282
+ evidenceType: 'config',
283
+ match: ({ configs }) => configs.includes('angular.json') ? 'angular.json' : null,
284
+ },
285
+ // ── Svelte ────────────────────────────────────────────────────────────────
286
+ {
287
+ tech: 'Svelte',
288
+ category: 'frontend',
289
+ evidenceType: 'dependency',
290
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['svelte'] ? 'svelte' : null,
291
+ },
292
+ {
293
+ tech: 'Svelte',
294
+ category: 'frontend',
295
+ evidenceType: 'devDependency',
296
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['svelte'] ? 'svelte' : null,
297
+ },
298
+ {
299
+ // *.svelte files — supporting evidence only (0.15 alone < threshold).
300
+ tech: 'Svelte',
301
+ category: 'frontend',
302
+ evidenceType: 'file',
303
+ match: ({ flatFiles }) => flatFiles.some(f => f.endsWith('.svelte')) ? '*.svelte component files' : null,
304
+ },
305
+ // ── SvelteKit ─────────────────────────────────────────────────────────────
306
+ // SvelteKit is a distinct meta-framework from bare Svelte — detected separately.
307
+ {
308
+ tech: 'SvelteKit',
309
+ category: 'frontend',
310
+ evidenceType: 'dependency',
311
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['@sveltejs/kit'] ? '@sveltejs/kit' : null,
312
+ },
313
+ {
314
+ tech: 'SvelteKit',
315
+ category: 'frontend',
316
+ evidenceType: 'devDependency',
317
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['@sveltejs/kit'] ? '@sveltejs/kit' : null,
318
+ },
319
+ {
320
+ tech: 'SvelteKit',
321
+ category: 'frontend',
322
+ evidenceType: 'config',
323
+ match: ({ configs }) => {
324
+ const found = configs.find(c => /^svelte\.config\.[a-zA-Z0-9]+$/.test(c));
325
+ return found ?? null;
326
+ },
327
+ },
328
+ // ── Astro ─────────────────────────────────────────────────────────────────
329
+ {
330
+ tech: 'Astro',
331
+ category: 'frontend',
332
+ evidenceType: 'dependency',
333
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['astro'] ? 'astro' : null,
334
+ },
335
+ {
336
+ tech: 'Astro',
337
+ category: 'frontend',
338
+ evidenceType: 'devDependency',
339
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['astro'] ? 'astro' : null,
340
+ },
341
+ {
342
+ tech: 'Astro',
343
+ category: 'frontend',
344
+ evidenceType: 'config',
345
+ match: ({ configs }) => {
346
+ const found = configs.find(c => /^astro\.config\.[a-zA-Z0-9]+$/.test(c));
347
+ return found ?? null;
348
+ },
349
+ },
350
+ // ── Remix ─────────────────────────────────────────────────────────────────
351
+ // Modern Remix (v2+) uses @remix-run/react + @remix-run/node.
352
+ {
353
+ tech: 'Remix',
354
+ category: 'frontend',
355
+ evidenceType: 'dependency',
356
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['@remix-run/react'] ? '@remix-run/react' : null,
357
+ },
358
+ {
359
+ tech: 'Remix',
360
+ category: 'frontend',
361
+ evidenceType: 'devDependency',
362
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['@remix-run/react'] ? '@remix-run/react' : null,
363
+ },
364
+ {
365
+ tech: 'Remix',
366
+ category: 'frontend',
367
+ evidenceType: 'dependency',
368
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['@remix-run/node'] ? '@remix-run/node' : null,
369
+ },
370
+ {
371
+ tech: 'Remix',
372
+ category: 'frontend',
373
+ evidenceType: 'devDependency',
374
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['@remix-run/node'] ? '@remix-run/node' : null,
375
+ },
376
+ // ==========================================================================
377
+ // STYLING
378
+ // ==========================================================================
379
+ // ── Tailwind CSS ──────────────────────────────────────────────────────────
380
+ {
381
+ tech: 'Tailwind CSS',
382
+ category: 'styling',
383
+ evidenceType: 'dependency',
384
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['tailwindcss'] ? 'tailwindcss' : null,
385
+ },
386
+ {
387
+ tech: 'Tailwind CSS',
388
+ category: 'styling',
389
+ evidenceType: 'devDependency',
390
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['tailwindcss'] ? 'tailwindcss' : null,
391
+ },
392
+ {
393
+ tech: 'Tailwind CSS',
394
+ category: 'styling',
395
+ evidenceType: 'config',
396
+ match: ({ configs }) => {
397
+ const found = configs.find(c => /^tailwind\.config\.[a-zA-Z0-9]+$/.test(c));
398
+ return found ?? null;
399
+ },
400
+ },
401
+ // ── Sass ──────────────────────────────────────────────────────────────────
402
+ {
403
+ tech: 'Sass',
404
+ category: 'styling',
405
+ evidenceType: 'dependency',
406
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['sass'] ? 'sass' : null,
407
+ },
408
+ {
409
+ tech: 'Sass',
410
+ category: 'styling',
411
+ evidenceType: 'devDependency',
412
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['sass'] ? 'sass' : null,
413
+ },
414
+ {
415
+ // *.scss files — supporting evidence only (0.15 alone < threshold).
416
+ tech: 'Sass',
417
+ category: 'styling',
418
+ evidenceType: 'file',
419
+ match: ({ flatFiles }) => flatFiles.some(f => f.endsWith('.scss')) ? '*.scss stylesheets' : null,
420
+ },
421
+ // ── Less ──────────────────────────────────────────────────────────────────
422
+ {
423
+ tech: 'Less',
424
+ category: 'styling',
425
+ evidenceType: 'dependency',
426
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['less'] ? 'less' : null,
427
+ },
428
+ {
429
+ tech: 'Less',
430
+ category: 'styling',
431
+ evidenceType: 'devDependency',
432
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['less'] ? 'less' : null,
433
+ },
434
+ {
435
+ tech: 'Less',
436
+ category: 'styling',
437
+ evidenceType: 'file',
438
+ match: ({ flatFiles }) => flatFiles.some(f => f.endsWith('.less')) ? '*.less stylesheets' : null,
439
+ },
440
+ // ── Styled Components ─────────────────────────────────────────────────────
441
+ {
442
+ tech: 'Styled Components',
443
+ category: 'styling',
444
+ evidenceType: 'dependency',
445
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['styled-components'] ? 'styled-components' : null,
446
+ },
447
+ {
448
+ tech: 'Styled Components',
449
+ category: 'styling',
450
+ evidenceType: 'devDependency',
451
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['styled-components'] ? 'styled-components' : null,
452
+ },
453
+ // ── Emotion ───────────────────────────────────────────────────────────────
454
+ {
455
+ tech: 'Emotion',
456
+ category: 'styling',
457
+ evidenceType: 'dependency',
458
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['@emotion/react'] ? '@emotion/react' : null,
459
+ },
460
+ {
461
+ tech: 'Emotion',
462
+ category: 'styling',
463
+ evidenceType: 'devDependency',
464
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['@emotion/react'] ? '@emotion/react' : null,
465
+ },
466
+ // ── Bootstrap ─────────────────────────────────────────────────────────────
467
+ {
468
+ tech: 'Bootstrap',
469
+ category: 'styling',
470
+ evidenceType: 'dependency',
471
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['bootstrap'] ? 'bootstrap' : null,
472
+ },
473
+ {
474
+ tech: 'Bootstrap',
475
+ category: 'styling',
476
+ evidenceType: 'devDependency',
477
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['bootstrap'] ? 'bootstrap' : null,
478
+ },
479
+ // ── Material UI ───────────────────────────────────────────────────────────
480
+ {
481
+ tech: 'Material UI',
482
+ category: 'styling',
483
+ evidenceType: 'dependency',
484
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['@mui/material'] ? '@mui/material' : null,
485
+ },
486
+ {
487
+ tech: 'Material UI',
488
+ category: 'styling',
489
+ evidenceType: 'devDependency',
490
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['@mui/material'] ? '@mui/material' : null,
491
+ },
492
+ // ── Chakra UI ─────────────────────────────────────────────────────────────
493
+ {
494
+ tech: 'Chakra UI',
495
+ category: 'styling',
496
+ evidenceType: 'dependency',
497
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['@chakra-ui/react'] ? '@chakra-ui/react' : null,
498
+ },
499
+ {
500
+ tech: 'Chakra UI',
501
+ category: 'styling',
502
+ evidenceType: 'devDependency',
503
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['@chakra-ui/react'] ? '@chakra-ui/react' : null,
504
+ },
505
+ // ==========================================================================
506
+ // BACKEND FRAMEWORKS
507
+ // ==========================================================================
508
+ //
509
+ // Node.js backends: detected via packageManifest deps.
510
+ // Python/Java/PHP/Ruby: detected via flatFiles manifest/config/file signals,
511
+ // since packageManifest is null for non-Node projects.
512
+ //
513
+ // Safety contract:
514
+ // - .env files are NEVER read or inspected.
515
+ // - No credentials or connection strings are ever accessed.
516
+ // - All evidence is structural (file presence, package names).
517
+ // ── Express ───────────────────────────────────────────────────────────────
518
+ {
519
+ tech: 'Express',
520
+ category: 'backend',
521
+ evidenceType: 'dependency',
522
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['express'] ? 'express' : null,
523
+ },
524
+ {
525
+ tech: 'Express',
526
+ category: 'backend',
527
+ evidenceType: 'devDependency',
528
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['express'] ? 'express' : null,
529
+ },
530
+ // ── Fastify ───────────────────────────────────────────────────────────────
531
+ {
532
+ tech: 'Fastify',
533
+ category: 'backend',
534
+ evidenceType: 'dependency',
535
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['fastify'] ? 'fastify' : null,
536
+ },
537
+ {
538
+ tech: 'Fastify',
539
+ category: 'backend',
540
+ evidenceType: 'devDependency',
541
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['fastify'] ? 'fastify' : null,
542
+ },
543
+ // ── NestJS ────────────────────────────────────────────────────────────────
544
+ {
545
+ tech: 'NestJS',
546
+ category: 'backend',
547
+ evidenceType: 'dependency',
548
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['@nestjs/core'] ? '@nestjs/core' : null,
549
+ },
550
+ {
551
+ tech: 'NestJS',
552
+ category: 'backend',
553
+ evidenceType: 'devDependency',
554
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['@nestjs/core'] ? '@nestjs/core' : null,
555
+ },
556
+ {
557
+ // nest-cli.json is a root-level NestJS CLI config file.
558
+ tech: 'NestJS',
559
+ category: 'backend',
560
+ evidenceType: 'config',
561
+ match: ({ flatFiles }) => flatFiles.includes('nest-cli.json') ? 'nest-cli.json' : null,
562
+ },
563
+ // ── Koa ───────────────────────────────────────────────────────────────────
564
+ {
565
+ tech: 'Koa',
566
+ category: 'backend',
567
+ evidenceType: 'dependency',
568
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['koa'] ? 'koa' : null,
569
+ },
570
+ {
571
+ tech: 'Koa',
572
+ category: 'backend',
573
+ evidenceType: 'devDependency',
574
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['koa'] ? 'koa' : null,
575
+ },
576
+ // ── Hapi ──────────────────────────────────────────────────────────────────
577
+ {
578
+ tech: 'Hapi',
579
+ category: 'backend',
580
+ evidenceType: 'dependency',
581
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['@hapi/hapi'] ? '@hapi/hapi' : null,
582
+ },
583
+ {
584
+ tech: 'Hapi',
585
+ category: 'backend',
586
+ evidenceType: 'devDependency',
587
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['@hapi/hapi'] ? '@hapi/hapi' : null,
588
+ },
589
+ // ── Django ────────────────────────────────────────────────────────────────
590
+ // manage.py at root is the canonical Django management script. It is
591
+ // generated by `django-admin startproject` and is unique to Django.
592
+ // Treated as manifest-level evidence (0.60) so Django crosses the threshold
593
+ // on its own. settings.py provides additional supporting evidence.
594
+ {
595
+ tech: 'Django',
596
+ category: 'backend',
597
+ evidenceType: 'manifest',
598
+ match: ({ flatFiles }) => flatFiles.includes('manage.py') ? 'manage.py' : null,
599
+ },
600
+ {
601
+ tech: 'Django',
602
+ category: 'backend',
603
+ evidenceType: 'file',
604
+ match: ({ flatFiles }) => flatFiles.some(f => f === 'settings.py' || f.endsWith('/settings.py'))
605
+ ? 'settings.py'
606
+ : null,
607
+ },
608
+ // ── Flask ─────────────────────────────────────────────────────────────────
609
+ // app.py at root + a Python manifest (requirements.txt / pyproject.toml /
610
+ // Pipfile) is treated as manifest-level evidence (0.60).
611
+ // wsgi.py provides additional supporting evidence.
612
+ {
613
+ tech: 'Flask',
614
+ category: 'backend',
615
+ evidenceType: 'manifest',
616
+ match: ({ flatFiles }) => {
617
+ const hasApp = flatFiles.includes('app.py');
618
+ const hasPyManifest = flatFiles.includes('requirements.txt') ||
619
+ flatFiles.includes('pyproject.toml') ||
620
+ flatFiles.includes('Pipfile');
621
+ return hasApp && hasPyManifest ? 'app.py' : null;
622
+ },
623
+ },
624
+ {
625
+ tech: 'Flask',
626
+ category: 'backend',
627
+ evidenceType: 'file',
628
+ match: ({ flatFiles }) => flatFiles.some(f => f === 'wsgi.py' || f.endsWith('/wsgi.py'))
629
+ ? 'wsgi.py'
630
+ : null,
631
+ },
632
+ // ── FastAPI ───────────────────────────────────────────────────────────────
633
+ // main.py at root + a Python manifest = manifest-level evidence (0.60).
634
+ // routers/ directory is additional supporting evidence.
635
+ {
636
+ tech: 'FastAPI',
637
+ category: 'backend',
638
+ evidenceType: 'manifest',
639
+ match: ({ flatFiles }) => {
640
+ const hasMain = flatFiles.includes('main.py');
641
+ const hasPyManifest = flatFiles.includes('requirements.txt') ||
642
+ flatFiles.includes('pyproject.toml') ||
643
+ flatFiles.includes('Pipfile');
644
+ return hasMain && hasPyManifest ? 'main.py' : null;
645
+ },
646
+ },
647
+ {
648
+ tech: 'FastAPI',
649
+ category: 'backend',
650
+ evidenceType: 'directory',
651
+ match: ({ flatFiles }) => flatFiles.some(f => f.startsWith('routers/') || f.startsWith('app/routers/'))
652
+ ? 'routers/ directory'
653
+ : null,
654
+ },
655
+ // ── Spring Boot ───────────────────────────────────────────────────────────
656
+ // pom.xml and build.gradle are recognised by detectConfigs (root-level only).
657
+ // *Application.java is the canonical Spring Boot entry-point convention.
658
+ {
659
+ tech: 'Spring Boot',
660
+ category: 'backend',
661
+ evidenceType: 'config',
662
+ match: ({ configs }) => configs.includes('pom.xml') ? 'pom.xml' : null,
663
+ },
664
+ {
665
+ tech: 'Spring Boot',
666
+ category: 'backend',
667
+ evidenceType: 'config',
668
+ match: ({ configs }) => configs.includes('build.gradle') || configs.includes('build.gradle.kts')
669
+ ? (configs.includes('build.gradle') ? 'build.gradle' : 'build.gradle.kts')
670
+ : null,
671
+ },
672
+ {
673
+ tech: 'Spring Boot',
674
+ category: 'backend',
675
+ evidenceType: 'file',
676
+ match: ({ flatFiles }) => flatFiles.some(f => f.endsWith('Application.java'))
677
+ ? 'Application.java'
678
+ : null,
679
+ },
680
+ // ── Laravel ───────────────────────────────────────────────────────────────
681
+ // artisan is present in every Laravel project (0.60 manifest).
682
+ // composer.json provides additional PHP-project confirmation.
683
+ {
684
+ tech: 'Laravel',
685
+ category: 'backend',
686
+ evidenceType: 'manifest',
687
+ match: ({ flatFiles }) => flatFiles.includes('artisan') ? 'artisan' : null,
688
+ },
689
+ {
690
+ tech: 'Laravel',
691
+ category: 'backend',
692
+ evidenceType: 'file',
693
+ match: ({ flatFiles }) => flatFiles.includes('composer.json') ? 'composer.json' : null,
694
+ },
695
+ // ── Rails ─────────────────────────────────────────────────────────────────
696
+ // Gemfile (0.60 manifest) + config/routes.rb (0.15 file) = 0.75.
697
+ {
698
+ tech: 'Rails',
699
+ category: 'backend',
700
+ evidenceType: 'manifest',
701
+ match: ({ flatFiles }) => flatFiles.includes('Gemfile') ? 'Gemfile' : null,
702
+ },
703
+ {
704
+ tech: 'Rails',
705
+ category: 'backend',
706
+ evidenceType: 'file',
707
+ match: ({ flatFiles }) => flatFiles.includes('config/routes.rb') ? 'config/routes.rb' : null,
708
+ },
709
+ // ==========================================================================
710
+ // DATABASES
711
+ // ==========================================================================
712
+ //
713
+ // Evidence: package.json deps OR structural config/schema files.
714
+ // Safety: .env files are NEVER read. DATABASE_URL / connection strings are
715
+ // NEVER used as evidence — only package names and config files.
716
+ // ── PostgreSQL ────────────────────────────────────────────────────────────
717
+ {
718
+ tech: 'PostgreSQL',
719
+ category: 'database',
720
+ evidenceType: 'dependency',
721
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['pg'] ? 'pg' : null,
722
+ },
723
+ {
724
+ tech: 'PostgreSQL',
725
+ category: 'database',
726
+ evidenceType: 'devDependency',
727
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['pg'] ? 'pg' : null,
728
+ },
729
+ {
730
+ tech: 'PostgreSQL',
731
+ category: 'database',
732
+ evidenceType: 'dependency',
733
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['postgres'] ? 'postgres' : null,
734
+ },
735
+ {
736
+ tech: 'PostgreSQL',
737
+ category: 'database',
738
+ evidenceType: 'dependency',
739
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['@neondatabase/serverless']
740
+ ? '@neondatabase/serverless'
741
+ : null,
742
+ },
743
+ // ── MySQL ─────────────────────────────────────────────────────────────────
744
+ {
745
+ tech: 'MySQL',
746
+ category: 'database',
747
+ evidenceType: 'dependency',
748
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['mysql'] ? 'mysql' : null,
749
+ },
750
+ {
751
+ tech: 'MySQL',
752
+ category: 'database',
753
+ evidenceType: 'dependency',
754
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['mysql2'] ? 'mysql2' : null,
755
+ },
756
+ {
757
+ tech: 'MySQL',
758
+ category: 'database',
759
+ evidenceType: 'devDependency',
760
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['mysql2'] ? 'mysql2' : null,
761
+ },
762
+ // ── MariaDB ───────────────────────────────────────────────────────────────
763
+ {
764
+ tech: 'MariaDB',
765
+ category: 'database',
766
+ evidenceType: 'dependency',
767
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['mariadb'] ? 'mariadb' : null,
768
+ },
769
+ {
770
+ tech: 'MariaDB',
771
+ category: 'database',
772
+ evidenceType: 'devDependency',
773
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['mariadb'] ? 'mariadb' : null,
774
+ },
775
+ // ── SQLite ────────────────────────────────────────────────────────────────
776
+ {
777
+ tech: 'SQLite',
778
+ category: 'database',
779
+ evidenceType: 'dependency',
780
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['sqlite3'] ? 'sqlite3' : null,
781
+ },
782
+ {
783
+ tech: 'SQLite',
784
+ category: 'database',
785
+ evidenceType: 'devDependency',
786
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['sqlite3'] ? 'sqlite3' : null,
787
+ },
788
+ {
789
+ tech: 'SQLite',
790
+ category: 'database',
791
+ evidenceType: 'dependency',
792
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['better-sqlite3'] ? 'better-sqlite3' : null,
793
+ },
794
+ {
795
+ tech: 'SQLite',
796
+ category: 'database',
797
+ evidenceType: 'devDependency',
798
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['better-sqlite3'] ? 'better-sqlite3' : null,
799
+ },
800
+ {
801
+ tech: 'SQLite',
802
+ category: 'database',
803
+ evidenceType: 'dependency',
804
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['@libsql/client'] ? '@libsql/client' : null,
805
+ },
806
+ {
807
+ // *.db / *.sqlite files are supporting structural evidence.
808
+ tech: 'SQLite',
809
+ category: 'database',
810
+ evidenceType: 'file',
811
+ match: ({ flatFiles }) => flatFiles.some(f => /\.(db|sqlite|sqlite3)$/.test(f))
812
+ ? '*.db / *.sqlite file'
813
+ : null,
814
+ },
815
+ // ── MongoDB ───────────────────────────────────────────────────────────────
816
+ {
817
+ tech: 'MongoDB',
818
+ category: 'database',
819
+ evidenceType: 'dependency',
820
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['mongodb'] ? 'mongodb' : null,
821
+ },
822
+ {
823
+ tech: 'MongoDB',
824
+ category: 'database',
825
+ evidenceType: 'devDependency',
826
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['mongodb'] ? 'mongodb' : null,
827
+ },
828
+ // ── Redis ─────────────────────────────────────────────────────────────────
829
+ {
830
+ tech: 'Redis',
831
+ category: 'database',
832
+ evidenceType: 'dependency',
833
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['redis'] ? 'redis' : null,
834
+ },
835
+ {
836
+ tech: 'Redis',
837
+ category: 'database',
838
+ evidenceType: 'devDependency',
839
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['redis'] ? 'redis' : null,
840
+ },
841
+ {
842
+ tech: 'Redis',
843
+ category: 'database',
844
+ evidenceType: 'dependency',
845
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['ioredis'] ? 'ioredis' : null,
846
+ },
847
+ {
848
+ tech: 'Redis',
849
+ category: 'database',
850
+ evidenceType: 'devDependency',
851
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['ioredis'] ? 'ioredis' : null,
852
+ },
853
+ // ==========================================================================
854
+ // ORM / DATA-ACCESS TOOLS
855
+ // ==========================================================================
856
+ //
857
+ // Each ORM has independent evidence — never inferred from the database type.
858
+ // Schema/config files are detected via presence only; content is never read.
859
+ // ── Prisma ────────────────────────────────────────────────────────────────
860
+ {
861
+ tech: 'Prisma',
862
+ category: 'orm',
863
+ evidenceType: 'dependency',
864
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['@prisma/client'] ? '@prisma/client' : null,
865
+ },
866
+ {
867
+ tech: 'Prisma',
868
+ category: 'orm',
869
+ evidenceType: 'devDependency',
870
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['prisma'] ? 'prisma' : null,
871
+ },
872
+ {
873
+ tech: 'Prisma',
874
+ category: 'orm',
875
+ evidenceType: 'devDependency',
876
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['@prisma/client'] ? '@prisma/client' : null,
877
+ },
878
+ {
879
+ // schema.prisma presence is structural evidence — file is NOT read.
880
+ tech: 'Prisma',
881
+ category: 'orm',
882
+ evidenceType: 'file',
883
+ match: ({ flatFiles }) => flatFiles.some(f => f === 'prisma/schema.prisma' || f.endsWith('/schema.prisma'))
884
+ ? 'prisma/schema.prisma'
885
+ : null,
886
+ },
887
+ // ── Drizzle ───────────────────────────────────────────────────────────────
888
+ {
889
+ tech: 'Drizzle',
890
+ category: 'orm',
891
+ evidenceType: 'dependency',
892
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['drizzle-orm'] ? 'drizzle-orm' : null,
893
+ },
894
+ {
895
+ tech: 'Drizzle',
896
+ category: 'orm',
897
+ evidenceType: 'devDependency',
898
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['drizzle-orm'] ? 'drizzle-orm' : null,
899
+ },
900
+ {
901
+ tech: 'Drizzle',
902
+ category: 'orm',
903
+ evidenceType: 'devDependency',
904
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['drizzle-kit'] ? 'drizzle-kit' : null,
905
+ },
906
+ // ── TypeORM ───────────────────────────────────────────────────────────────
907
+ {
908
+ tech: 'TypeORM',
909
+ category: 'orm',
910
+ evidenceType: 'dependency',
911
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['typeorm'] ? 'typeorm' : null,
912
+ },
913
+ {
914
+ tech: 'TypeORM',
915
+ category: 'orm',
916
+ evidenceType: 'devDependency',
917
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['typeorm'] ? 'typeorm' : null,
918
+ },
919
+ // ── Sequelize ─────────────────────────────────────────────────────────────
920
+ {
921
+ tech: 'Sequelize',
922
+ category: 'orm',
923
+ evidenceType: 'dependency',
924
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['sequelize'] ? 'sequelize' : null,
925
+ },
926
+ {
927
+ tech: 'Sequelize',
928
+ category: 'orm',
929
+ evidenceType: 'devDependency',
930
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['sequelize'] ? 'sequelize' : null,
931
+ },
932
+ // ── Mongoose ──────────────────────────────────────────────────────────────
933
+ {
934
+ tech: 'Mongoose',
935
+ category: 'orm',
936
+ evidenceType: 'dependency',
937
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['mongoose'] ? 'mongoose' : null,
938
+ },
939
+ {
940
+ tech: 'Mongoose',
941
+ category: 'orm',
942
+ evidenceType: 'devDependency',
943
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['mongoose'] ? 'mongoose' : null,
944
+ },
945
+ // ── Knex ──────────────────────────────────────────────────────────────────
946
+ {
947
+ tech: 'Knex',
948
+ category: 'orm',
949
+ evidenceType: 'dependency',
950
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['knex'] ? 'knex' : null,
951
+ },
952
+ {
953
+ tech: 'Knex',
954
+ category: 'orm',
955
+ evidenceType: 'devDependency',
956
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['knex'] ? 'knex' : null,
957
+ },
958
+ // ── SQLAlchemy ────────────────────────────────────────────────────────────
959
+ // alembic.ini + Python manifest (requirements.txt / pyproject.toml / Pipfile)
960
+ // = manifest-level evidence (0.60). alembic.ini alone = config (0.25 < threshold).
961
+ {
962
+ tech: 'SQLAlchemy',
963
+ category: 'orm',
964
+ evidenceType: 'config',
965
+ match: ({ flatFiles }) => flatFiles.includes('alembic.ini') ? 'alembic.ini' : null,
966
+ },
967
+ {
968
+ tech: 'SQLAlchemy',
969
+ category: 'orm',
970
+ evidenceType: 'manifest',
971
+ match: ({ flatFiles }) => {
972
+ const hasAlembic = flatFiles.includes('alembic.ini');
973
+ const hasPython = flatFiles.includes('requirements.txt') ||
974
+ flatFiles.includes('pyproject.toml') ||
975
+ flatFiles.includes('Pipfile');
976
+ return hasAlembic && hasPython ? 'alembic.ini (SQLAlchemy/Alembic)' : null;
977
+ },
978
+ },
979
+ // ── Hibernate ─────────────────────────────────────────────────────────────
980
+ // persistence.xml (JPA standard) or hibernate.cfg.xml (classic config).
981
+ // pom.xml/build.gradle (0.25 each) + persistence.xml (0.15) = 0.40 < threshold.
982
+ // Use persistence.xml as manifest-level (0.60) when pom.xml is also present.
983
+ {
984
+ tech: 'Hibernate',
985
+ category: 'orm',
986
+ evidenceType: 'manifest',
987
+ match: ({ flatFiles, configs }) => {
988
+ const hasBuildFile = configs.includes('pom.xml') ||
989
+ configs.includes('build.gradle') ||
990
+ configs.includes('build.gradle.kts');
991
+ const hasPersistence = flatFiles.some(f => f.endsWith('persistence.xml'));
992
+ return hasBuildFile && hasPersistence ? 'persistence.xml' : null;
993
+ },
994
+ },
995
+ {
996
+ tech: 'Hibernate',
997
+ category: 'orm',
998
+ evidenceType: 'manifest',
999
+ match: ({ flatFiles, configs }) => {
1000
+ const hasBuildFile = configs.includes('pom.xml') ||
1001
+ configs.includes('build.gradle') ||
1002
+ configs.includes('build.gradle.kts');
1003
+ const hasHibernateCfg = flatFiles.some(f => f.endsWith('hibernate.cfg.xml'));
1004
+ return hasBuildFile && hasHibernateCfg ? 'hibernate.cfg.xml' : null;
1005
+ },
1006
+ },
1007
+ // ── Django ORM ────────────────────────────────────────────────────────────
1008
+ // models.py + manage.py = manifest-level evidence (0.60).
1009
+ {
1010
+ tech: 'Django ORM',
1011
+ category: 'orm',
1012
+ evidenceType: 'manifest',
1013
+ match: ({ flatFiles }) => {
1014
+ const hasManage = flatFiles.includes('manage.py');
1015
+ const hasModels = flatFiles.some(f => f === 'models.py' || f.endsWith('/models.py'));
1016
+ return hasManage && hasModels ? 'models.py' : null;
1017
+ },
1018
+ },
1019
+ // ── Eloquent ──────────────────────────────────────────────────────────────
1020
+ // artisan + app/Models/ directory = manifest-level evidence (0.60).
1021
+ {
1022
+ tech: 'Eloquent',
1023
+ category: 'orm',
1024
+ evidenceType: 'manifest',
1025
+ match: ({ flatFiles }) => {
1026
+ const hasArtisan = flatFiles.includes('artisan');
1027
+ const hasModels = flatFiles.some(f => f.startsWith('app/Models/'));
1028
+ return hasArtisan && hasModels ? 'app/Models/ (Eloquent)' : null;
1029
+ },
1030
+ },
1031
+ // ── Active Record ─────────────────────────────────────────────────────────
1032
+ // Gemfile + db/schema.rb = manifest-level evidence (0.60).
1033
+ {
1034
+ tech: 'Active Record',
1035
+ category: 'orm',
1036
+ evidenceType: 'manifest',
1037
+ match: ({ flatFiles }) => {
1038
+ const hasGemfile = flatFiles.includes('Gemfile');
1039
+ const hasSchema = flatFiles.includes('db/schema.rb');
1040
+ return hasGemfile && hasSchema ? 'db/schema.rb' : null;
1041
+ },
1042
+ },
1043
+ // ==========================================================================
1044
+ // PACKAGE MANAGERS
1045
+ // ==========================================================================
1046
+ // ── npm ───────────────────────────────────────────────────────────────────
1047
+ {
1048
+ tech: 'npm',
1049
+ category: 'package-manager',
1050
+ evidenceType: 'file',
1051
+ match: ({ packageManager, flatFiles }) => packageManager === 'npm' || flatFiles.includes('package-lock.json')
1052
+ ? 'package-lock.json'
1053
+ : null,
1054
+ },
1055
+ // ── pnpm ──────────────────────────────────────────────────────────────────
1056
+ {
1057
+ tech: 'pnpm',
1058
+ category: 'package-manager',
1059
+ evidenceType: 'file',
1060
+ match: ({ packageManager, flatFiles }) => packageManager === 'pnpm' || flatFiles.includes('pnpm-lock.yaml')
1061
+ ? 'pnpm-lock.yaml'
1062
+ : null,
1063
+ },
1064
+ // ── Yarn ──────────────────────────────────────────────────────────────────
1065
+ {
1066
+ tech: 'Yarn',
1067
+ category: 'package-manager',
1068
+ evidenceType: 'file',
1069
+ match: ({ packageManager, flatFiles }) => packageManager === 'yarn' || flatFiles.includes('yarn.lock')
1070
+ ? 'yarn.lock'
1071
+ : null,
1072
+ },
1073
+ // ── Bun ───────────────────────────────────────────────────────────────────
1074
+ {
1075
+ tech: 'Bun',
1076
+ category: 'package-manager',
1077
+ evidenceType: 'file',
1078
+ match: ({ packageManager, flatFiles }) => packageManager === 'bun' ||
1079
+ flatFiles.includes('bun.lock') ||
1080
+ flatFiles.includes('bun.lockb')
1081
+ ? (flatFiles.includes('bun.lockb') ? 'bun.lockb' : 'bun.lock')
1082
+ : null,
1083
+ },
1084
+ // ==========================================================================
1085
+ // TESTING FRAMEWORKS
1086
+ // ==========================================================================
1087
+ // ── Jest ─────────────────────────────────────────────────────────────────────
1088
+ {
1089
+ tech: 'Jest',
1090
+ category: 'testing',
1091
+ evidenceType: 'dependency',
1092
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['jest'] ? 'jest' : null,
1093
+ },
1094
+ {
1095
+ tech: 'Jest',
1096
+ category: 'testing',
1097
+ evidenceType: 'dependency',
1098
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['jest'] ? 'jest' : null,
1099
+ },
1100
+ {
1101
+ tech: 'Jest',
1102
+ category: 'testing',
1103
+ evidenceType: 'manifest',
1104
+ match: ({ configs }) => {
1105
+ const found = configs.find(c => /^jest\.config\.[a-zA-Z0-9]+$/.test(c));
1106
+ return found ?? null;
1107
+ },
1108
+ },
1109
+ {
1110
+ tech: 'Jest',
1111
+ category: 'testing',
1112
+ evidenceType: 'script',
1113
+ match: ({ scripts }) => scripts.some(s => /\bjest\b/.test(s.command)) ? 'jest script' : null,
1114
+ },
1115
+ // ── Vitest ────────────────────────────────────────────────────────────────────
1116
+ {
1117
+ tech: 'Vitest',
1118
+ category: 'testing',
1119
+ evidenceType: 'devDependency',
1120
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['vitest'] ? 'vitest' : null,
1121
+ },
1122
+ {
1123
+ tech: 'Vitest',
1124
+ category: 'testing',
1125
+ evidenceType: 'dependency',
1126
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['vitest'] ? 'vitest' : null,
1127
+ },
1128
+ {
1129
+ tech: 'Vitest',
1130
+ category: 'testing',
1131
+ evidenceType: 'manifest',
1132
+ match: ({ configs }) => {
1133
+ const found = configs.find(c => /^vitest\.config\.[a-zA-Z0-9]+$/.test(c));
1134
+ return found ?? null;
1135
+ },
1136
+ },
1137
+ // ── Mocha ────────────────────────────────────────────────────────────────────
1138
+ {
1139
+ tech: 'Mocha',
1140
+ category: 'testing',
1141
+ evidenceType: 'devDependency',
1142
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['mocha'] ? 'mocha' : null,
1143
+ },
1144
+ {
1145
+ tech: 'Mocha',
1146
+ category: 'testing',
1147
+ evidenceType: 'dependency',
1148
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['mocha'] ? 'mocha' : null,
1149
+ },
1150
+ {
1151
+ tech: 'Mocha',
1152
+ category: 'testing',
1153
+ evidenceType: 'manifest',
1154
+ match: ({ configs }) => configs.includes('.mocharc.yml') || configs.includes('.mocharc.json') ||
1155
+ configs.includes('.mocharc.js') || configs.includes('.mocharc.cjs')
1156
+ ? '.mocharc.*'
1157
+ : null,
1158
+ },
1159
+ // ── Playwright ────────────────────────────────────────────────────────────────
1160
+ {
1161
+ tech: 'Playwright',
1162
+ category: 'testing',
1163
+ evidenceType: 'devDependency',
1164
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['@playwright/test'] ? '@playwright/test' : null,
1165
+ },
1166
+ {
1167
+ tech: 'Playwright',
1168
+ category: 'testing',
1169
+ evidenceType: 'dependency',
1170
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['@playwright/test'] ? '@playwright/test' : null,
1171
+ },
1172
+ {
1173
+ tech: 'Playwright',
1174
+ category: 'testing',
1175
+ evidenceType: 'manifest',
1176
+ match: ({ configs, flatFiles }) => {
1177
+ const found = configs.find(c => /^playwright\.config\.[a-zA-Z0-9]+$/.test(c)) ??
1178
+ flatFiles.find(f => /^playwright\.config\.[a-zA-Z0-9]+$/.test(f.split('/').pop() ?? ''));
1179
+ return found ? 'playwright.config.*' : null;
1180
+ },
1181
+ },
1182
+ // ── Cypress ────────────────────────────────────────────────────────────────────
1183
+ {
1184
+ tech: 'Cypress',
1185
+ category: 'testing',
1186
+ evidenceType: 'devDependency',
1187
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['cypress'] ? 'cypress' : null,
1188
+ },
1189
+ {
1190
+ tech: 'Cypress',
1191
+ category: 'testing',
1192
+ evidenceType: 'dependency',
1193
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['cypress'] ? 'cypress' : null,
1194
+ },
1195
+ {
1196
+ tech: 'Cypress',
1197
+ category: 'testing',
1198
+ evidenceType: 'manifest',
1199
+ match: ({ configs, flatFiles }) => {
1200
+ const found = configs.find(c => /^cypress\.config\.[a-zA-Z0-9]+$/.test(c)) ??
1201
+ flatFiles.find(f => /^cypress\.config\.[a-zA-Z0-9]+$/.test(f.split('/').pop() ?? ''));
1202
+ return found ? 'cypress.config.*' : null;
1203
+ },
1204
+ },
1205
+ // ── Testing Library ──────────────────────────────────────────────────────────
1206
+ {
1207
+ tech: 'Testing Library',
1208
+ category: 'testing',
1209
+ evidenceType: 'devDependency',
1210
+ match: ({ packageManifest }) => {
1211
+ const devDeps = packageManifest?.devDependencies ?? {};
1212
+ const found = Object.keys(devDeps).find(k => k.startsWith('@testing-library/'));
1213
+ return found ?? null;
1214
+ },
1215
+ },
1216
+ {
1217
+ tech: 'Testing Library',
1218
+ category: 'testing',
1219
+ evidenceType: 'dependency',
1220
+ match: ({ packageManifest }) => {
1221
+ const deps = packageManifest?.dependencies ?? {};
1222
+ const found = Object.keys(deps).find(k => k.startsWith('@testing-library/'));
1223
+ return found ?? null;
1224
+ },
1225
+ },
1226
+ // ── Pytest ────────────────────────────────────────────────────────────────────
1227
+ // pytest.ini or pyproject.toml + a Python project = manifest evidence (0.60)
1228
+ {
1229
+ tech: 'Pytest',
1230
+ category: 'testing',
1231
+ evidenceType: 'manifest',
1232
+ match: ({ flatFiles }) => {
1233
+ const hasPytest = flatFiles.includes('pytest.ini') || flatFiles.includes('setup.cfg');
1234
+ const hasPython = flatFiles.includes('requirements.txt') ||
1235
+ flatFiles.includes('pyproject.toml') ||
1236
+ flatFiles.includes('Pipfile');
1237
+ return hasPytest && hasPython ? 'pytest.ini' : null;
1238
+ },
1239
+ },
1240
+ {
1241
+ tech: 'Pytest',
1242
+ category: 'testing',
1243
+ evidenceType: 'file',
1244
+ match: ({ flatFiles }) => {
1245
+ // conftest.py is a pytest-specific file
1246
+ return flatFiles.some(f => f === 'conftest.py' || f.endsWith('/conftest.py'))
1247
+ ? 'conftest.py'
1248
+ : null;
1249
+ },
1250
+ },
1251
+ // ── JUnit ────────────────────────────────────────────────────────────────────
1252
+ // JUnit is detected via Java build files + test source patterns
1253
+ {
1254
+ tech: 'JUnit',
1255
+ category: 'testing',
1256
+ evidenceType: 'manifest',
1257
+ match: ({ flatFiles, configs }) => {
1258
+ const hasBuild = configs.includes('pom.xml') ||
1259
+ configs.includes('build.gradle') ||
1260
+ configs.includes('build.gradle.kts');
1261
+ const hasTestJava = flatFiles.some(f => f.endsWith('Test.java') || f.endsWith('Tests.java'));
1262
+ return hasBuild && hasTestJava ? 'Test.java' : null;
1263
+ },
1264
+ },
1265
+ // ==========================================================================
1266
+ // BUILD TOOLS
1267
+ // ==========================================================================
1268
+ // ── Vite ─────────────────────────────────────────────────────────────────────
1269
+ {
1270
+ tech: 'Vite',
1271
+ category: 'build',
1272
+ evidenceType: 'devDependency',
1273
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['vite'] ? 'vite' : null,
1274
+ },
1275
+ {
1276
+ tech: 'Vite',
1277
+ category: 'build',
1278
+ evidenceType: 'dependency',
1279
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['vite'] ? 'vite' : null,
1280
+ },
1281
+ {
1282
+ tech: 'Vite',
1283
+ category: 'build',
1284
+ evidenceType: 'manifest',
1285
+ match: ({ configs }) => {
1286
+ const found = configs.find(c => /^vite\.config\.[a-zA-Z0-9]+$/.test(c));
1287
+ return found ?? null;
1288
+ },
1289
+ },
1290
+ // ── Webpack ───────────────────────────────────────────────────────────────────
1291
+ // IMPORTANT: Only detect explicit usage. Do NOT detect webpack just because
1292
+ // a framework may use it internally (e.g. Next.js uses webpack internally).
1293
+ {
1294
+ tech: 'Webpack',
1295
+ category: 'build',
1296
+ evidenceType: 'devDependency',
1297
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['webpack'] ? 'webpack' : null,
1298
+ },
1299
+ {
1300
+ tech: 'Webpack',
1301
+ category: 'build',
1302
+ evidenceType: 'dependency',
1303
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['webpack'] ? 'webpack' : null,
1304
+ },
1305
+ {
1306
+ tech: 'Webpack',
1307
+ category: 'build',
1308
+ evidenceType: 'manifest',
1309
+ match: ({ configs, flatFiles }) => {
1310
+ const found = configs.find(c => /^webpack\.config\.[a-zA-Z0-9]+$/.test(c)) ??
1311
+ flatFiles.find(f => /^webpack\.config\.[a-zA-Z0-9]+$/.test(f.split('/').pop() ?? ''));
1312
+ return found ? 'webpack.config.*' : null;
1313
+ },
1314
+ },
1315
+ // ── Rollup ────────────────────────────────────────────────────────────────────
1316
+ {
1317
+ tech: 'Rollup',
1318
+ category: 'build',
1319
+ evidenceType: 'devDependency',
1320
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['rollup'] ? 'rollup' : null,
1321
+ },
1322
+ {
1323
+ tech: 'Rollup',
1324
+ category: 'build',
1325
+ evidenceType: 'dependency',
1326
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['rollup'] ? 'rollup' : null,
1327
+ },
1328
+ {
1329
+ tech: 'Rollup',
1330
+ category: 'build',
1331
+ evidenceType: 'manifest',
1332
+ match: ({ configs, flatFiles }) => {
1333
+ const found = configs.find(c => /^rollup\.config\.[a-zA-Z0-9]+$/.test(c)) ??
1334
+ flatFiles.find(f => /^rollup\.config\.[a-zA-Z0-9]+$/.test(f.split('/').pop() ?? ''));
1335
+ return found ? 'rollup.config.*' : null;
1336
+ },
1337
+ },
1338
+ // ── esbuild ───────────────────────────────────────────────────────────────────
1339
+ {
1340
+ tech: 'esbuild',
1341
+ category: 'build',
1342
+ evidenceType: 'devDependency',
1343
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['esbuild'] ? 'esbuild' : null,
1344
+ },
1345
+ {
1346
+ tech: 'esbuild',
1347
+ category: 'build',
1348
+ evidenceType: 'dependency',
1349
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['esbuild'] ? 'esbuild' : null,
1350
+ },
1351
+ // ==========================================================================
1352
+ // QUALITY TOOLS
1353
+ // ==========================================================================
1354
+ // ── ESLint ────────────────────────────────────────────────────────────────────
1355
+ {
1356
+ tech: 'ESLint',
1357
+ category: 'quality',
1358
+ evidenceType: 'devDependency',
1359
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['eslint'] ? 'eslint' : null,
1360
+ },
1361
+ {
1362
+ tech: 'ESLint',
1363
+ category: 'quality',
1364
+ evidenceType: 'dependency',
1365
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['eslint'] ? 'eslint' : null,
1366
+ },
1367
+ {
1368
+ tech: 'ESLint',
1369
+ category: 'quality',
1370
+ evidenceType: 'manifest',
1371
+ match: ({ configs, flatFiles }) => {
1372
+ // eslint.config.* (flat config, v9+)
1373
+ const flatConfig = configs.find(c => /^eslint\.config\.[a-zA-Z0-9]+$/.test(c));
1374
+ if (flatConfig)
1375
+ return flatConfig;
1376
+ // .eslintrc* legacy configs
1377
+ const legacyConfig = flatFiles.find(f => {
1378
+ const base = f.split('/').pop() ?? '';
1379
+ return /^\.eslintrc(\.json|\.js|\.cjs|\.yml|\.yaml|)$/.test(base);
1380
+ });
1381
+ return legacyConfig ? '.eslintrc*' : null;
1382
+ },
1383
+ },
1384
+ // ── Prettier ──────────────────────────────────────────────────────────────────
1385
+ {
1386
+ tech: 'Prettier',
1387
+ category: 'quality',
1388
+ evidenceType: 'devDependency',
1389
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['prettier'] ? 'prettier' : null,
1390
+ },
1391
+ {
1392
+ tech: 'Prettier',
1393
+ category: 'quality',
1394
+ evidenceType: 'dependency',
1395
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['prettier'] ? 'prettier' : null,
1396
+ },
1397
+ {
1398
+ tech: 'Prettier',
1399
+ category: 'quality',
1400
+ evidenceType: 'manifest',
1401
+ match: ({ flatFiles }) => {
1402
+ const found = flatFiles.find(f => {
1403
+ const base = f.split('/').pop() ?? '';
1404
+ return /^\.prettierrc(\.json|\.js|\.cjs|\.yml|\.yaml|)$/.test(base);
1405
+ });
1406
+ return found ? '.prettierrc*' : null;
1407
+ },
1408
+ },
1409
+ // ── Biome ────────────────────────────────────────────────────────────────────
1410
+ {
1411
+ tech: 'Biome',
1412
+ category: 'quality',
1413
+ evidenceType: 'devDependency',
1414
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['@biomejs/biome'] ? '@biomejs/biome' : null,
1415
+ },
1416
+ {
1417
+ tech: 'Biome',
1418
+ category: 'quality',
1419
+ evidenceType: 'dependency',
1420
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['@biomejs/biome'] ? '@biomejs/biome' : null,
1421
+ },
1422
+ {
1423
+ tech: 'Biome',
1424
+ category: 'quality',
1425
+ evidenceType: 'manifest',
1426
+ match: ({ configs, flatFiles }) => configs.includes('biome.json') || flatFiles.includes('biome.json')
1427
+ ? 'biome.json'
1428
+ : null,
1429
+ },
1430
+ // ── Stylelint ─────────────────────────────────────────────────────────────────
1431
+ {
1432
+ tech: 'Stylelint',
1433
+ category: 'quality',
1434
+ evidenceType: 'devDependency',
1435
+ match: ({ packageManifest }) => packageManifest?.devDependencies?.['stylelint'] ? 'stylelint' : null,
1436
+ },
1437
+ {
1438
+ tech: 'Stylelint',
1439
+ category: 'quality',
1440
+ evidenceType: 'dependency',
1441
+ match: ({ packageManifest }) => packageManifest?.dependencies?.['stylelint'] ? 'stylelint' : null,
1442
+ },
1443
+ {
1444
+ tech: 'Stylelint',
1445
+ category: 'quality',
1446
+ evidenceType: 'manifest',
1447
+ match: ({ flatFiles }) => {
1448
+ const found = flatFiles.find(f => {
1449
+ const base = f.split('/').pop() ?? '';
1450
+ return /^\.stylelintrc(\.json|\.js|\.cjs|\.yml|\.yaml|)$/.test(base) || base === 'stylelint.config.js' || base === 'stylelint.config.cjs';
1451
+ });
1452
+ return found ? '.stylelintrc*' : null;
1453
+ },
1454
+ },
1455
+ // ==========================================================================
1456
+ // CONTAINER
1457
+ // ==========================================================================
1458
+ // ── Docker ────────────────────────────────────────────────────────────────────
1459
+ {
1460
+ tech: 'Docker',
1461
+ category: 'container',
1462
+ evidenceType: 'manifest',
1463
+ match: ({ flatFiles }) => flatFiles.some(f => f === 'Dockerfile' || /\/Dockerfile$/.test(f) || /^Dockerfile\.[a-zA-Z0-9]+$/.test(f.split('/').pop() ?? ''))
1464
+ ? 'Dockerfile'
1465
+ : null,
1466
+ },
1467
+ // ── Docker Compose ────────────────────────────────────────────────────────────
1468
+ {
1469
+ tech: 'Docker Compose',
1470
+ category: 'container',
1471
+ evidenceType: 'manifest',
1472
+ match: ({ flatFiles }) => flatFiles.some(f => {
1473
+ const base = f.split('/').pop() ?? '';
1474
+ return ['docker-compose.yml', 'docker-compose.yaml', 'compose.yml', 'compose.yaml'].includes(base);
1475
+ })
1476
+ ? 'docker-compose.yml'
1477
+ : null,
1478
+ },
1479
+ // ==========================================================================
1480
+ // DEPLOYMENT
1481
+ // ==========================================================================
1482
+ // ── Vercel ────────────────────────────────────────────────────────────────────
1483
+ // Only detect from explicit vercel.json — NOT from Next.js alone.
1484
+ {
1485
+ tech: 'Vercel',
1486
+ category: 'deployment',
1487
+ evidenceType: 'manifest',
1488
+ match: ({ configs, flatFiles }) => configs.includes('vercel.json') || flatFiles.includes('vercel.json')
1489
+ ? 'vercel.json'
1490
+ : null,
1491
+ },
1492
+ // ── Netlify ───────────────────────────────────────────────────────────────────
1493
+ {
1494
+ tech: 'Netlify',
1495
+ category: 'deployment',
1496
+ evidenceType: 'manifest',
1497
+ match: ({ flatFiles }) => flatFiles.includes('netlify.toml') ? 'netlify.toml' : null,
1498
+ },
1499
+ // ── Fly.io ────────────────────────────────────────────────────────────────────
1500
+ {
1501
+ tech: 'Fly.io',
1502
+ category: 'deployment',
1503
+ evidenceType: 'manifest',
1504
+ match: ({ flatFiles }) => flatFiles.includes('fly.toml') ? 'fly.toml' : null,
1505
+ },
1506
+ // ── Railway ────────────────────────────────────────────────────────────────────
1507
+ {
1508
+ tech: 'Railway',
1509
+ category: 'deployment',
1510
+ evidenceType: 'manifest',
1511
+ match: ({ flatFiles }) => flatFiles.includes('railway.json') || flatFiles.includes('railway.toml')
1512
+ ? (flatFiles.includes('railway.json') ? 'railway.json' : 'railway.toml')
1513
+ : null,
1514
+ },
1515
+ // ── Render ────────────────────────────────────────────────────────────────────
1516
+ {
1517
+ tech: 'Render',
1518
+ category: 'deployment',
1519
+ evidenceType: 'manifest',
1520
+ match: ({ flatFiles }) => flatFiles.includes('render.yaml') || flatFiles.includes('render.yml')
1521
+ ? (flatFiles.includes('render.yaml') ? 'render.yaml' : 'render.yml')
1522
+ : null,
1523
+ },
1524
+ // ── Serverless Framework ──────────────────────────────────────────────────────
1525
+ {
1526
+ tech: 'Serverless Framework',
1527
+ category: 'deployment',
1528
+ evidenceType: 'manifest',
1529
+ match: ({ flatFiles }) => flatFiles.includes('serverless.yml') || flatFiles.includes('serverless.yaml') ||
1530
+ flatFiles.includes('serverless.json') || flatFiles.includes('serverless.ts')
1531
+ ? 'serverless.yml'
1532
+ : null,
1533
+ },
1534
+ // ── AWS SAM ───────────────────────────────────────────────────────────────────
1535
+ {
1536
+ tech: 'AWS SAM',
1537
+ category: 'deployment',
1538
+ evidenceType: 'manifest',
1539
+ match: ({ flatFiles }) => flatFiles.includes('template.yaml') || flatFiles.includes('template.yml')
1540
+ ? (flatFiles.includes('template.yaml') ? 'template.yaml' : 'template.yml')
1541
+ : null,
1542
+ },
1543
+ ];
1544
+ // ---------------------------------------------------------------------------
1545
+ // Engine helpers
1546
+ // ---------------------------------------------------------------------------
1547
+ /** Normalise a technology name to a stable deduplication key. */
1548
+ function techKey(name) {
1549
+ return name.toLowerCase().replace(/[\s./]/g, '-');
1550
+ }
1551
+ /** Compute confidence from raw score, capped at 1.0. */
1552
+ function toConfidence(rawScore) {
1553
+ return Math.min(rawScore, 1.0);
1554
+ }
1555
+ /** Category sort rank (lower = first). */
1556
+ function categoryRank(cat) {
1557
+ const idx = CATEGORY_ORDER.indexOf(cat);
1558
+ return idx === -1 ? CATEGORY_ORDER.length : idx;
1559
+ }
1560
+ // ---------------------------------------------------------------------------
1561
+ // Public API
1562
+ // ---------------------------------------------------------------------------
1563
+ /**
1564
+ * Detect the technology stack from already-collected scan context.
1565
+ *
1566
+ * @param context - Pre-collected scan data. No disk I/O is performed.
1567
+ * @returns TechnologyStack with deduplicated, ordered, threshold-filtered
1568
+ * technologies.
1569
+ */
1570
+ export function detectTechnologyStack(context) {
1571
+ // Map from dedup-key → mutable accumulator
1572
+ const accumulators = new Map();
1573
+ for (const rule of TECH_RULES) {
1574
+ const matchedValue = rule.match(context);
1575
+ if (matchedValue == null)
1576
+ continue; // rule did not fire
1577
+ const key = techKey(rule.tech);
1578
+ const evidence = {
1579
+ type: rule.evidenceType,
1580
+ value: matchedValue,
1581
+ };
1582
+ const scoreContribution = SCORE[rule.evidenceType];
1583
+ const existing = accumulators.get(key);
1584
+ if (existing) {
1585
+ // Merge: accumulate score and evidence (avoid duplicate evidence entries)
1586
+ existing.rawScore += scoreContribution;
1587
+ const alreadyHas = existing.evidence.some(e => e.type === evidence.type && e.value === evidence.value);
1588
+ if (!alreadyHas) {
1589
+ existing.evidence.push(evidence);
1590
+ }
1591
+ }
1592
+ else {
1593
+ accumulators.set(key, {
1594
+ name: rule.tech,
1595
+ category: rule.category,
1596
+ rawScore: scoreContribution,
1597
+ evidence: [evidence],
1598
+ });
1599
+ }
1600
+ }
1601
+ // Build Technology objects, applying confidence threshold
1602
+ const technologies = [];
1603
+ for (const acc of accumulators.values()) {
1604
+ const confidence = toConfidence(acc.rawScore);
1605
+ if (confidence < CONFIDENCE_THRESHOLD)
1606
+ continue; // suppress weak guesses
1607
+ technologies.push({
1608
+ name: acc.name,
1609
+ category: acc.category,
1610
+ confidence,
1611
+ evidence: acc.evidence,
1612
+ });
1613
+ }
1614
+ // Deterministic ordering:
1615
+ // 1. Category rank (ascending)
1616
+ // 2. Confidence (descending)
1617
+ // 3. Name (ascending, alphabetical tie-breaker)
1618
+ technologies.sort((a, b) => {
1619
+ const catDiff = categoryRank(a.category) - categoryRank(b.category);
1620
+ if (catDiff !== 0)
1621
+ return catDiff;
1622
+ const confDiff = b.confidence - a.confidence;
1623
+ if (confDiff !== 0)
1624
+ return confDiff;
1625
+ return a.name.localeCompare(b.name);
1626
+ });
1627
+ return { technologies };
1628
+ }
1629
+ //# sourceMappingURL=technology-stack-detector.js.map