flamerjs 0.0.0-stage → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +204 -2
  3. package/dist/cli.js +3308 -0
  4. package/package.json +49 -4
package/dist/cli.js ADDED
@@ -0,0 +1,3308 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { writeFileSync, existsSync as existsSync3 } from "fs";
5
+ import { resolve, basename } from "path";
6
+
7
+ // src/util/fs.ts
8
+ import {
9
+ readdirSync,
10
+ readFileSync,
11
+ statSync,
12
+ lstatSync,
13
+ existsSync as fsExists,
14
+ openSync,
15
+ readSync,
16
+ closeSync
17
+ } from "fs";
18
+ import { join, relative, extname, sep } from "path";
19
+ var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
20
+ "node_modules",
21
+ "bower_components",
22
+ "jspm_packages",
23
+ ".pnpm-store",
24
+ ".yarn",
25
+ ".next",
26
+ ".nuxt",
27
+ ".svelte-kit",
28
+ ".astro",
29
+ ".docusaurus",
30
+ ".vercel",
31
+ ".netlify",
32
+ ".turbo",
33
+ ".parcel-cache",
34
+ ".angular",
35
+ ".vite",
36
+ "storybook-static",
37
+ "dist",
38
+ "build",
39
+ "out",
40
+ ".output",
41
+ ".git",
42
+ ".hg",
43
+ ".svn",
44
+ ".bzr",
45
+ "CVS",
46
+ ".idea",
47
+ ".vs",
48
+ ".vscode-test",
49
+ "coverage",
50
+ ".nyc_output",
51
+ "htmlcov",
52
+ ".tox",
53
+ ".nox",
54
+ ".pytest_cache",
55
+ ".mypy_cache",
56
+ ".ruff_cache",
57
+ ".hypothesis",
58
+ ".gradle",
59
+ ".m2",
60
+ ".dart_tool",
61
+ ".pub-cache",
62
+ "__pycache__",
63
+ ".venv",
64
+ "venv",
65
+ "env",
66
+ "virtualenv",
67
+ ".eggs",
68
+ "*.egg-info",
69
+ "site-packages",
70
+ ".Python",
71
+ "pip-wheel-metadata",
72
+ "target",
73
+ "vendor",
74
+ "Pods",
75
+ "DerivedData",
76
+ "bin",
77
+ "obj",
78
+ ".stack-work",
79
+ "cabal",
80
+ "_build",
81
+ "zig-cache",
82
+ "zig-out",
83
+ ".ccls-cache",
84
+ ".clangd",
85
+ ".terraform",
86
+ ".terragrunt-cache",
87
+ ".serverless",
88
+ ".pulumi",
89
+ "cdk.out",
90
+ "tmp",
91
+ "temp",
92
+ ".tmp",
93
+ ".cache",
94
+ ".sass-cache",
95
+ ".ipynb_checkpoints",
96
+ ".history",
97
+ ".yarn-backups",
98
+ ".eslintcache",
99
+ ".DS_Store",
100
+ "__snapshots__"
101
+ ]);
102
+ var ALLOW_HIDDEN_DIRS = /* @__PURE__ */ new Set([
103
+ ".github",
104
+ ".gitlab",
105
+ ".circleci",
106
+ ".config",
107
+ ".husky",
108
+ ".changeset",
109
+ ".devcontainer",
110
+ ".claude",
111
+ ".cursor",
112
+ ".github-workflows",
113
+ ".cargo",
114
+ ".clion"
115
+ ]);
116
+ var LOCKFILES = /* @__PURE__ */ new Set([
117
+ "package-lock.json",
118
+ "yarn.lock",
119
+ "pnpm-lock.yaml",
120
+ "bun.lockb",
121
+ "bun.lock",
122
+ "npm-shrinkwrap.json",
123
+ "deno.lock",
124
+ "composer.lock",
125
+ "Gemfile.lock",
126
+ "poetry.lock",
127
+ "Pipfile.lock",
128
+ "uv.lock",
129
+ "pdm.lock",
130
+ "Cargo.lock",
131
+ "go.sum",
132
+ "Gopkg.lock",
133
+ "mix.lock",
134
+ "flake.lock",
135
+ "gradle.lockfile",
136
+ "paket.lock",
137
+ "packages.lock.json",
138
+ "pubspec.lock",
139
+ "Podfile.lock",
140
+ "Package.resolved",
141
+ "packages.config",
142
+ "cabal.project.freeze",
143
+ "conan.lock",
144
+ ".terraform.lock.hcl",
145
+ "bun.lockb"
146
+ ]);
147
+ var GENERATED_SUFFIXES = [
148
+ ".min.js",
149
+ ".min.css",
150
+ ".bundle.js",
151
+ ".chunk.js",
152
+ ".map",
153
+ ".lock",
154
+ ".pb.go",
155
+ "_pb2.py",
156
+ "_pb2_grpc.py",
157
+ ".designer.cs",
158
+ ".g.cs",
159
+ ".g.dart",
160
+ ".freezed.dart",
161
+ "_generated.go",
162
+ ".generated.ts",
163
+ ".gen.go",
164
+ ".d.ts"
165
+ ];
166
+ function readText(file) {
167
+ try {
168
+ return readFileSync(file, "utf-8");
169
+ } catch {
170
+ return "";
171
+ }
172
+ }
173
+ function readJSON(file) {
174
+ try {
175
+ return JSON.parse(readFileSync(file, "utf-8"));
176
+ } catch {
177
+ return null;
178
+ }
179
+ }
180
+ function exists(p) {
181
+ try {
182
+ return fsExists(p);
183
+ } catch {
184
+ return false;
185
+ }
186
+ }
187
+ var existsSync = exists;
188
+ function isDir(p) {
189
+ try {
190
+ return statSync(p).isDirectory();
191
+ } catch {
192
+ return false;
193
+ }
194
+ }
195
+ function looksBinary(buf) {
196
+ const len = Math.min(buf.length, 4096);
197
+ if (len === 0) return false;
198
+ for (let i = 0; i < len; i++) {
199
+ if (buf[i] === 0) return true;
200
+ }
201
+ let control = 0;
202
+ for (let i = 0; i < len; i++) {
203
+ const b = buf[i];
204
+ if (b < 32 && b !== 9 && b !== 10 && b !== 13 && b !== 12) control++;
205
+ }
206
+ if (control / len > 0.1) return true;
207
+ const decoded = utf8Decoder.decode(buf.subarray(0, len));
208
+ if (!decoded.slice(0, -4).includes("\uFFFD")) return false;
209
+ let high = 0;
210
+ for (let i = 0; i < len; i++) {
211
+ if (buf[i] > 126) high++;
212
+ }
213
+ return high / len > 0.3;
214
+ }
215
+ var utf8Decoder = new TextDecoder("utf-8", { fatal: false });
216
+ function readHead(file, bytes = 4096) {
217
+ let fd;
218
+ try {
219
+ fd = openSync(file, "r");
220
+ const buf = Buffer.alloc(bytes);
221
+ const read = readSync(fd, buf, 0, bytes, 0);
222
+ return buf.subarray(0, read);
223
+ } catch {
224
+ return Buffer.alloc(0);
225
+ } finally {
226
+ if (fd !== void 0) {
227
+ try {
228
+ closeSync(fd);
229
+ } catch {
230
+ }
231
+ }
232
+ }
233
+ }
234
+ function safeStat(p) {
235
+ try {
236
+ return lstatSync(p);
237
+ } catch {
238
+ return null;
239
+ }
240
+ }
241
+ function rel(root, abs) {
242
+ return relative(root, abs).split(sep).join("/");
243
+ }
244
+ function isGeneratedName(name) {
245
+ const lower = name.toLowerCase();
246
+ if (GENERATED_SUFFIXES.some((s) => lower.endsWith(s))) return true;
247
+ if (/\.min\.(js|css)$/.test(lower)) return true;
248
+ if (/(^|[._-])(generated|gen|autogen|auto_gen|__generated__)([._-]|$)/.test(lower)) return true;
249
+ return false;
250
+ }
251
+ function walkTree(root, opts = {}) {
252
+ const {
253
+ extraIgnores = /* @__PURE__ */ new Set(),
254
+ followSymlinks = false,
255
+ onFile = () => {
256
+ },
257
+ onDir = () => {
258
+ }
259
+ } = opts;
260
+ const ignoreDirs = /* @__PURE__ */ new Set([...DEFAULT_IGNORE_DIRS, ...extraIgnores]);
261
+ const files = [];
262
+ const dirs = [];
263
+ let skippedBytes = 0;
264
+ const stack = [{ abs: root, rel: "", depth: 0 }];
265
+ while (stack.length) {
266
+ const { abs: dirAbs, rel: dirRel, depth } = stack.pop();
267
+ let entries;
268
+ try {
269
+ entries = readdirSync(dirAbs, { withFileTypes: true });
270
+ } catch {
271
+ continue;
272
+ }
273
+ for (const entry of entries) {
274
+ const name = entry.name;
275
+ const abs = join(dirAbs, name);
276
+ const r = rel(root, abs);
277
+ const isHidden = name.startsWith(".");
278
+ let isDirectory = entry.isDirectory();
279
+ let isSymlink = entry.isSymbolicLink();
280
+ if (isSymlink) {
281
+ if (!followSymlinks) continue;
282
+ const st2 = safeStat(abs);
283
+ if (!st2) continue;
284
+ isDirectory = st2.isDirectory();
285
+ isSymlink = false;
286
+ }
287
+ if (isDirectory) {
288
+ if (ignoreDirs.has(name)) continue;
289
+ if (isHidden && !ALLOW_HIDDEN_DIRS.has(name)) continue;
290
+ dirs.push(r);
291
+ onDir(abs, name, r);
292
+ stack.push({ abs, rel: r, depth: depth + 1 });
293
+ continue;
294
+ }
295
+ if (!entry.isFile() && !isSymlink) continue;
296
+ if (isHidden && depth > 0 && !ALLOW_HIDDEN_DIRS.has(dirRel.split("/")[0])) {
297
+ const st2 = safeStat(abs);
298
+ if (st2) {
299
+ onFile({
300
+ abs,
301
+ path: abs,
302
+ rel: r,
303
+ name,
304
+ ext: extname(name),
305
+ size: st2.size,
306
+ depth,
307
+ hidden: true
308
+ });
309
+ files.push({ abs, rel: r, name, ext: extname(name), size: st2.size, depth, hidden: true });
310
+ }
311
+ continue;
312
+ }
313
+ const st = safeStat(abs);
314
+ if (!st || !st.isFile()) continue;
315
+ skippedBytes += st.size;
316
+ const rec = { abs, rel: r, name, ext: extname(name), size: st.size, depth, hidden: isHidden };
317
+ onFile(rec);
318
+ files.push(rec);
319
+ }
320
+ }
321
+ return { files, dirs, truncated: false, skippedBytes };
322
+ }
323
+ function subdirs(dir) {
324
+ try {
325
+ return readdirSync(dir, { withFileTypes: true }).filter((e) => (e.isDirectory() || e.isSymbolicLink()) && !e.name.startsWith(".")).map((e) => e.name);
326
+ } catch {
327
+ return [];
328
+ }
329
+ }
330
+ function filesIn(dir) {
331
+ try {
332
+ return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile()).map((e) => e.name);
333
+ } catch {
334
+ return [];
335
+ }
336
+ }
337
+
338
+ // src/util/findings.ts
339
+ var SEVERITIES = ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"];
340
+ var RANK = Object.fromEntries(SEVERITIES.map((s, i) => [s, i]));
341
+ var CATEGORIES = [
342
+ "security",
343
+ "secrets",
344
+ "deps",
345
+ "testing",
346
+ "bloat",
347
+ "hygiene",
348
+ "markers",
349
+ "vcs",
350
+ "config",
351
+ "build",
352
+ "script",
353
+ "structure",
354
+ "git",
355
+ "error",
356
+ "unfinished"
357
+ ];
358
+ function rank(severity) {
359
+ const r = RANK[String(severity).toUpperCase()];
360
+ return r === void 0 ? SEVERITIES.length : r;
361
+ }
362
+ function finding(f) {
363
+ const severity = String(f.severity || "LOW").toUpperCase();
364
+ if (RANK[severity] === void 0) {
365
+ throw new Error(`Unknown severity "${f.severity}" (want one of ${SEVERITIES.join(", ")})`);
366
+ }
367
+ const category = f.category || "hygiene";
368
+ if (!CATEGORIES.includes(category)) {
369
+ throw new Error(`Unknown category "${f.category}" (want one of ${CATEGORIES.join(", ")})`);
370
+ }
371
+ return {
372
+ category,
373
+ severity,
374
+ title: f.title || "Untitled finding",
375
+ detail: f.detail || "",
376
+ ...f.files && f.files.length ? { files: dedupe(f.files).slice(0, 25) } : {},
377
+ ...f.count !== void 0 ? { count: f.count } : {},
378
+ ...f.evidence !== void 0 ? { evidence: f.evidence } : {},
379
+ ...f.lang ? { lang: f.lang } : {}
380
+ };
381
+ }
382
+ function dedupe(arr) {
383
+ return [...new Set(arr)];
384
+ }
385
+ function sortFindings(findings) {
386
+ return [...findings].sort(
387
+ (a, b) => rank(a.severity) - rank(b.severity) || a.category.localeCompare(b.category) || a.title.localeCompare(b.title)
388
+ );
389
+ }
390
+ function severityTally(findings) {
391
+ const tally = Object.fromEntries(SEVERITIES.map((s) => [s, 0]));
392
+ for (const f of findings) tally[f.severity] = (tally[f.severity] || 0) + 1;
393
+ return tally;
394
+ }
395
+ function worstSeverity(findings) {
396
+ let worst = null;
397
+ for (const f of findings) {
398
+ if (worst === null || rank(f.severity) < rank(worst)) worst = f.severity;
399
+ }
400
+ return worst;
401
+ }
402
+
403
+ // src/detect.ts
404
+ import { join as join2 } from "path";
405
+ import { existsSync as existsSync2 } from "fs";
406
+
407
+ // src/registry.ts
408
+ var LANGUAGES = [
409
+ {
410
+ id: "javascript",
411
+ label: "JavaScript",
412
+ exts: [".js", ".jsx", ".mjs", ".cjs"],
413
+ filenames: ["package.json"],
414
+ comment: { line: "//", block: [["/*", "*/"]] },
415
+ bloat: { soft: 300, hard: 700 },
416
+ tests: [/\.(test|spec)\.(js|jsx|mjs|cjs)$/, /(^|\/)__tests__\//, /(^|\/)test(s)?\//],
417
+ manifests: ["package.json"],
418
+ linters: [
419
+ ".eslintrc",
420
+ ".eslintrc.js",
421
+ ".eslintrc.json",
422
+ ".eslintrc.cjs",
423
+ "eslint.config.js",
424
+ "eslint.config.mjs",
425
+ "biome.json"
426
+ ],
427
+ lockfiles: ["package-lock.json", "yarn.lock", "pnpm-lock.yaml", "bun.lockb"]
428
+ },
429
+ {
430
+ id: "typescript",
431
+ label: "TypeScript",
432
+ exts: [".ts", ".tsx", ".mts", ".cts"],
433
+ filenames: ["tsconfig.json"],
434
+ comment: { line: "//", block: [["/*", "*/"]] },
435
+ bloat: { soft: 300, hard: 700 },
436
+ tests: [/\.(test|spec)\.(ts|tsx|mts|cts)$/, /(^|\/)__tests__\//],
437
+ manifests: ["tsconfig.json"],
438
+ linters: ["tsconfig.json"],
439
+ lockfiles: ["package-lock.json", "yarn.lock", "pnpm-lock.yaml", "bun.lockb"]
440
+ },
441
+ {
442
+ id: "python",
443
+ label: "Python",
444
+ exts: [".py", ".pyi", ".pyw"],
445
+ filenames: [
446
+ "pyproject.toml",
447
+ "setup.py",
448
+ "setup.cfg",
449
+ "requirements.txt",
450
+ "Pipfile",
451
+ "poetry.lock"
452
+ ],
453
+ comment: { line: "#", block: [] },
454
+ bloat: { soft: 400, hard: 900 },
455
+ tests: [/(^|\/)tests?\//, /(^|\/)test_.*\.py$/, /_test\.py$/, /(^|\/)conftest\.py$/],
456
+ manifests: ["pyproject.toml", "setup.py", "requirements.txt", "Pipfile", "setup.cfg"],
457
+ linters: [".flake8", "ruff.toml", ".ruff.toml", "mypy.ini", ".pylintrc", "pyproject.toml"],
458
+ lockfiles: ["poetry.lock", "Pipfile.lock", "uv.lock", "requirements.lock", "pdm.lock"]
459
+ },
460
+ {
461
+ id: "rust",
462
+ label: "Rust",
463
+ exts: [".rs"],
464
+ filenames: ["Cargo.toml", "Cargo.lock", "rust-toolchain.toml", "clippy.toml", "rustfmt.toml"],
465
+ comment: { line: "//", block: [["/*", "*/"]] },
466
+ bloat: { soft: 450, hard: 1e3 },
467
+ tests: [/(^|\/)tests?\//, /_test\.rs$/, /\/tests\/.*\.rs$/],
468
+ manifests: ["Cargo.toml"],
469
+ linters: ["clippy.toml", "rustfmt.toml", ".rustfmt.toml", "deny.toml"],
470
+ lockfiles: ["Cargo.lock"]
471
+ },
472
+ {
473
+ id: "go",
474
+ label: "Go",
475
+ exts: [".go"],
476
+ filenames: ["go.mod", "go.sum", "go.work"],
477
+ comment: { line: "//", block: [["/*", "*/"]] },
478
+ bloat: { soft: 450, hard: 900 },
479
+ tests: [/_test\.go$/, /(^|\/)tests?\//],
480
+ manifests: ["go.mod", "go.work"],
481
+ linters: [".golangci.yml", ".golangci.yaml", "golangci.yml"],
482
+ lockfiles: ["go.sum"]
483
+ },
484
+ {
485
+ id: "c",
486
+ label: "C",
487
+ exts: [".c", ".h"],
488
+ filenames: ["Makefile", "makefile", "CMakeLists.txt", "meson.build", "configure.ac"],
489
+ comment: { line: "//", block: [["/*", "*/"]] },
490
+ bloat: { soft: 500, hard: 1200 },
491
+ tests: [/(^|\/)tests?\//, /(^|\/)test_[^/]+\.c$/, /_test\.c$/],
492
+ manifests: ["Makefile", "CMakeLists.txt", "meson.build", "configure.ac"],
493
+ linters: [".clang-format", ".clang-tidy", ".editorconfig"],
494
+ lockfiles: ["conan.lock", "conanfile.txt"]
495
+ },
496
+ {
497
+ id: "cpp",
498
+ label: "C++",
499
+ exts: [".cpp", ".cc", ".cxx", ".hpp", ".hh", ".hxx", ".ipp", ".tpp", ".mm"],
500
+ filenames: ["CMakeLists.txt"],
501
+ comment: { line: "//", block: [["/*", "*/"]] },
502
+ bloat: { soft: 500, hard: 1200 },
503
+ tests: [/(^|\/)tests?\//, /_test\.cc$|test.*\.cpp$/],
504
+ manifests: ["CMakeLists.txt", "meson.build"],
505
+ linters: [".clang-format", ".clang-tidy"],
506
+ lockfiles: ["conan.lock"]
507
+ },
508
+ {
509
+ id: "csharp",
510
+ label: "C#",
511
+ exts: [".cs", ".csx", ".vb", ".fs"],
512
+ // .editorconfig is deliberately absent: it is not tied to one ecosystem and would
513
+ // make every repo that has one look like a C# project.
514
+ filenames: ["global.json", "Directory.Build.props"],
515
+ comment: { line: "//", block: [["/*", "*/"]] },
516
+ bloat: { soft: 400, hard: 900 },
517
+ tests: [/\.Tests?\/.*\.cs$/, /(^|\/)tests?\//, /Tests\.cs$/],
518
+ manifests: [".sln", "global.json"],
519
+ linters: [".editorconfig", "stylecop.json", ".globalconfig"],
520
+ lockfiles: ["packages.lock.json"]
521
+ },
522
+ {
523
+ id: "java",
524
+ label: "Java / JVM",
525
+ exts: [".java", ".kt", ".kts", ".scala", ".sc", ".groovy", ".clj", ".cljs"],
526
+ filenames: ["pom.xml", "build.gradle", "build.gradle.kts", "settings.gradle"],
527
+ comment: { line: "//", block: [["/*", "*/"]] },
528
+ bloat: { soft: 450, hard: 1e3 },
529
+ tests: [/(^|\/)src\/test\//, /(^|\/)tests?\//, /Test\.java$|Tests\.kt$/],
530
+ manifests: ["pom.xml", "build.gradle", "build.gradle.kts"],
531
+ linters: ["checkstyle.xml", "spotbugs-exclude.xml", "ktlint", ".editorconfig"],
532
+ lockfiles: ["gradle.lockfile", "pom.xml"]
533
+ },
534
+ {
535
+ id: "ruby",
536
+ label: "Ruby",
537
+ exts: [".rb", ".rake", ".gemspec"],
538
+ filenames: ["Gemfile", "Gemfile.lock", "Rakefile", ".ruby-version"],
539
+ comment: { line: "#", block: [] },
540
+ bloat: { soft: 350, hard: 800 },
541
+ tests: [/(^|\/)spec\//, /_spec\.rb$/, /(^|\/)test(s)?\//],
542
+ manifests: ["Gemfile", "*.gemspec"],
543
+ linters: [".rubocop.yml", ".standard.yml"],
544
+ lockfiles: ["Gemfile.lock"]
545
+ },
546
+ {
547
+ id: "php",
548
+ label: "PHP",
549
+ exts: [".php", ".phtml"],
550
+ filenames: ["composer.json", "composer.lock", "artisan", "phpunit.xml"],
551
+ comment: {
552
+ line: "//",
553
+ block: [
554
+ ["/*", "*/"],
555
+ ["#", "#"]
556
+ ]
557
+ },
558
+ bloat: { soft: 450, hard: 1e3 },
559
+ tests: [/(^|\/)tests?\//, /Test\.php$/],
560
+ manifests: ["composer.json"],
561
+ linters: ["phpcs.xml", "phpcs.xml.dist", ".php-cs-fixer.php", "phpstan.neon"],
562
+ lockfiles: ["composer.lock"]
563
+ },
564
+ {
565
+ id: "shell",
566
+ label: "Shell",
567
+ exts: [".sh", ".bash", ".zsh", ".fish", ".ksh"],
568
+ filenames: [],
569
+ comment: { line: "#", block: [] },
570
+ bloat: { soft: 250, hard: 600 },
571
+ tests: [/(^|\/)tests?\//, /\.bats$/, /\.shspec$/],
572
+ manifests: [],
573
+ linters: [".shellcheckrc", ".editorconfig"],
574
+ lockfiles: []
575
+ },
576
+ {
577
+ id: "powershell",
578
+ label: "PowerShell",
579
+ exts: [".ps1", ".psm1", ".psd1"],
580
+ filenames: [],
581
+ comment: { line: "#", block: [["<#", "#>"]] },
582
+ bloat: { soft: 300, hard: 700 },
583
+ tests: [/\.Tests\.ps1$/, /(^|\/)tests?\//],
584
+ manifests: [],
585
+ linters: ["PSScriptAnalyzerSettings.psd1", ".editorconfig"],
586
+ lockfiles: []
587
+ },
588
+ {
589
+ id: "sql",
590
+ label: "SQL",
591
+ exts: [".sql", ".ddl", ".dml"],
592
+ filenames: [],
593
+ comment: { line: "--", block: [["/*", "*/"]] },
594
+ bloat: { soft: 600, hard: 1500 },
595
+ tests: [/(^|\/)tests?\//, /_test\.sql$/],
596
+ manifests: [],
597
+ linters: [".sqlfluff", ".editorconfig"],
598
+ lockfiles: []
599
+ },
600
+ {
601
+ id: "terraform",
602
+ label: "Terraform / HCL",
603
+ exts: [".tf", ".tfvars", ".hcl"],
604
+ filenames: [".terraformrc", "terragrunt.hcl", "Chart.yaml", "values.yaml"],
605
+ comment: { line: "#", block: [["/*", "*/"]] },
606
+ bloat: { soft: 300, hard: 700 },
607
+ tests: [/(^|\/)tests?\//, /_test\.go$/, /\.tftest\.hcl$/],
608
+ manifests: ["main.tf", "versions.tf", "providers.tf"],
609
+ linters: [".tflint.hcl", ".checkov.yaml", ".pre-commit-config.yaml"],
610
+ lockfiles: [".terraform.lock.hcl"]
611
+ },
612
+ {
613
+ id: "docker",
614
+ label: "Docker",
615
+ exts: [".dockerfile"],
616
+ filenames: ["Dockerfile", "docker-compose.yml", "docker-compose.yaml", ".dockerignore"],
617
+ comment: { line: "#", block: [] },
618
+ bloat: { soft: 120, hard: 300 },
619
+ tests: [],
620
+ manifests: ["Dockerfile", "docker-compose.yml", "docker-compose.yaml"],
621
+ linters: [".hadolint.yaml", ".dockerignore"],
622
+ lockfiles: []
623
+ },
624
+ {
625
+ id: "notebook",
626
+ label: "Jupyter Notebook",
627
+ exts: [".ipynb"],
628
+ filenames: [],
629
+ comment: { line: "#", block: [] },
630
+ bloat: { soft: 400, hard: 1200 },
631
+ tests: [],
632
+ manifests: [],
633
+ linters: [],
634
+ lockfiles: []
635
+ },
636
+ {
637
+ id: "swift",
638
+ label: "Swift / Apple",
639
+ exts: [".swift"],
640
+ filenames: ["Package.swift", "Podfile"],
641
+ comment: { line: "//", block: [["/*", "*/"]] },
642
+ bloat: { soft: 400, hard: 900 },
643
+ tests: [/(^|\/)Tests?\//, /Tests\.swift$/],
644
+ manifests: ["Package.swift", "Podfile"],
645
+ linters: [".swiftlint.yml", ".swiftformat"],
646
+ lockfiles: ["Podfile.lock", "Package.resolved"]
647
+ },
648
+ {
649
+ id: "dart",
650
+ label: "Dart / Flutter",
651
+ exts: [".dart"],
652
+ filenames: ["pubspec.yaml", "analysis_options.yaml"],
653
+ comment: { line: "//", block: [["/*", "*/"]] },
654
+ bloat: { soft: 400, hard: 900 },
655
+ tests: [/(^|\/)test\//, /_test\.dart$/],
656
+ manifests: ["pubspec.yaml"],
657
+ linters: ["analysis_options.yaml"],
658
+ lockfiles: ["pubspec.lock"]
659
+ },
660
+ {
661
+ id: "haskell",
662
+ label: "Haskell",
663
+ exts: [".hs", ".lhs"],
664
+ filenames: ["stack.yaml", "*.cabal", "cabal.project"],
665
+ comment: { line: "--", block: [["{-", "-}"]] },
666
+ bloat: { soft: 300, hard: 700 },
667
+ tests: [/(^|\/)test(s)?\//, /Spec\.hs$/, /\/test\/.*\.hs$/],
668
+ manifests: ["stack.yaml", "cabal.project", "*.cabal"],
669
+ linters: [".hlint.yaml", "fourmolu.yaml"],
670
+ lockfiles: ["cabal.project.freeze", "flake.lock"]
671
+ },
672
+ {
673
+ id: "elixir",
674
+ label: "Elixir",
675
+ exts: [".ex", ".exs", ".eex", ".heex"],
676
+ filenames: ["mix.exs", "mix.lock"],
677
+ comment: { line: "#", block: [] },
678
+ bloat: { soft: 300, hard: 700 },
679
+ tests: [/(^|\/)test\//, /_test\.exs?$/],
680
+ manifests: ["mix.exs"],
681
+ linters: [".credo.exs", ".formatter.exs"],
682
+ lockfiles: ["mix.lock"]
683
+ },
684
+ {
685
+ id: "lua",
686
+ label: "Lua",
687
+ exts: [".lua"],
688
+ filenames: [".luacheckrc", "*.rockspec"],
689
+ comment: { line: "--", block: [["--[[", "]]"]] },
690
+ bloat: { soft: 300, hard: 700 },
691
+ tests: [/(^|\/)spec\//, /_spec\.lua$/],
692
+ manifests: ["*.rockspec"],
693
+ linters: [".luacheckrc"],
694
+ lockfiles: []
695
+ },
696
+ {
697
+ id: "r",
698
+ label: "R",
699
+ exts: [".r", ".rmd"],
700
+ filenames: ["DESCRIPTION", "renv.lock"],
701
+ comment: { line: "#", block: [] },
702
+ bloat: { soft: 400, hard: 900 },
703
+ tests: [/(^|\/)tests?\//, /testthat\.R$/, /_test\.R$/],
704
+ manifests: ["DESCRIPTION"],
705
+ linters: [".lintr", "renv.lock"],
706
+ lockfiles: ["renv.lock"]
707
+ },
708
+ {
709
+ id: "solidity",
710
+ label: "Solidity",
711
+ exts: [".sol"],
712
+ filenames: ["hardhat.config.js", "hardhat.config.ts", "foundry.toml"],
713
+ comment: { line: "//", block: [["/*", "*/"]] },
714
+ bloat: { soft: 400, hard: 900 },
715
+ tests: [/\.t\.sol$/, /(^|\/)test\//],
716
+ manifests: ["foundry.toml", "hardhat.config.js"],
717
+ linters: ["solhint.json", ".solhint.json"],
718
+ lockfiles: ["pnpm-lock.yaml", "package-lock.json"]
719
+ },
720
+ {
721
+ id: "zig",
722
+ label: "Zig",
723
+ exts: [".zig"],
724
+ filenames: ["build.zig", "build.zig.zon"],
725
+ comment: { line: "//", block: [] },
726
+ bloat: { soft: 400, hard: 900 },
727
+ tests: [/(^|\/)test\//],
728
+ manifests: ["build.zig"],
729
+ linters: [],
730
+ lockfiles: ["build.zig.zon"]
731
+ }
732
+ ];
733
+ var EXT_MAP = (() => {
734
+ const map = /* @__PURE__ */ new Map();
735
+ for (const lang of LANGUAGES) {
736
+ for (const ext of lang.exts) {
737
+ if (!map.has(ext)) map.set(ext, lang.id);
738
+ }
739
+ }
740
+ return map;
741
+ })();
742
+ var FILENAME_MAP = (() => {
743
+ const map = /* @__PURE__ */ new Map();
744
+ for (const lang of LANGUAGES) {
745
+ for (const name of lang.filenames) {
746
+ const key = name.toLowerCase();
747
+ if (!map.has(key)) map.set(key, lang.id);
748
+ }
749
+ }
750
+ return map;
751
+ })();
752
+ function matchLang(relPath) {
753
+ const dot = relPath.lastIndexOf(".");
754
+ const slash = relPath.lastIndexOf("/");
755
+ if (dot > slash && dot !== -1) {
756
+ const ext = relPath.slice(dot).toLowerCase();
757
+ const id2 = EXT_MAP.get(ext);
758
+ if (id2) return { lang: LANGUAGES.find((l) => l.id === id2), via: "ext" };
759
+ }
760
+ const base = relPath.slice(relPath.lastIndexOf("/") + 1).toLowerCase();
761
+ const id = FILENAME_MAP.get(base);
762
+ if (id) return { lang: LANGUAGES.find((l) => l.id === id), via: "filename" };
763
+ return { lang: null, via: null };
764
+ }
765
+ function languageById(id) {
766
+ return LANGUAGES.find((l) => l.id === id) || null;
767
+ }
768
+ function allExtensions() {
769
+ return [...EXT_MAP.keys()].sort();
770
+ }
771
+
772
+ // src/detect.ts
773
+ var MONOREPO_DIRS = [
774
+ "packages",
775
+ "apps",
776
+ "services",
777
+ "libs",
778
+ "modules",
779
+ "crates",
780
+ "cmd",
781
+ "internal",
782
+ "projects",
783
+ "components",
784
+ "plugins",
785
+ "extensions",
786
+ "workers"
787
+ ];
788
+ var WORKSPACE_MARKERS = [
789
+ "pnpm-workspace.yaml",
790
+ "lerna.json",
791
+ "nx.json",
792
+ "turbo.json",
793
+ "rush.json",
794
+ "go.work",
795
+ "Cargo.toml",
796
+ "package.json",
797
+ "pom.xml",
798
+ "settings.gradle",
799
+ "build.gradle",
800
+ "build.gradle.kts",
801
+ "pyproject.toml",
802
+ "mix.exs",
803
+ "flake.nix",
804
+ "stack.yaml",
805
+ "dune-project",
806
+ "melange.yaml",
807
+ "pubspec.yaml",
808
+ "Package.swift"
809
+ ];
810
+ var SCOPE_MANIFESTS = /* @__PURE__ */ new Set([
811
+ "package.json",
812
+ "Cargo.toml",
813
+ "go.mod",
814
+ "pyproject.toml",
815
+ "setup.py",
816
+ "requirements.txt",
817
+ "Pipfile",
818
+ "composer.json",
819
+ "Gemfile",
820
+ "pom.xml",
821
+ "build.gradle",
822
+ "build.gradle.kts",
823
+ "mix.exs",
824
+ "pubspec.yaml",
825
+ "Package.swift",
826
+ "CMakeLists.txt",
827
+ "Makefile",
828
+ "deno.json",
829
+ "build.zig",
830
+ "Package.resolved"
831
+ ]);
832
+ function isScopeDir(abs) {
833
+ const names = filesIn(abs);
834
+ for (const n of names) {
835
+ if (SCOPE_MANIFESTS.has(n)) return true;
836
+ if (n === ".csproj" || n === ".sln") return true;
837
+ if (n.endsWith(".csproj") || n.endsWith(".fsproj") || n.endsWith(".vbproj")) return true;
838
+ }
839
+ return false;
840
+ }
841
+ function describeScope(name, relPath, abs) {
842
+ const names = filesIn(abs);
843
+ const manifests2 = names.filter(
844
+ (n) => SCOPE_MANIFESTS.has(n) || /\.(csproj|sln|fsproj|vbproj)$/.test(n)
845
+ );
846
+ const lockfiles = names.filter((n) => LOCKFILES.has(n));
847
+ return {
848
+ name,
849
+ path: relPath,
850
+ abs,
851
+ isRoot: relPath === "",
852
+ manifests: manifests2,
853
+ lockfiles
854
+ };
855
+ }
856
+ function detectScopes(root) {
857
+ const scopes = [describeScope("", "", root)];
858
+ const found = /* @__PURE__ */ new Map();
859
+ for (const dir of MONOREPO_DIRS) {
860
+ const abs = join2(root, dir);
861
+ if (!isDir(abs)) continue;
862
+ for (const child of subdirs(abs)) {
863
+ const childAbs = join2(abs, child);
864
+ if (!isScopeDir(childAbs)) continue;
865
+ const relPath = `${dir}/${child}`;
866
+ if (!found.has(relPath)) {
867
+ found.set(relPath, describeScope(child, relPath, childAbs));
868
+ }
869
+ }
870
+ }
871
+ for (const child of subdirs(root)) {
872
+ if (MONOREPO_DIRS.includes(child)) continue;
873
+ const childAbs = join2(root, child);
874
+ if (isScopeDir(childAbs)) {
875
+ const relPath = child;
876
+ if (!found.has(relPath)) found.set(relPath, describeScope(child, relPath, childAbs));
877
+ }
878
+ }
879
+ const nonRoot = [...found.values()];
880
+ const workspaceMarker = WORKSPACE_MARKERS.some((m) => existsSync2(join2(root, m)));
881
+ const all = [scopes[0], ...nonRoot];
882
+ let layout = "flat";
883
+ if (nonRoot.length > 0) layout = "monorepo";
884
+ else if (workspaceMarker) layout = "single";
885
+ return { layout, scopes: all };
886
+ }
887
+ function langsFromManifest(name) {
888
+ const switchName = name;
889
+ if (switchName === "package.json") return ["javascript"];
890
+ if (switchName === "tsconfig.json") return ["typescript"];
891
+ if (switchName === "Cargo.toml") return ["rust"];
892
+ if (switchName === "go.mod" || switchName === "go.work") return ["go"];
893
+ if (["pyproject.toml", "setup.py", "requirements.txt", "Pipfile", "setup.cfg"].includes(switchName))
894
+ return ["python"];
895
+ if (["composer.json"].includes(switchName)) return ["php"];
896
+ if (["Gemfile"].includes(switchName)) return ["ruby"];
897
+ if (["pom.xml", "build.gradle", "build.gradle.kts", "settings.gradle"].includes(switchName))
898
+ return ["java"];
899
+ if (["mix.exs"].includes(switchName)) return ["elixir"];
900
+ if (["pubspec.yaml"].includes(switchName)) return ["dart"];
901
+ if (["Package.swift"].includes(switchName)) return ["swift"];
902
+ if (["build.zig"].includes(switchName)) return ["zig"];
903
+ if (["CMakeLists.txt"].includes(switchName)) return ["c", "cpp"];
904
+ if (["Makefile", "makefile"].includes(switchName)) return ["c", "cpp"];
905
+ if (["Dockerfile", "docker-compose.yml", "docker-compose.yaml"].includes(switchName))
906
+ return ["docker"];
907
+ if (name.endsWith(".csproj") || name.endsWith(".sln") || name.endsWith(".fsproj") || name.endsWith(".vbproj"))
908
+ return ["csharp"];
909
+ if (name.endsWith(".cabal") || name === "stack.yaml") return ["haskell"];
910
+ if (name === "DESCRIPTION") return ["r"];
911
+ if (name.endsWith(".rockspec")) return ["ruby"];
912
+ if (name === "meson.build" || name === "configure.ac") return ["c", "cpp"];
913
+ if (name === "foundry.toml" || name === "hardhat.config.js" || name === "hardhat.config.ts")
914
+ return ["solidity"];
915
+ return [];
916
+ }
917
+ function buildProfile(root, detected, files = []) {
918
+ const ecosystemLangs = /* @__PURE__ */ new Set();
919
+ for (const scope of detected.scopes) {
920
+ for (const manifest of scope.manifests) {
921
+ for (const id of langsFromManifest(manifest)) ecosystemLangs.add(id);
922
+ }
923
+ for (const lock of scope.lockfiles) {
924
+ const id = langsFromManifest(lock);
925
+ if (id.length) ecosystemLangs.add(id[0]);
926
+ }
927
+ }
928
+ const counts = /* @__PURE__ */ new Map();
929
+ for (const f of files) {
930
+ if (!f.lang) continue;
931
+ if (f.langVia && f.langVia !== "ext") continue;
932
+ counts.set(f.lang, (counts.get(f.lang) || 0) + 1);
933
+ }
934
+ const sourceLangs = new Set(counts.keys());
935
+ const allLangs = /* @__PURE__ */ new Set([...ecosystemLangs, ...sourceLangs]);
936
+ const languages = [...allLangs].map((id) => languageById(id)).filter(Boolean).map((l) => ({ id: l.id, label: l.label }));
937
+ let dominant = null;
938
+ let best = 0;
939
+ for (const [id, n] of counts) {
940
+ if (n > best) {
941
+ best = n;
942
+ dominant = id;
943
+ }
944
+ }
945
+ const tooling = {
946
+ hasGitignore: existsSync2(join2(root, ".gitignore")),
947
+ hasReadme: filesIn(root).some((f) => /^readme(\.|$)/i.test(f)),
948
+ hasLicense: filesIn(root).some((f) => /^(licen[sc]e|copying)(\.|$)/i.test(f)),
949
+ hasEditorconfig: existsSync2(join2(root, ".editorconfig")),
950
+ hasPreCommit: existsSync2(join2(root, ".pre-commit-config.yaml")) || isDir(join2(root, ".husky")),
951
+ hasCi: isDir(join2(root, ".github", "workflows")) || existsSync2(join2(root, ".gitlab-ci.yml")) || isDir(join2(root, ".circleci")),
952
+ hasDocker: existsSync2(join2(root, "Dockerfile")) || filesIn(root).some((f) => /^Dockerfile\./.test(f)) || existsSync2(join2(root, "docker-compose.yml")),
953
+ hasEnvExample: filesIn(root).some(
954
+ (f) => f === ".env.example" || f === ".env.sample" || f === ".env.template"
955
+ ),
956
+ hasEnv: existsSync2(join2(root, ".env")),
957
+ isPython: ecosystemLangs.has("python"),
958
+ isNode: ecosystemLangs.has("javascript") || ecosystemLangs.has("typescript"),
959
+ isRust: ecosystemLangs.has("rust"),
960
+ isGo: ecosystemLangs.has("go"),
961
+ isDotnet: ecosystemLangs.has("csharp"),
962
+ isTerraform: ecosystemLangs.has("terraform")
963
+ };
964
+ return {
965
+ layout: detected.layout,
966
+ scopes: detected.scopes,
967
+ languages,
968
+ dominantLanguage: dominant,
969
+ ecosystemLangs: [...ecosystemLangs],
970
+ tooling
971
+ };
972
+ }
973
+ function projectKind(profile) {
974
+ const t = profile.tooling;
975
+ const kinds = [];
976
+ if (t.isNode) kinds.push("node");
977
+ if (t.isPython) kinds.push("python");
978
+ if (t.isRust) kinds.push("rust");
979
+ if (t.isGo) kinds.push("go");
980
+ if (t.isDotnet) kinds.push("dotnet");
981
+ if (t.isTerraform) kinds.push("terraform");
982
+ if (profile.dominantLanguage === "notebook") kinds.push("notebook");
983
+ if (kinds.length === 0 && profile.dominantLanguage) kinds.push(profile.dominantLanguage);
984
+ if (kinds.length === 0) kinds.push("unknown");
985
+ return kinds;
986
+ }
987
+
988
+ // src/scan.ts
989
+ var MAX_CONTENT_BYTES = 2 * 1024 * 1024;
990
+ var MAX_SCAN_BYTES = 512 * 1024;
991
+ var MARKER_KEYWORDS = ["TODO", "FIXME", "HACK", "XXX", "BUG", "OPTIMIZE", "REVIEW"];
992
+ var MARKER_RE = /\b(TODO|FIXME|HACK|XXX|BUG|OPTIMIZE|REVIEW)\b/g;
993
+ var SECRET_ASSIGNMENT_NAMES = [
994
+ "api[_-]?key",
995
+ "apikey",
996
+ "secret",
997
+ "passwd",
998
+ "password",
999
+ "token",
1000
+ "access[_-]?key",
1001
+ "private[_-]?key",
1002
+ "client[_-]?secret",
1003
+ "auth[_-]?token"
1004
+ ];
1005
+ var SECRET_ASSIGNMENT_RE = new RegExp(
1006
+ "\\b(?:" + SECRET_ASSIGNMENT_NAMES.join("|") + ")\\b\\s*[:=]\\s*[\"'`]([^\"'`\\s]{12,})[\"'`]",
1007
+ "gi"
1008
+ );
1009
+ var SECRET_PATTERNS = [
1010
+ {
1011
+ kind: "AWS access key id",
1012
+ re: /\b(A3T[A-Z0-9]|AKIA|ASIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ABIA|ACCA)[A-Z0-9]{16}\b/g,
1013
+ group: 0,
1014
+ prefilter: [
1015
+ "A3T",
1016
+ "AKIA",
1017
+ "ASIA",
1018
+ "AGPA",
1019
+ "AIDA",
1020
+ "AROA",
1021
+ "AIPA",
1022
+ "ANPA",
1023
+ "ANVA",
1024
+ "ABIA",
1025
+ "ACCA"
1026
+ ]
1027
+ },
1028
+ {
1029
+ kind: "private key block",
1030
+ re: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----/g,
1031
+ group: 0,
1032
+ prefilter: ["-----BEGIN"]
1033
+ },
1034
+ {
1035
+ kind: "GitHub token",
1036
+ re: /\bgh[pousr]_[A-Za-z0-9]{36,255}\b/g,
1037
+ group: 0,
1038
+ prefilter: ["ghp_", "gho_", "ghu_", "ghs_", "ghr_"]
1039
+ },
1040
+ { kind: "Slack token", re: /\bxox[abposr]-[A-Za-z0-9-]{10,}\b/g, group: 0, prefilter: ["xox"] },
1041
+ {
1042
+ kind: "Stripe key",
1043
+ re: /\b[sr]k_(?:live|test)_[A-Za-z0-9]{20,}\b/g,
1044
+ group: 0,
1045
+ prefilter: ["_live_", "_test_"]
1046
+ },
1047
+ { kind: "Google API key", re: /\bAIza[0-9A-Za-z_-]{35}\b/g, group: 0, prefilter: ["AIza"] },
1048
+ {
1049
+ kind: "OpenAI-style key",
1050
+ re: /\bsk-(?:proj-)?[A-Za-z0-9_-]{32,}\b/g,
1051
+ group: 0,
1052
+ prefilter: ["sk-"]
1053
+ },
1054
+ {
1055
+ kind: "Anthropic API key",
1056
+ re: /\bsk-ant-[A-Za-z0-9_-]{32,}\b/g,
1057
+ group: 0,
1058
+ prefilter: ["sk-ant-"]
1059
+ },
1060
+ { kind: "npm token", re: /\bnpm_[A-Za-z0-9]{36}\b/g, group: 0, prefilter: ["npm_"] },
1061
+ { kind: "PyPI token", re: /\bpypi-[A-Za-z0-9_-]{16,}\b/g, group: 0, prefilter: ["pypi-"] },
1062
+ {
1063
+ kind: "JSON Web Token",
1064
+ re: /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g,
1065
+ group: 0,
1066
+ prefilter: ["eyJ"]
1067
+ },
1068
+ {
1069
+ kind: "connection string with password",
1070
+ re: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp|mssql):\/\/[^\s:@/]+:[^\s:@/]{3,}@/g,
1071
+ group: 0,
1072
+ prefilter: ["://"]
1073
+ },
1074
+ { kind: "hardcoded secret assignment", re: SECRET_ASSIGNMENT_RE, group: 1 },
1075
+ {
1076
+ kind: "bearer token",
1077
+ re: /\bBearer\s+[A-Za-z0-9._~+/-]{24,}={0,2}\b/g,
1078
+ group: 0,
1079
+ prefilter: ["Bearer"]
1080
+ }
1081
+ ];
1082
+ var PLACEHOLDERS = /* @__PURE__ */ new Set([
1083
+ "your_api_key",
1084
+ "yourapikey",
1085
+ "api_key_here",
1086
+ "changeme",
1087
+ "change_me",
1088
+ "replace_me",
1089
+ "xxxxxxxxxxxx",
1090
+ "your-token-here",
1091
+ "placeholder",
1092
+ "example",
1093
+ "dummy",
1094
+ "todo",
1095
+ "insert_key_here",
1096
+ "your-secret-here",
1097
+ "none",
1098
+ "null",
1099
+ "test",
1100
+ "fake",
1101
+ "your_password",
1102
+ "password_here",
1103
+ "not_a_real_key"
1104
+ ]);
1105
+ var SECRET_EXEMPT_PATHS = [
1106
+ /(^|\/)(test|tests|spec|specs|__tests__|__mocks__|fixtures?|mocks?|examples?|samples?|e2e|testdata|golden)(\/|$)/i,
1107
+ // A test file that sits alongside the source: foo.test.ts, foo.spec.js,
1108
+ // foo_test.go, test_foo.py. These are not under a tests/ directory, so the
1109
+ // rule above misses them. A secret scanner is exactly the kind of tool whose
1110
+ // fixtures hold fake credentials, and reporting those trains people to
1111
+ // ignore the real findings.
1112
+ /\.(test|spec)\.[a-z0-9]+$/i,
1113
+ /(^|\/)(test_[^/]+|[^/]+_test)\.[a-z0-9]+$/i,
1114
+ /\.(example|sample|template|dist)$/i,
1115
+ /(^|\/)(seed|seeds|dev-?seed)\w*\.[a-z0-9]+$/i,
1116
+ /\.lock$/i,
1117
+ /(^|\/)(CHANGELOG|HISTORY|NEWS|README|CONTRIBUTING)(\.|$)/i
1118
+ ];
1119
+ var CONFLICT_RE = /^(<{7}|={7}|>{7})(?: |$)/;
1120
+ var SHEBANG_RE = /^#!\s*\/[^\n]*/;
1121
+ var UNFINISHED_PATTERNS = [
1122
+ {
1123
+ kind: "not-implemented error",
1124
+ re: /\b(?:throw\s+new\s+Error|raise\s+NotImplementedError)\s*\(\s*["'`](?:not implemented|not implemented yet|todo|coming soon)[^"'`]*["'`]\s*\)/i
1125
+ },
1126
+ {
1127
+ kind: "empty error handler",
1128
+ re: /\bcatch\s*(?:\([^)]*\))?\s*\{\s*\}/
1129
+ }
1130
+ ];
1131
+ function isSecretExempt(rel2) {
1132
+ return SECRET_EXEMPT_PATHS.some((re) => re.test(rel2));
1133
+ }
1134
+ function extractMarkers(text, spec) {
1135
+ const out = [];
1136
+ if (!spec || !spec.line) return out;
1137
+ const linePrefix = spec.line;
1138
+ const n = text.length;
1139
+ let i = 0;
1140
+ let lineNo = 1;
1141
+ let inLine = false;
1142
+ let inBlock = -1;
1143
+ let inString = null;
1144
+ let blockStartLine = 0;
1145
+ let commentText = "";
1146
+ const emit = (body, baseLine) => {
1147
+ MARKER_RE.lastIndex = 0;
1148
+ let m;
1149
+ while ((m = MARKER_RE.exec(body)) !== null) {
1150
+ let line = baseLine;
1151
+ for (let k = 0; k < m.index; k++) if (body.charCodeAt(k) === 10) line++;
1152
+ out.push({ type: m[1], line, text: body.slice(m.index, m.index + 80).trim() });
1153
+ }
1154
+ };
1155
+ while (i < n) {
1156
+ const ch = text[i];
1157
+ if (ch === "\n") {
1158
+ if (inLine) {
1159
+ emit(commentText, lineNo);
1160
+ inLine = false;
1161
+ commentText = "";
1162
+ } else if (inBlock !== -1) {
1163
+ commentText += "\n";
1164
+ }
1165
+ lineNo++;
1166
+ i++;
1167
+ continue;
1168
+ }
1169
+ if (inLine) {
1170
+ commentText += ch;
1171
+ i++;
1172
+ continue;
1173
+ }
1174
+ if (inBlock !== -1) {
1175
+ const [, close] = spec.block[inBlock];
1176
+ if (text.startsWith(close, i)) {
1177
+ emit(commentText, blockStartLine);
1178
+ commentText = "";
1179
+ inBlock = -1;
1180
+ i += close.length;
1181
+ continue;
1182
+ }
1183
+ commentText += ch;
1184
+ i++;
1185
+ continue;
1186
+ }
1187
+ if (inString) {
1188
+ if (ch === "\\") {
1189
+ i += 2;
1190
+ continue;
1191
+ }
1192
+ if (ch === inString) inString = null;
1193
+ i++;
1194
+ continue;
1195
+ }
1196
+ if (ch === '"' || ch === "'" || ch === "`") {
1197
+ inString = ch;
1198
+ i++;
1199
+ continue;
1200
+ }
1201
+ if (text.startsWith(linePrefix, i)) {
1202
+ inLine = true;
1203
+ commentText = linePrefix;
1204
+ i += linePrefix.length;
1205
+ continue;
1206
+ }
1207
+ if (ch !== " " && ch !== " ") {
1208
+ const idx = (spec.block || []).findIndex((b) => text.startsWith(b[0], i));
1209
+ if (idx !== -1) {
1210
+ const open = spec.block[idx][0];
1211
+ inBlock = idx;
1212
+ blockStartLine = lineNo;
1213
+ commentText = open;
1214
+ i += open.length;
1215
+ continue;
1216
+ }
1217
+ }
1218
+ i++;
1219
+ }
1220
+ if (inLine) emit(commentText, lineNo);
1221
+ else if (inBlock !== -1) emit(commentText, blockStartLine);
1222
+ return out;
1223
+ }
1224
+ function redact(match) {
1225
+ if (match.length <= 8) return "*".repeat(match.length);
1226
+ return `${match.slice(0, 4)}${"*".repeat(Math.min(match.length - 8, 12))}${match.slice(-4)}`;
1227
+ }
1228
+ function lineStarts(text) {
1229
+ const starts = [0];
1230
+ let nl = text.indexOf("\n");
1231
+ while (nl !== -1) {
1232
+ starts.push(nl + 1);
1233
+ nl = text.indexOf("\n", nl + 1);
1234
+ }
1235
+ return starts;
1236
+ }
1237
+ function lineOf(starts, index) {
1238
+ let lo = 0;
1239
+ let hi = starts.length - 1;
1240
+ while (lo < hi) {
1241
+ const mid = lo + hi + 1 >> 1;
1242
+ if (starts[mid] <= index) lo = mid;
1243
+ else hi = mid - 1;
1244
+ }
1245
+ return lo + 1;
1246
+ }
1247
+ function extractSecrets(text, starts) {
1248
+ const out = [];
1249
+ let lineIndex = starts || null;
1250
+ for (const { kind, re, group, prefilter } of SECRET_PATTERNS) {
1251
+ if (prefilter && !prefilter.some((needle) => text.includes(needle))) continue;
1252
+ re.lastIndex = 0;
1253
+ let m;
1254
+ let guard = 0;
1255
+ while ((m = re.exec(text)) !== null && guard++ < 20) {
1256
+ const value = m[group];
1257
+ if (!value) continue;
1258
+ if (PLACEHOLDERS.has(value.toLowerCase())) continue;
1259
+ if (lineIndex === null) lineIndex = lineStarts(text);
1260
+ out.push({ kind, line: lineOf(lineIndex, m.index), preview: redact(value) });
1261
+ if (m.index === re.lastIndex) re.lastIndex++;
1262
+ }
1263
+ }
1264
+ return out;
1265
+ }
1266
+ function extractUnfinished(text) {
1267
+ const out = [];
1268
+ const lines = text.split("\n");
1269
+ for (let i = 0; i < lines.length; i++) {
1270
+ const line = lines[i];
1271
+ const trimmed = line.trim();
1272
+ if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("--")) {
1273
+ continue;
1274
+ }
1275
+ for (const { kind, re } of UNFINISHED_PATTERNS) {
1276
+ if (re.test(line)) {
1277
+ out.push({ kind, line: i + 1 });
1278
+ break;
1279
+ }
1280
+ }
1281
+ }
1282
+ return out;
1283
+ }
1284
+ function scanFile(rec) {
1285
+ const match = matchLang(rec.rel);
1286
+ const lang = match.lang;
1287
+ rec.lang = lang ? lang.id : null;
1288
+ rec.langLabel = lang ? lang.label : null;
1289
+ rec.commentSpec = lang ? lang.comment : null;
1290
+ rec.langVia = match.via;
1291
+ rec.isGenerated = isGeneratedName(rec.name);
1292
+ rec.isLockfile = LOCKFILES.has(rec.name);
1293
+ rec.isTest = lang ? lang.tests.some((re) => re.test(rec.rel)) : /(^|\/)(test|tests|spec|specs|__tests__)(\/|$)/i.test(rec.rel);
1294
+ rec.isBinary = false;
1295
+ rec.lines = 0;
1296
+ rec.maxLine = 0;
1297
+ rec.longLines = 0;
1298
+ rec.crlf = false;
1299
+ rec.noTrailingNewline = false;
1300
+ rec.hasShebang = false;
1301
+ rec.markers = [];
1302
+ rec.secrets = [];
1303
+ rec.unfinished = [];
1304
+ rec.conflictLines = [];
1305
+ if (rec.size === 0) {
1306
+ rec.empty = true;
1307
+ return rec;
1308
+ }
1309
+ rec.empty = false;
1310
+ if (rec.size > MAX_CONTENT_BYTES) {
1311
+ rec.skippedContent = true;
1312
+ return rec;
1313
+ }
1314
+ const head = readHead(rec.abs, 4096);
1315
+ if (looksBinary(head)) {
1316
+ rec.isBinary = true;
1317
+ return rec;
1318
+ }
1319
+ const text = readText(rec.abs);
1320
+ if (!text) {
1321
+ rec.unreadable = true;
1322
+ return rec;
1323
+ }
1324
+ rec.hasShebang = SHEBANG_RE.test(head);
1325
+ let lines = 0;
1326
+ let maxLine = 0;
1327
+ let longLines = 0;
1328
+ let start = 0;
1329
+ let nl = text.indexOf("\n");
1330
+ while (nl !== -1) {
1331
+ lines++;
1332
+ const len = nl - start;
1333
+ if (len > maxLine) maxLine = len;
1334
+ if (len > 200) longLines++;
1335
+ start = nl + 1;
1336
+ nl = text.indexOf("\n", start);
1337
+ }
1338
+ if (start < text.length) {
1339
+ lines++;
1340
+ const len = text.length - start;
1341
+ if (len > maxLine) maxLine = len;
1342
+ if (len > 200) longLines++;
1343
+ }
1344
+ rec.lines = lines;
1345
+ rec.maxLine = maxLine;
1346
+ rec.longLines = longLines;
1347
+ rec.crlf = text.includes("\r\n");
1348
+ rec.noTrailingNewline = text.length > 0 && !text.endsWith("\n");
1349
+ if (text.includes("<<<<<<<") || text.includes("=======") || text.includes(">>>>>>>")) {
1350
+ const linesArr = text.split("\n");
1351
+ for (let i = 0; i < linesArr.length; i++) {
1352
+ if (CONFLICT_RE.test(linesArr[i])) rec.conflictLines.push(i + 1);
1353
+ }
1354
+ }
1355
+ rec.unfinished = extractUnfinished(text).slice(0, 20);
1356
+ const hasMarkerKeyword = MARKER_KEYWORDS.some((k) => text.includes(k));
1357
+ if (hasMarkerKeyword && rec.commentSpec) {
1358
+ rec.markers = extractMarkers(text, rec.commentSpec).slice(0, 20);
1359
+ }
1360
+ if (!isSecretExempt(rec.rel)) {
1361
+ const slice = text.length > MAX_SCAN_BYTES ? text.slice(0, MAX_SCAN_BYTES) : text;
1362
+ rec.secrets = extractSecrets(slice).slice(0, 10);
1363
+ }
1364
+ return rec;
1365
+ }
1366
+ function isSource(rec) {
1367
+ if (!rec.lang) return false;
1368
+ if (rec.langVia !== "ext") return false;
1369
+ if (rec.isBinary || rec.isGenerated || rec.isLockfile) return false;
1370
+ return true;
1371
+ }
1372
+ function isLineStructured(rec) {
1373
+ return isSource(rec) && rec.lang !== "notebook";
1374
+ }
1375
+
1376
+ // src/analyzers/universal.ts
1377
+ function percentile(sorted, p) {
1378
+ if (sorted.length === 0) return 0;
1379
+ const idx = Math.min(sorted.length - 1, Math.max(0, Math.ceil(p / 100 * sorted.length) - 1));
1380
+ return sorted[idx];
1381
+ }
1382
+ function median(sorted) {
1383
+ if (sorted.length === 0) return 0;
1384
+ const mid = Math.floor(sorted.length / 2);
1385
+ return sorted.length % 2 ? sorted[mid] : Math.round((sorted[mid - 1] + sorted[mid]) / 2);
1386
+ }
1387
+ var fmt = (n) => n.toLocaleString("en-US");
1388
+ function analyzeBloat(ctx) {
1389
+ const out = [];
1390
+ const byLang = /* @__PURE__ */ new Map();
1391
+ for (const f of ctx.sourceFiles) {
1392
+ if (!byLang.has(f.lang)) byLang.set(f.lang, []);
1393
+ byLang.get(f.lang).push(f);
1394
+ }
1395
+ for (const [langId, group] of byLang) {
1396
+ const lang = languageById(langId);
1397
+ const soft = lang?.bloat?.soft ?? 300;
1398
+ const hard = lang?.bloat?.hard ?? 700;
1399
+ const lines = group.map((f) => f.lines).sort((a, b) => a - b);
1400
+ const p90 = percentile(lines, 90);
1401
+ const med = median(lines);
1402
+ const adaptive = Math.max(soft, Math.round(med * 3));
1403
+ const offenders = group.filter((f) => f.lines > adaptive).sort((a, b) => b.lines - a.lines);
1404
+ if (p90 > hard) {
1405
+ out.push(
1406
+ finding({
1407
+ category: "bloat",
1408
+ severity: "MEDIUM",
1409
+ title: `${lang?.label ?? langId} codebase is structurally oversized`,
1410
+ detail: `p90 line count is ${fmt(p90)}, above the ${fmt(hard)}-line guideline, so the norm itself is the problem rather than a few outliers. Median is ${fmt(med)} across ${fmt(group.length)} file(s). Consider a module-boundary pass.`,
1411
+ lang: langId
1412
+ })
1413
+ );
1414
+ }
1415
+ if (offenders.length === 0) continue;
1416
+ const severe = offenders.filter((f) => f.lines > hard);
1417
+ const totalLines = offenders.reduce((s, f) => s + f.lines, 0);
1418
+ if (severe.length > 0) {
1419
+ out.push(
1420
+ finding({
1421
+ category: "bloat",
1422
+ severity: "HIGH",
1423
+ title: `${severe.length} oversized ${lang?.label ?? langId} file(s) over ${fmt(hard)} lines`,
1424
+ detail: `${severe.length} file(s) exceed the ${lang?.label ?? langId} hard limit of ${fmt(hard)} lines. Language median is ${fmt(med)} lines, p90 is ${fmt(p90)}. Consider splitting by responsibility.`,
1425
+ files: severe.slice(0, 10).map((f) => f.rel),
1426
+ count: severe.length,
1427
+ lang: langId
1428
+ })
1429
+ );
1430
+ }
1431
+ const rest = offenders.filter((f) => f.lines <= hard);
1432
+ if (rest.length > 0) {
1433
+ out.push(
1434
+ finding({
1435
+ category: "bloat",
1436
+ severity: "MEDIUM",
1437
+ title: `${offenders.length} ${lang?.label ?? langId} file(s) above the ${fmt(adaptive)}-line adaptive threshold`,
1438
+ detail: `Threshold derived from this repo's own median (${fmt(med)} lines, 3x = ${fmt(adaptive)}), floored at the ${lang?.label ?? langId} baseline of ${fmt(soft)}. These files total ${fmt(totalLines)} lines.`,
1439
+ files: offenders.slice(0, 10).map((f) => f.rel),
1440
+ count: offenders.length,
1441
+ lang: langId
1442
+ })
1443
+ );
1444
+ }
1445
+ }
1446
+ return out;
1447
+ }
1448
+ function analyzeEmpty(ctx) {
1449
+ const out = [];
1450
+ const empties = ctx.sourceFiles.filter((f) => f.size === 0);
1451
+ const structured = ctx.structuredFiles;
1452
+ const langHasSubstantial = new Set(structured.filter((f) => f.lines > 20).map((f) => f.lang));
1453
+ const stubs = structured.filter(
1454
+ (f) => f.size > 0 && f.lines <= 2 && langHasSubstantial.has(f.lang)
1455
+ );
1456
+ if (empties.length > 0) {
1457
+ out.push(
1458
+ finding({
1459
+ category: "bloat",
1460
+ severity: empties.length > 5 ? "MEDIUM" : "LOW",
1461
+ title: `${empties.length} completely empty source file(s)`,
1462
+ detail: "Zero-byte source files. Either a placeholder that was committed by accident, or a file whose contents were lost.",
1463
+ files: empties.slice(0, 10).map((f) => f.rel),
1464
+ count: empties.length
1465
+ })
1466
+ );
1467
+ }
1468
+ if (stubs.length > 0) {
1469
+ out.push(
1470
+ finding({
1471
+ category: "bloat",
1472
+ severity: "LOW",
1473
+ title: `${stubs.length} stub source file(s) of 2 lines or fewer`,
1474
+ detail: "Files with almost no content. Verify these are intentional scaffolding and not abandoned work.",
1475
+ files: stubs.slice(0, 10).map((f) => f.rel),
1476
+ count: stubs.length
1477
+ })
1478
+ );
1479
+ }
1480
+ return out;
1481
+ }
1482
+ function analyzeMarkers(ctx) {
1483
+ const out = [];
1484
+ const byType = /* @__PURE__ */ new Map();
1485
+ for (const f of ctx.sourceFiles) {
1486
+ for (const m of f.markers || []) {
1487
+ if (!byType.has(m.type)) byType.set(m.type, { count: 0, files: /* @__PURE__ */ new Set(), stale: 0 });
1488
+ const entry = byType.get(m.type);
1489
+ entry.count++;
1490
+ entry.files.add(f.rel);
1491
+ }
1492
+ }
1493
+ const total = [...byType.values()].reduce((s, e) => s + e.count, 0);
1494
+ if (total === 0) return out;
1495
+ for (const [type, entry] of byType) {
1496
+ const severity = type === "FIXME" || type === "BUG" || type === "HACK" ? "HIGH" : type === "XXX" ? "HIGH" : "MEDIUM";
1497
+ out.push(
1498
+ finding({
1499
+ category: "markers",
1500
+ severity: entry.count > 20 ? severity === "MEDIUM" ? "HIGH" : severity : severity,
1501
+ title: `${entry.count} ${type} marker(s) across ${entry.files.size} file(s)`,
1502
+ detail: `Extracted from comments only (string literals excluded), so these are real annotations. A ${type} is a promise to future-you with no due date.`,
1503
+ files: [...entry.files].slice(0, 10),
1504
+ count: entry.count
1505
+ })
1506
+ );
1507
+ }
1508
+ if (total > 50) {
1509
+ out.push(
1510
+ finding({
1511
+ category: "markers",
1512
+ severity: "HIGH",
1513
+ title: `${fmt(total)} unresolved markers repo-wide`,
1514
+ detail: "Marker count is high enough to be a tracking-system failure rather than a set of individual notes.",
1515
+ count: total
1516
+ })
1517
+ );
1518
+ }
1519
+ return out;
1520
+ }
1521
+ function analyzeSecrets(ctx) {
1522
+ const out = [];
1523
+ const GENERIC = /* @__PURE__ */ new Set(["hardcoded secret assignment", "bearer token"]);
1524
+ const unique = /* @__PURE__ */ new Map();
1525
+ for (const f of ctx.sourceFiles) {
1526
+ for (const s of f.secrets || []) {
1527
+ const key = `${f.rel}:${s.line}`;
1528
+ const prev = unique.get(key);
1529
+ if (!prev) {
1530
+ unique.set(key, { ...s, rel: f.rel });
1531
+ } else if (GENERIC.has(prev.kind) && !GENERIC.has(s.kind)) {
1532
+ unique.set(key, { ...s, rel: f.rel });
1533
+ }
1534
+ }
1535
+ }
1536
+ if (unique.size === 0) return out;
1537
+ const byKind = /* @__PURE__ */ new Map();
1538
+ for (const s of unique.values()) {
1539
+ if (!byKind.has(s.kind)) byKind.set(s.kind, { files: /* @__PURE__ */ new Set(), samples: [] });
1540
+ const e = byKind.get(s.kind);
1541
+ e.files.add(s.rel);
1542
+ if (e.samples.length < 3) e.samples.push(`${s.rel}:${s.line} (${s.preview})`);
1543
+ }
1544
+ for (const [kind, e] of byKind) {
1545
+ out.push(
1546
+ finding({
1547
+ category: "secrets",
1548
+ severity: "CRITICAL",
1549
+ title: `Possible ${kind} committed to the repository`,
1550
+ detail: `Found in ${e.files.size} file(s). Values are redacted in this report. Examples: ${e.samples.join("; ")}. Treat as compromised: rotate the credential, then purge it from history.`,
1551
+ files: [...e.files].slice(0, 10),
1552
+ count: e.files.size
1553
+ })
1554
+ );
1555
+ }
1556
+ return out;
1557
+ }
1558
+ function analyzeConflicts(ctx) {
1559
+ const out = [];
1560
+ const conflicted = ctx.sourceFiles.filter((f) => f.conflictLines && f.conflictLines.length > 0);
1561
+ if (conflicted.length === 0) return out;
1562
+ out.push(
1563
+ finding({
1564
+ category: "hygiene",
1565
+ severity: "CRITICAL",
1566
+ title: `Unresolved merge conflict markers in ${conflicted.length} file(s)`,
1567
+ detail: `Conflict markers are still present in the working tree. Files: ${conflicted.slice(0, 8).map((f) => f.rel).join(", ")}.` + (conflicted.length > 8 ? ` (+${conflicted.length - 8} more)` : ""),
1568
+ files: conflicted.slice(0, 10).map((f) => f.rel),
1569
+ count: conflicted.length
1570
+ })
1571
+ );
1572
+ return out;
1573
+ }
1574
+ function analyzeUnfinished(ctx) {
1575
+ const unfinished = ctx.sourceFiles.filter(
1576
+ (f) => !f.isTest && f.unfinished && f.unfinished.length > 0
1577
+ );
1578
+ if (unfinished.length === 0) return [];
1579
+ const notImplemented = unfinished.reduce(
1580
+ (count, file) => count + file.unfinished.filter((item) => item.kind === "not-implemented error").length,
1581
+ 0
1582
+ );
1583
+ const emptyHandlers = unfinished.reduce(
1584
+ (count, file) => count + file.unfinished.filter((item) => item.kind === "empty error handler").length,
1585
+ 0
1586
+ );
1587
+ const kinds = [];
1588
+ if (notImplemented > 0) kinds.push(`${notImplemented} not-implemented error(s)`);
1589
+ if (emptyHandlers > 0) kinds.push(`${emptyHandlers} empty error handler(s)`);
1590
+ return [
1591
+ finding({
1592
+ category: "unfinished",
1593
+ severity: "HIGH",
1594
+ title: `${unfinished.length} source file(s) contain unfinished implementation`,
1595
+ detail: `${kinds.join(" and ")} detected. These paths either fail at runtime or silently discard errors; replace the placeholder with working behavior or an explicit, documented fallback.`,
1596
+ files: unfinished.slice(0, 10).map((f) => f.rel),
1597
+ count: unfinished.length
1598
+ })
1599
+ ];
1600
+ }
1601
+ function analyzeFormatting(ctx) {
1602
+ const out = [];
1603
+ const structured = ctx.structuredFiles;
1604
+ const long = structured.filter((f) => f.longLines > 0);
1605
+ if (long.length > 0) {
1606
+ out.push(
1607
+ finding({
1608
+ category: "hygiene",
1609
+ severity: "MEDIUM",
1610
+ title: `${long.length} file(s) contain lines over 200 characters`,
1611
+ detail: `${fmt(long.reduce((s, f) => s + f.longLines, 0))} overlong line(s) in total. Longest offenders: ${long.sort((a, b) => b.maxLine - a.maxLine).slice(0, 5).map((f) => `${f.rel} (${f.maxLine} cols)`).join(", ")}.`,
1612
+ files: long.slice(0, 10).map((f) => f.rel),
1613
+ count: long.length
1614
+ })
1615
+ );
1616
+ }
1617
+ const crlf = structured.filter((f) => f.crlf);
1618
+ const lf = structured.filter((f) => !f.crlf);
1619
+ if (crlf.length > 0 && lf.length > 0) {
1620
+ const crlfPct = Math.round(crlf.length / (crlf.length + lf.length) * 100);
1621
+ if (crlfPct > 3 && crlfPct < 97) {
1622
+ out.push(
1623
+ finding({
1624
+ category: "hygiene",
1625
+ severity: "LOW",
1626
+ title: `Mixed line endings (${crlfPct}% CRLF, ${100 - crlfPct}% LF)`,
1627
+ detail: `${crlf.length} CRLF file(s) and ${lf.length} LF file(s) coexist. A .gitattributes with * text=auto eol=lf removes the whole category of bug.`,
1628
+ files: crlf.slice(0, 8).map((f) => f.rel),
1629
+ count: crlf.length
1630
+ })
1631
+ );
1632
+ }
1633
+ }
1634
+ const noNl = structured.filter((f) => f.noTrailingNewline);
1635
+ if (noNl.length > 0) {
1636
+ out.push(
1637
+ finding({
1638
+ category: "hygiene",
1639
+ severity: "LOW",
1640
+ title: `${noNl.length} file(s) missing a trailing newline`,
1641
+ detail: "POSIX convention: text files end with a newline. Causes noisy diffs.",
1642
+ files: noNl.slice(0, 10).map((f) => f.rel),
1643
+ count: noNl.length
1644
+ })
1645
+ );
1646
+ }
1647
+ return out;
1648
+ }
1649
+ function analyzeStructure(ctx) {
1650
+ const out = [];
1651
+ const deep = ctx.structuredFiles.filter((f) => f.depth > 8);
1652
+ if (deep.length > 0) {
1653
+ out.push(
1654
+ finding({
1655
+ category: "structure",
1656
+ severity: "LOW",
1657
+ title: `${deep.length} file(s) nested more than 8 directories deep`,
1658
+ detail: `Deepest: ${deep.sort((a, b) => b.depth - a.depth).slice(0, 5).map((f) => `${f.rel} (depth ${f.depth})`).join(", ")}.`,
1659
+ files: deep.slice(0, 8).map((f) => f.rel),
1660
+ count: deep.length
1661
+ })
1662
+ );
1663
+ }
1664
+ const vendored = ctx.structuredFiles.filter(
1665
+ (f) => f.isGenerated || /(^|\/)(vendor|third[_-]?party|extern|external|deps|generated)([_/-]|$)/i.test(f.rel)
1666
+ );
1667
+ if (vendored.length > 20) {
1668
+ out.push(
1669
+ finding({
1670
+ category: "structure",
1671
+ severity: "MEDIUM",
1672
+ title: `${fmt(vendored.length)} vendored or generated source file(s) checked into the tree`,
1673
+ detail: "Generated and third-party sources are committed alongside first-party code, which inflates search results, language statistics, and review surface.",
1674
+ files: vendored.slice(0, 10).map((f) => f.rel),
1675
+ count: vendored.length
1676
+ })
1677
+ );
1678
+ }
1679
+ return out;
1680
+ }
1681
+ function analyzeShebangs(ctx) {
1682
+ const out = [];
1683
+ const scripts = ctx.sourceFiles.filter(
1684
+ (f) => ["shell", "python", "ruby", "perl"].includes(f.lang)
1685
+ );
1686
+ const noShebang = scripts.filter((f) => !f.hasShebang && f.size > 0);
1687
+ if (noShebang.length > 0) {
1688
+ out.push(
1689
+ finding({
1690
+ category: "hygiene",
1691
+ severity: "LOW",
1692
+ title: `${noShebang.length} script file(s) with no shebang`,
1693
+ detail: `Scripts without an interpreter line depend on however they are invoked. Files: ${noShebang.slice(0, 6).map((f) => f.rel).join(", ")}.`,
1694
+ files: noShebang.slice(0, 10).map((f) => f.rel),
1695
+ count: noShebang.length
1696
+ })
1697
+ );
1698
+ }
1699
+ return out;
1700
+ }
1701
+ function analyze(ctx) {
1702
+ return [
1703
+ ...analyzeSecrets(ctx),
1704
+ ...analyzeConflicts(ctx),
1705
+ ...analyzeUnfinished(ctx),
1706
+ ...analyzeBloat(ctx),
1707
+ ...analyzeEmpty(ctx),
1708
+ ...analyzeMarkers(ctx),
1709
+ ...analyzeFormatting(ctx),
1710
+ ...analyzeStructure(ctx),
1711
+ ...analyzeShebangs(ctx)
1712
+ ];
1713
+ }
1714
+
1715
+ // src/analyzers/testing.ts
1716
+ var fmt2 = (n) => n.toLocaleString("en-US");
1717
+ var BANDS = [
1718
+ {
1719
+ min: 0,
1720
+ max: 1e-4,
1721
+ severity: "CRITICAL",
1722
+ title: "No tests at all",
1723
+ note: "This is a house with no smoke detectors."
1724
+ },
1725
+ {
1726
+ min: 1e-4,
1727
+ max: 0.05,
1728
+ severity: "HIGH",
1729
+ title: "Effectively untested",
1730
+ note: "A rounding error is not a test suite."
1731
+ },
1732
+ {
1733
+ min: 0.05,
1734
+ max: 0.15,
1735
+ severity: "MEDIUM",
1736
+ title: "Sparse test coverage",
1737
+ note: "Token coverage. The risky paths stay untested."
1738
+ },
1739
+ {
1740
+ min: 0.15,
1741
+ max: 0.35,
1742
+ severity: "LOW",
1743
+ title: "Thin but present test coverage",
1744
+ note: "Real tests, but a low ratio against production code."
1745
+ },
1746
+ {
1747
+ min: 0.35,
1748
+ max: 1,
1749
+ severity: "INFO",
1750
+ title: "Reasonable test coverage",
1751
+ note: "Solid ratio. The main gap will be in the untestable edges."
1752
+ }
1753
+ ];
1754
+ function analyzeTestRatio(ctx) {
1755
+ const out = [];
1756
+ const byLang = /* @__PURE__ */ new Map();
1757
+ for (const f of ctx.sourceFiles) {
1758
+ if (!byLang.has(f.lang)) byLang.set(f.lang, { src: 0, test: 0, testFiles: [] });
1759
+ const e = byLang.get(f.lang);
1760
+ if (f.isTest) {
1761
+ e.test++;
1762
+ e.testFiles.push(f.rel);
1763
+ } else {
1764
+ e.src++;
1765
+ }
1766
+ }
1767
+ for (const [langId, e] of byLang) {
1768
+ if (["docker", "terraform", "notebook", "sql"].includes(langId)) continue;
1769
+ const lang = languageById(langId);
1770
+ const total = e.src + e.test;
1771
+ if (total === 0 || e.src === 0) continue;
1772
+ const ratio = e.test / total;
1773
+ const pct = Math.round(ratio * 100);
1774
+ const band = BANDS.find((b) => ratio >= b.min && ratio < b.max) || BANDS[BANDS.length - 1];
1775
+ const repoIsTiny = (ctx.repoSourceFileCount ?? ctx.sourceFiles.length) < 3;
1776
+ if (e.test === 0 && repoIsTiny) continue;
1777
+ if (e.test === 0) {
1778
+ out.push(
1779
+ finding({
1780
+ category: "testing",
1781
+ severity: "CRITICAL",
1782
+ title: `Zero ${lang?.label ?? langId} tests against ${fmt2(e.src)} source file(s)`,
1783
+ detail: `No test files matched the ${lang?.label ?? langId} test patterns (${lang?.tests?.slice(0, 3).join(", ")}) across ${fmt2(e.src)} source file(s).`,
1784
+ lang: langId
1785
+ })
1786
+ );
1787
+ continue;
1788
+ }
1789
+ out.push(
1790
+ finding({
1791
+ category: "testing",
1792
+ severity: band.severity,
1793
+ title: `${lang?.label ?? langId} test ratio ${pct}% (${fmt2(e.test)} test / ${fmt2(e.src)} source)`,
1794
+ detail: `${band.note}`,
1795
+ count: e.test,
1796
+ lang: langId
1797
+ })
1798
+ );
1799
+ }
1800
+ return out;
1801
+ }
1802
+ function analyzeTestFramework(ctx) {
1803
+ const out = [];
1804
+ const has = (name) => ctx.rootFileNames.has(name);
1805
+ const anyOf = (names) => names.some((n) => has(n));
1806
+ const claimsTests = ctx.profile.dominantLanguage === "javascript" || ctx.profile.dominantLanguage === "typescript";
1807
+ const testFilesExist = ctx.sourceFiles.some((f) => f.isTest);
1808
+ if (claimsTests && testFilesExist) {
1809
+ const hasRunner = anyOf([
1810
+ "vitest.config.ts",
1811
+ "vitest.config.js",
1812
+ "jest.config.js",
1813
+ "jest.config.ts",
1814
+ "jest.config.mjs"
1815
+ ]);
1816
+ if (!hasRunner && !ctx.sourceFiles.some(
1817
+ (f) => /\.test\.[cm]?[jt]sx?$/.test(f.rel) || /\.spec\.[cm]?[jt]sx?$/.test(f.rel)
1818
+ )) {
1819
+ out.push(
1820
+ finding({
1821
+ category: "testing",
1822
+ severity: "LOW",
1823
+ title: "Test files present but no runner configuration found",
1824
+ detail: "No jest/vitest/mocha config and no conventionally-named spec files were detected."
1825
+ })
1826
+ );
1827
+ }
1828
+ }
1829
+ return out;
1830
+ }
1831
+ function analyze2(ctx) {
1832
+ return [...analyzeTestRatio(ctx), ...analyzeTestFramework(ctx)];
1833
+ }
1834
+
1835
+ // src/util/git.ts
1836
+ import { execFileSync } from "child_process";
1837
+ var cachedAvailability = null;
1838
+ function gitDir(cwd) {
1839
+ if (cachedAvailability === false) return null;
1840
+ try {
1841
+ const out = execFileSync("git", ["rev-parse", "--git-dir"], {
1842
+ cwd,
1843
+ encoding: "utf-8",
1844
+ stdio: ["ignore", "pipe", "ignore"],
1845
+ timeout: 1e4
1846
+ }).trim();
1847
+ if (out) cachedAvailability = true;
1848
+ return out || null;
1849
+ } catch {
1850
+ cachedAvailability = false;
1851
+ return null;
1852
+ }
1853
+ }
1854
+ function isRepo(cwd) {
1855
+ return gitDir(cwd) !== null;
1856
+ }
1857
+ function run(args, cwd, { timeout = 2e4 } = {}) {
1858
+ try {
1859
+ return execFileSync("git", args, {
1860
+ cwd,
1861
+ encoding: "utf-8",
1862
+ stdio: ["ignore", "pipe", "ignore"],
1863
+ timeout,
1864
+ maxBuffer: 64 * 1024 * 1024
1865
+ });
1866
+ } catch {
1867
+ return null;
1868
+ }
1869
+ }
1870
+ function trackedFiles(cwd) {
1871
+ const out = run(["ls-files", "-z"], cwd);
1872
+ if (out === null) return /* @__PURE__ */ new Set();
1873
+ return new Set(out.split("\0").filter(Boolean));
1874
+ }
1875
+ function commits(cwd, { limit = 0, all = true } = {}) {
1876
+ const sep2 = "";
1877
+ const rec = "";
1878
+ const args = ["log", `--format=%h${sep2}%an${sep2}%ae${sep2}%aI${sep2}%s${rec}`];
1879
+ if (all) args.push("--all");
1880
+ if (limit > 0) args.push(`-n${limit}`);
1881
+ const out = run(args, cwd, { timeout: 6e4 });
1882
+ if (out === null) return null;
1883
+ return out.split(rec).map((chunk) => chunk.replace(/^\n+/, "")).filter(Boolean).map((chunk) => {
1884
+ const [hash, author, email, date, ...rest] = chunk.split(sep2);
1885
+ return {
1886
+ hash: (hash || "").trim(),
1887
+ author: (author || "").trim(),
1888
+ email: (email || "").trim(),
1889
+ date: (date || "").trim(),
1890
+ subject: rest.join(sep2).trim()
1891
+ };
1892
+ }).filter((c) => c.hash);
1893
+ }
1894
+ function largestTrackedFiles(cwd, limit = 10) {
1895
+ const out = run(["ls-tree", "-r", "-l", "HEAD"], cwd, { timeout: 3e4 });
1896
+ if (out === null) return [];
1897
+ return out.split("\n").map((line) => {
1898
+ const tabIdx = line.indexOf(" ");
1899
+ if (tabIdx === -1) return null;
1900
+ const meta = line.slice(0, tabIdx).split(/\s+/);
1901
+ const path = line.slice(tabIdx + 1);
1902
+ const size = Number.parseInt(meta[3], 10);
1903
+ if (!Number.isFinite(size) || size < 200 * 1024) return null;
1904
+ return { path, kb: Math.round(size / 1024) };
1905
+ }).filter(Boolean).sort((a, b) => b.kb - a.kb).slice(0, limit);
1906
+ }
1907
+
1908
+ // src/analyzers/git.ts
1909
+ var GENERIC_MSG_SUBJECTS = /* @__PURE__ */ new Set([
1910
+ "fix",
1911
+ "fixed",
1912
+ "fixes",
1913
+ "update",
1914
+ "updates",
1915
+ "updated",
1916
+ "change",
1917
+ "changes",
1918
+ "changed",
1919
+ "remove",
1920
+ "removed",
1921
+ "test",
1922
+ "tests",
1923
+ "refactor",
1924
+ "cleanup",
1925
+ "minor",
1926
+ "tweak",
1927
+ "adjust",
1928
+ "patch",
1929
+ "typo",
1930
+ "rename",
1931
+ "move",
1932
+ "edit",
1933
+ "add",
1934
+ "added",
1935
+ "new",
1936
+ "old",
1937
+ "initial",
1938
+ "first",
1939
+ "start",
1940
+ "wip",
1941
+ "todo",
1942
+ "done",
1943
+ "progress",
1944
+ "working",
1945
+ "checkpoint",
1946
+ "save",
1947
+ "commit",
1948
+ "wip2",
1949
+ "final",
1950
+ "final2",
1951
+ "stuff",
1952
+ "things",
1953
+ "misc",
1954
+ "more",
1955
+ "whatever",
1956
+ "meh",
1957
+ "idk",
1958
+ "ok",
1959
+ "nice",
1960
+ "foo",
1961
+ "bar",
1962
+ "baz",
1963
+ "qux",
1964
+ "asdf",
1965
+ "oops",
1966
+ "hmm",
1967
+ "tmp",
1968
+ "temp",
1969
+ "scratch",
1970
+ "hack",
1971
+ "fixup",
1972
+ "squash"
1973
+ ]);
1974
+ var MIN_MSG_LENGTH = 10;
1975
+ function isGenericMessage(subject) {
1976
+ const s = subject.trim().toLowerCase();
1977
+ return GENERIC_MSG_SUBJECTS.has(s) || s.length < MIN_MSG_LENGTH;
1978
+ }
1979
+ var fmt3 = (n) => n.toLocaleString("en-US");
1980
+ function analyze3(ctx) {
1981
+ const out = [];
1982
+ const hasRepo = isRepo(ctx.root);
1983
+ if (!hasRepo) {
1984
+ out.push(
1985
+ finding({
1986
+ category: "git",
1987
+ severity: "LOW",
1988
+ title: "No git repository detected",
1989
+ detail: "Version control history is unavailable, so history-based findings were skipped."
1990
+ })
1991
+ );
1992
+ }
1993
+ const log = hasRepo ? commits(ctx.root, { limit: ctx.options.commitLimit }) : null;
1994
+ if (log && log.length === 0) {
1995
+ out.push(
1996
+ finding({
1997
+ category: "git",
1998
+ severity: "LOW",
1999
+ title: "Git repository has no commits",
2000
+ detail: "The repo is initialized but empty."
2001
+ })
2002
+ );
2003
+ }
2004
+ if (log && log.length > 0) {
2005
+ const authors = new Set(log.map((c) => c.author || c.email));
2006
+ const generic = log.filter((c) => isGenericMessage(c.subject));
2007
+ if (log.length < 10) {
2008
+ out.push(
2009
+ finding({
2010
+ category: "git",
2011
+ severity: "MEDIUM",
2012
+ title: `Sparse git history (${log.length} commit(s))`,
2013
+ detail: `Only ${log.length} commit(s) across ${authors.size} author(s). Most recent: ${log.slice(0, 3).map((c) => `"${c.subject}"`).join(", ")}.`,
2014
+ count: log.length
2015
+ })
2016
+ );
2017
+ }
2018
+ const genericPct = Math.round(generic.length / log.length * 100);
2019
+ if (generic.length > 0 && generic.length / log.length > 0.25) {
2020
+ out.push(
2021
+ finding({
2022
+ category: "git",
2023
+ severity: "MEDIUM",
2024
+ title: `${generic.length}/${log.length} (${genericPct}%) commit messages are generic or too short`,
2025
+ detail: `Examples: ${generic.slice(0, 5).map((c) => `"${c.subject}"`).join(", ")}. Uninformative messages make bisect and blame archaeology painful.`,
2026
+ count: generic.length
2027
+ })
2028
+ );
2029
+ }
2030
+ if (authors.size === 1 && log.length > 3) {
2031
+ out.push(
2032
+ finding({
2033
+ category: "git",
2034
+ severity: "LOW",
2035
+ title: `Solo project (${log.length} commits, 1 author)`,
2036
+ detail: "A single author across the whole history. Either a solo project or teammates are not committing.",
2037
+ count: log.length
2038
+ })
2039
+ );
2040
+ }
2041
+ }
2042
+ const big = hasRepo ? largestTrackedFiles(ctx.root, 10) : [];
2043
+ if (big.length > 0) {
2044
+ out.push(
2045
+ finding({
2046
+ category: "git",
2047
+ severity: "LOW",
2048
+ title: `${big.length} large blob(s) tracked in git (>200KB)`,
2049
+ detail: `Largest: ${big.slice(0, 5).map((b) => `${b.path} (${fmt3(b.kb)}KB)`).join(", ")}. Large binaries bloat clone time forever.`,
2050
+ files: big.slice(0, 6).map((b) => b.path),
2051
+ count: big.length
2052
+ })
2053
+ );
2054
+ }
2055
+ return out;
2056
+ }
2057
+
2058
+ // src/analyzers/node.ts
2059
+ import { join as join3 } from "path";
2060
+ var SCRIPT_CHECKS = [
2061
+ { name: "test", severity: "HIGH", alt: ["test:run", "tests", "test:unit"] },
2062
+ { name: "build", severity: "HIGH", alt: ["build:prod", "compile"] },
2063
+ { name: "lint", severity: "MEDIUM", alt: ["lint:js", "eslint"] },
2064
+ { name: "typecheck", severity: "MEDIUM", alt: ["type-check", "typecheck:ci", "tsc"] }
2065
+ ];
2066
+ var FIELDS = [
2067
+ { key: "name", severity: "HIGH", msg: 'missing "name"' },
2068
+ { key: "version", severity: "MEDIUM", msg: 'missing "version"' },
2069
+ { key: "license", severity: "MEDIUM", msg: 'missing "license"' },
2070
+ { key: "description", severity: "LOW", msg: 'missing "description"' },
2071
+ { key: "repository", severity: "LOW", msg: 'missing "repository"' }
2072
+ ];
2073
+ var DEP_TYPES = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
2074
+ function collectManifests(ctx) {
2075
+ const manifests2 = [];
2076
+ for (const scope of ctx.profile.scopes) {
2077
+ const pkgPath = scope.isRoot ? join3(ctx.root, "package.json") : join3(ctx.root, scope.path, "package.json");
2078
+ if (!existsSync(pkgPath)) continue;
2079
+ const pkg = readJSON(pkgPath);
2080
+ if (!pkg) {
2081
+ manifests2.push({
2082
+ scope,
2083
+ pkg: null,
2084
+ relPath: scope.isRoot ? "package.json" : `${scope.path}/package.json`,
2085
+ invalid: true
2086
+ });
2087
+ continue;
2088
+ }
2089
+ manifests2.push({
2090
+ scope,
2091
+ pkg,
2092
+ relPath: scope.isRoot ? "package.json" : `${scope.path}/package.json`,
2093
+ invalid: false
2094
+ });
2095
+ }
2096
+ return manifests2;
2097
+ }
2098
+ function analyze4(ctx) {
2099
+ const out = [];
2100
+ const manifests2 = collectManifests(ctx);
2101
+ if (manifests2.length === 0) return out;
2102
+ for (const m of manifests2.filter((x) => x.invalid)) {
2103
+ out.push(
2104
+ finding({
2105
+ category: "config",
2106
+ severity: "HIGH",
2107
+ title: `Invalid JSON in ${m.relPath}`,
2108
+ detail: "The manifest could not be parsed. This will break installs and most tooling.",
2109
+ files: [m.relPath]
2110
+ })
2111
+ );
2112
+ }
2113
+ const valid = manifests2.filter((m) => !m.invalid);
2114
+ const depIndex = /* @__PURE__ */ new Map();
2115
+ for (const m of valid) {
2116
+ for (const type of DEP_TYPES) {
2117
+ const deps = m.pkg[type];
2118
+ if (!deps || typeof deps !== "object") continue;
2119
+ for (const [name, range] of Object.entries(deps)) {
2120
+ if (!depIndex.has(name)) depIndex.set(name, /* @__PURE__ */ new Map());
2121
+ depIndex.get(name).set(m.scope.name || "root", { range, type });
2122
+ }
2123
+ }
2124
+ }
2125
+ for (const [dep, byPkg] of depIndex) {
2126
+ if (byPkg.size < 2) continue;
2127
+ const ranges = [...byPkg.values()].map((v) => String(v.range));
2128
+ const unique = [...new Set(ranges)];
2129
+ if (unique.length > 1) {
2130
+ out.push(
2131
+ finding({
2132
+ category: "deps",
2133
+ severity: "CRITICAL",
2134
+ title: `Dependency "${dep}" has ${unique.length} different version ranges across packages`,
2135
+ detail: [...byPkg.entries()].map(([pkg, v]) => `${pkg}: ${v.range} (${v.type})`).join(", ") + ". Pin one version and use workspace protocol / overrides.",
2136
+ count: byPkg.size
2137
+ })
2138
+ );
2139
+ }
2140
+ }
2141
+ for (const m of valid) {
2142
+ const name = m.scope.name || "root";
2143
+ const scripts = m.pkg.scripts || {};
2144
+ for (const check of SCRIPT_CHECKS) {
2145
+ const present = scripts[check.name] || (check.alt || []).some((a) => scripts[a]);
2146
+ const hasSource = m.scope.isRoot ? ctx.sourceFiles.some((f) => ["javascript", "typescript"].includes(f.lang)) : ctx.sourceFiles.some(
2147
+ (f) => f.rel.startsWith(`${m.scope.path}/`) && ["javascript", "typescript"].includes(f.lang)
2148
+ );
2149
+ if (!present && hasSource) {
2150
+ out.push(
2151
+ finding({
2152
+ category: "script",
2153
+ severity: check.severity,
2154
+ title: `Package "${name}" has no "${check.name}" script`,
2155
+ detail: `No ${check.name} script in ${m.relPath}.${check.alt?.length ? ` (also checked: ${check.alt.join(", ")})` : ""}`,
2156
+ files: [m.relPath]
2157
+ })
2158
+ );
2159
+ }
2160
+ }
2161
+ for (const f of FIELDS) {
2162
+ if (m.pkg[f.key] === void 0) {
2163
+ out.push(
2164
+ finding({
2165
+ category: "config",
2166
+ severity: f.severity,
2167
+ title: `Package "${name}" ${f.msg}`,
2168
+ detail: `${m.relPath} has no "${f.key}" field.`,
2169
+ files: [m.relPath]
2170
+ })
2171
+ );
2172
+ }
2173
+ }
2174
+ const unpinned = Object.entries(m.pkg.dependencies || {}).filter(
2175
+ ([, r]) => /^[~^*]|\|\||\bx\b|\*$/i.test(String(r)) && !/^(npm|file|link|workspace|git|http)/.test(String(r))
2176
+ );
2177
+ if (unpinned.length > 0) {
2178
+ out.push(
2179
+ finding({
2180
+ category: "deps",
2181
+ severity: "MEDIUM",
2182
+ title: `Package "${name}" has ${unpinned.length} loosely-pinned runtime dependenc(ies)`,
2183
+ detail: unpinned.map(([d, r]) => `${d}@${r}`).join(", "),
2184
+ files: [m.relPath],
2185
+ count: unpinned.length
2186
+ })
2187
+ );
2188
+ }
2189
+ const wildcards = Object.entries(m.pkg.dependencies || {}).filter(
2190
+ ([, r]) => String(r).trim() === "*" || String(r).trim() === ""
2191
+ );
2192
+ if (wildcards.length > 0) {
2193
+ out.push(
2194
+ finding({
2195
+ category: "deps",
2196
+ severity: "HIGH",
2197
+ title: `Package "${name}" uses wildcard version(s) in dependencies`,
2198
+ detail: wildcards.map(([d]) => d).join(", ") + ". Wildcards make builds non-reproducible.",
2199
+ files: [m.relPath]
2200
+ })
2201
+ );
2202
+ }
2203
+ }
2204
+ const rootPkg = valid.find((m) => m.scope.isRoot);
2205
+ if (rootPkg) {
2206
+ const hasTsconfig = existsSync(join3(ctx.root, "tsconfig.json"));
2207
+ const declaresTs = Boolean(
2208
+ rootPkg.pkg.devDependencies?.typescript || rootPkg.pkg.dependencies?.typescript
2209
+ );
2210
+ const usesTsExt = ctx.sourceFiles.some((f) => f.lang === "typescript");
2211
+ if (usesTsExt && !declaresTs) {
2212
+ out.push(
2213
+ finding({
2214
+ category: "deps",
2215
+ severity: "HIGH",
2216
+ title: 'TypeScript sources present but "typescript" is not a declared dependency',
2217
+ detail: "No typescript in dependencies/devDependencies. Editor and CI type-checking will silently do nothing.",
2218
+ files: ["package.json"]
2219
+ })
2220
+ );
2221
+ }
2222
+ if (!hasTsconfig && usesTsExt) {
2223
+ out.push(
2224
+ finding({
2225
+ category: "config",
2226
+ severity: "HIGH",
2227
+ title: "TypeScript sources but no tsconfig.json",
2228
+ detail: "Without a tsconfig, there is no configured target, module resolution, or strictness."
2229
+ })
2230
+ );
2231
+ }
2232
+ const lockPresent = filesIn(ctx.root).some(
2233
+ (f) => [
2234
+ "package-lock.json",
2235
+ "yarn.lock",
2236
+ "pnpm-lock.yaml",
2237
+ "bun.lockb",
2238
+ "bun.lock",
2239
+ "npm-shrinkwrap.json"
2240
+ ].includes(f)
2241
+ );
2242
+ if (!lockPresent && !ctx.options.noLockfileWarning) {
2243
+ out.push(
2244
+ finding({
2245
+ category: "deps",
2246
+ severity: "MEDIUM",
2247
+ title: "No JavaScript lockfile at the repository root",
2248
+ detail: "No package-lock.json / yarn.lock / pnpm-lock.yaml / bun.lock. Dependency resolution is not reproducible."
2249
+ })
2250
+ );
2251
+ }
2252
+ if (!rootPkg.pkg.engines?.node) {
2253
+ out.push(
2254
+ finding({
2255
+ category: "config",
2256
+ severity: "LOW",
2257
+ title: 'Root package.json has no "engines.node" constraint',
2258
+ detail: "Node version is unpinned, so local and CI runtimes can drift silently.",
2259
+ files: ["package.json"]
2260
+ })
2261
+ );
2262
+ }
2263
+ }
2264
+ const tsconfigs = ctx.sourceFiles.filter((f) => f.name === "tsconfig.json");
2265
+ for (const tsc of tsconfigs) {
2266
+ const raw = readJSON(join3(ctx.root, tsc.rel));
2267
+ if (!raw) continue;
2268
+ const compiler = raw.compilerOptions || {};
2269
+ const strict = compiler.strict === true || compiler.strictNullChecks === true && compiler.noImplicitAny === true;
2270
+ if (!strict) {
2271
+ out.push(
2272
+ finding({
2273
+ category: "config",
2274
+ severity: "MEDIUM",
2275
+ title: `${tsc.rel} does not enable TypeScript strict mode`,
2276
+ detail: 'Without "strict": true, null/undefined and implicit-any bugs pass the compiler silently.',
2277
+ files: [tsc.rel]
2278
+ })
2279
+ );
2280
+ }
2281
+ }
2282
+ return out;
2283
+ }
2284
+
2285
+ // src/analyzers/python.ts
2286
+ import { join as join4 } from "path";
2287
+ var PY_MANIFESTS = [
2288
+ "pyproject.toml",
2289
+ "setup.py",
2290
+ "setup.cfg",
2291
+ "requirements.txt",
2292
+ "Pipfile",
2293
+ "environment.yml"
2294
+ ];
2295
+ function analyze5(ctx) {
2296
+ const out = [];
2297
+ if (!ctx.profile.tooling.isPython && !ctx.sourceFiles.some((f) => f.lang === "python"))
2298
+ return out;
2299
+ const foundManifests = [];
2300
+ for (const name of PY_MANIFESTS) {
2301
+ if (filesIn(ctx.root).includes(name)) foundManifests.push(name);
2302
+ }
2303
+ if (foundManifests.length === 0 && ctx.sourceFiles.some((f) => f.lang === "python")) {
2304
+ out.push(
2305
+ finding({
2306
+ category: "config",
2307
+ severity: "MEDIUM",
2308
+ title: "Python code with no dependency manifest",
2309
+ detail: `None of ${PY_MANIFESTS.join(", ")} present. Dependencies are undeclared and not reproducible.`
2310
+ })
2311
+ );
2312
+ }
2313
+ for (const req of ["requirements.txt", "requirements-dev.txt"]) {
2314
+ const rec = ctx.files.find((f) => f.rel === req);
2315
+ if (!rec) continue;
2316
+ const text = readText(rec.abs);
2317
+ if (!text) continue;
2318
+ const lines = text.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#") && !l.startsWith("-"));
2319
+ const unpinned = lines.filter((l) => !/[=><~!]|\bgit\+|@|file:/.test(l));
2320
+ if (unpinned.length > 0) {
2321
+ out.push(
2322
+ finding({
2323
+ category: "deps",
2324
+ severity: "MEDIUM",
2325
+ title: `${unpinned.length} unpinned requirement(s) in ${req}`,
2326
+ detail: unpinned.slice(0, 12).join(", ") + ". Unpinned requirements make installs non-reproducible.",
2327
+ files: [req],
2328
+ count: unpinned.length
2329
+ })
2330
+ );
2331
+ }
2332
+ }
2333
+ const venvDirs = [".venv", "venv", "env", "virtualenv"];
2334
+ const trackedVenv = [];
2335
+ for (const d of venvDirs) {
2336
+ if (ctx.gitTracked && ctx.gitTracked.size > 0) {
2337
+ for (const p of ctx.gitTracked) {
2338
+ if (p.startsWith(`${d}/`) || p.includes(`/${d}/`)) trackedVenv.push(p);
2339
+ }
2340
+ } else if (isDir(join4(ctx.root, d))) {
2341
+ trackedVenv.push(d);
2342
+ }
2343
+ }
2344
+ if (trackedVenv.length > 0) {
2345
+ out.push(
2346
+ finding({
2347
+ category: "vcs",
2348
+ severity: "HIGH",
2349
+ title: "Python virtualenv committed to the repository",
2350
+ detail: `Virtualenv directories (${venvDirs.join(", ")}) contain thousands of absolute-path-bound files. Files: ${[...new Set(trackedVenv)].slice(0, 5).join(", ")}.`,
2351
+ files: [...new Set(trackedVenv)].slice(0, 5),
2352
+ count: new Set(trackedVenv).size
2353
+ })
2354
+ );
2355
+ }
2356
+ const pycache = ctx.sourceFiles.filter((f) => /__pycache__|\.pyc$/.test(f.rel));
2357
+ if (pycache.length > 0) {
2358
+ out.push(
2359
+ finding({
2360
+ category: "vcs",
2361
+ severity: "LOW",
2362
+ title: `${pycache.length} Python bytecode file(s) present in the tree`,
2363
+ detail: "__pycache__/*.pyc should be gitignored. They are machine-specific build artifacts.",
2364
+ files: pycache.slice(0, 5).map((f) => f.rel),
2365
+ count: pycache.length
2366
+ })
2367
+ );
2368
+ }
2369
+ const pyFiles = ctx.sourceFiles.filter((f) => f.lang === "python");
2370
+ if (pyFiles.length > 5) {
2371
+ const hasMypy = filesIn(ctx.root).some(
2372
+ (f) => ["mypy.ini", ".mypy.ini", "setup.cfg", "pyproject.toml"].includes(f)
2373
+ );
2374
+ const hasPyrightCfg = filesIn(ctx.root).includes("pyrightconfig.json");
2375
+ if (!hasMypy && !hasPyrightCfg) {
2376
+ out.push(
2377
+ finding({
2378
+ category: "config",
2379
+ severity: "LOW",
2380
+ title: "No Python type checker configured",
2381
+ detail: `${pyFiles.length} Python source file(s) but no mypy/pyright configuration. Type errors go unnoticed.`
2382
+ })
2383
+ );
2384
+ }
2385
+ }
2386
+ return out;
2387
+ }
2388
+
2389
+ // src/analyzers/systems.ts
2390
+ var fmt4 = (n) => n.toLocaleString("en-US");
2391
+ function rust(ctx) {
2392
+ const out = [];
2393
+ const files = ctx.sourceFiles.filter((f) => f.lang === "rust");
2394
+ if (files.length === 0) return out;
2395
+ const hasCargo = filesIn(ctx.root).includes("Cargo.toml");
2396
+ if (!hasCargo) {
2397
+ out.push(
2398
+ finding({
2399
+ category: "build",
2400
+ severity: "HIGH",
2401
+ title: "Rust sources present but no Cargo.toml at the root",
2402
+ detail: "These .rs files are not part of a Cargo build.",
2403
+ lang: "rust"
2404
+ })
2405
+ );
2406
+ }
2407
+ const lockPresent = filesIn(ctx.root).includes("Cargo.lock");
2408
+ if (hasCargo && !lockPresent) {
2409
+ out.push(
2410
+ finding({
2411
+ category: "deps",
2412
+ severity: "MEDIUM",
2413
+ title: "Cargo.lock not committed",
2414
+ detail: "For a binary crate, committing Cargo.lock pins reproducible builds. (Libraries may omit it.)",
2415
+ files: ["Cargo.toml"],
2416
+ lang: "rust"
2417
+ })
2418
+ );
2419
+ }
2420
+ const unwrapFiles = [];
2421
+ let unwrapCount = 0;
2422
+ for (const f of files) {
2423
+ const text = readText(f.abs);
2424
+ if (!text) continue;
2425
+ const n = (text.match(/\.unwrap\(\)/g) || []).length + (text.match(/\.expect\(/g) || []).length;
2426
+ if (n > 0) {
2427
+ unwrapCount += n;
2428
+ unwrapFiles.push({ rel: f.rel, n });
2429
+ }
2430
+ }
2431
+ if (unwrapCount > 0) {
2432
+ out.push(
2433
+ finding({
2434
+ category: "bloat",
2435
+ severity: unwrapCount > 30 ? "MEDIUM" : "LOW",
2436
+ title: `${fmt4(unwrapCount)} unwrap()/expect() call(s) across ${unwrapFiles.length} Rust file(s)`,
2437
+ detail: "These panic on failure. Concentrated use in library code turns bad input into a process crash. " + unwrapFiles.sort((a, b) => b.n - a.n).slice(0, 5).map((f) => `${f.rel} (${f.n})`).join(", "),
2438
+ files: unwrapFiles.slice(0, 6).map((f) => f.rel),
2439
+ count: unwrapCount,
2440
+ lang: "rust"
2441
+ })
2442
+ );
2443
+ }
2444
+ return out;
2445
+ }
2446
+ function go(ctx) {
2447
+ const out = [];
2448
+ const files = ctx.sourceFiles.filter((f) => f.lang === "go");
2449
+ if (files.length === 0) return out;
2450
+ const hasMod = filesIn(ctx.root).includes("go.mod");
2451
+ if (!hasMod) {
2452
+ out.push(
2453
+ finding({
2454
+ category: "build",
2455
+ severity: "HIGH",
2456
+ title: "Go sources present but no go.mod",
2457
+ detail: "Go requires go.mod for module resolution and reproducible builds.",
2458
+ lang: "go"
2459
+ })
2460
+ );
2461
+ } else if (!filesIn(ctx.root).includes("go.sum")) {
2462
+ out.push(
2463
+ finding({
2464
+ category: "deps",
2465
+ severity: "MEDIUM",
2466
+ title: "go.mod present but no go.sum",
2467
+ detail: "go.sum pins module checksums. Without it, dependency integrity is not verified.",
2468
+ files: ["go.mod"],
2469
+ lang: "go"
2470
+ })
2471
+ );
2472
+ }
2473
+ const ignored = [];
2474
+ for (const f of files) {
2475
+ if (f.name.endsWith("_test.go")) continue;
2476
+ const text = readText(f.abs);
2477
+ if (!text) continue;
2478
+ const n = (text.match(/^\s*_\s*=\s*\w+\([^)]*\)\s*$/gm) || []).length;
2479
+ if (n > 0) ignored.push({ rel: f.rel, n });
2480
+ }
2481
+ if (ignored.length > 0) {
2482
+ const total = ignored.reduce((s, f) => s + f.n, 0);
2483
+ out.push(
2484
+ finding({
2485
+ category: "hygiene",
2486
+ severity: total > 20 ? "MEDIUM" : "LOW",
2487
+ title: `${total} discarded error value(s) assigned to _ in Go`,
2488
+ detail: "Explicitly ignoring returned errors. Some is correct at boundaries, but volume like this usually hides real failures. " + ignored.slice(0, 5).map((f) => `${f.rel} (${f.n})`).join(", "),
2489
+ files: ignored.slice(0, 6).map((f) => f.rel),
2490
+ count: total,
2491
+ lang: "go"
2492
+ })
2493
+ );
2494
+ }
2495
+ return out;
2496
+ }
2497
+ function dotnet(ctx) {
2498
+ const out = [];
2499
+ const cs = ctx.sourceFiles.filter((f) => f.lang === "csharp");
2500
+ const projects = ctx.files.filter((f) => /\.csproj$|\.sln$|\.fsproj$|\.vbproj$/.test(f.rel));
2501
+ if (cs.length === 0 && projects.length === 0) return out;
2502
+ if (projects.length === 0 && cs.length > 0) {
2503
+ out.push(
2504
+ finding({
2505
+ category: "build",
2506
+ severity: "HIGH",
2507
+ title: "C# sources present but no .csproj or .sln",
2508
+ detail: "No project file found. dotnet build will not discover these sources.",
2509
+ lang: "csharp"
2510
+ })
2511
+ );
2512
+ }
2513
+ const csprojNoNullable = [];
2514
+ for (const p of projects.filter((x) => /\.csproj$/.test(x.rel))) {
2515
+ const raw = readText(p.abs);
2516
+ if (/<Nullable>/.test(raw) && /<Nullable>disable<\/Nullable>/.test(raw))
2517
+ csprojNoNullable.push(p.rel);
2518
+ }
2519
+ if (cs.length > 3 && csprojNoNullable.length > 0) {
2520
+ out.push(
2521
+ finding({
2522
+ category: "config",
2523
+ severity: "MEDIUM",
2524
+ title: `${csprojNoNullable.length} C# project(s) with nullable reference types disabled`,
2525
+ detail: `<Nullable>disable</Nullable> in ${csprojNoNullable.join(", ")}. NullReferenceException risks stay invisible to the compiler.`,
2526
+ files: csprojNoNullable,
2527
+ lang: "csharp"
2528
+ })
2529
+ );
2530
+ }
2531
+ if (projects.length > 0 && !filesIn(ctx.root).includes("global.json")) {
2532
+ out.push(
2533
+ finding({
2534
+ category: "build",
2535
+ severity: "LOW",
2536
+ title: "No global.json pinning the .NET SDK version",
2537
+ detail: "Different machines/CI runners may build against different SDKs.",
2538
+ lang: "csharp"
2539
+ })
2540
+ );
2541
+ }
2542
+ return out;
2543
+ }
2544
+ function zig(ctx) {
2545
+ const out = [];
2546
+ const files = ctx.sourceFiles.filter((f) => f.lang === "zig");
2547
+ if (files.length === 0) return out;
2548
+ if (!filesIn(ctx.root).includes("build.zig")) {
2549
+ out.push(
2550
+ finding({
2551
+ category: "build",
2552
+ severity: "MEDIUM",
2553
+ title: "Zig sources present but no build.zig",
2554
+ detail: "No build script. Zig projects are conventionally driven by build.zig.",
2555
+ lang: "zig"
2556
+ })
2557
+ );
2558
+ }
2559
+ return out;
2560
+ }
2561
+ function analyze6(ctx) {
2562
+ return [...rust(ctx), ...go(ctx), ...dotnet(ctx), ...zig(ctx)];
2563
+ }
2564
+
2565
+ // src/analyzers/notebooks.ts
2566
+ var fmtKB = (bytes) => `${Math.round(bytes / 1024)}KB`;
2567
+ function outputText(output) {
2568
+ if (typeof output?.text === "string") return output.text;
2569
+ if (Array.isArray(output?.text)) return output.text.join("");
2570
+ if (output?.data) {
2571
+ const d = output.data;
2572
+ return [...d["text/plain"] || [], ...d["text/html"] || []].join(" ");
2573
+ }
2574
+ return "";
2575
+ }
2576
+ function analyze7(ctx) {
2577
+ const out = [];
2578
+ const notebooks = ctx.files.filter((f) => f.rel.endsWith(".ipynb"));
2579
+ if (notebooks.length === 0) return out;
2580
+ let withOutputs = 0;
2581
+ let runInOrder = 0;
2582
+ let embeddedImages = 0;
2583
+ let secretOutputs = 0;
2584
+ const bigNotebooks = [];
2585
+ const outputFiles = [];
2586
+ for (const nb of notebooks) {
2587
+ const raw = readText(nb.abs);
2588
+ let json;
2589
+ try {
2590
+ json = JSON.parse(raw);
2591
+ } catch {
2592
+ out.push(
2593
+ finding({
2594
+ category: "config",
2595
+ severity: "HIGH",
2596
+ title: `Invalid JSON in notebook ${nb.rel}`,
2597
+ detail: "The .ipynb could not be parsed. It is likely corrupt or was hand-edited.",
2598
+ files: [nb.rel]
2599
+ })
2600
+ );
2601
+ continue;
2602
+ }
2603
+ const cells = Array.isArray(json.cells) ? json.cells : [];
2604
+ let nbOutputs = 0;
2605
+ let nbImages = 0;
2606
+ let nbSecrets = 0;
2607
+ let nbRun = false;
2608
+ for (const cell of cells) {
2609
+ if (cell.execution_count != null) nbRun = true;
2610
+ for (const output of cell.outputs || []) {
2611
+ nbOutputs++;
2612
+ if (output.data?.["image/png"] || output.data?.["image/jpeg"] || output.data?.["application/octet-stream"]) {
2613
+ nbImages++;
2614
+ }
2615
+ const text = outputText(output);
2616
+ if (/AKIA[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9]{36}|-----BEGIN [A-Z ]*PRIVATE KEY|sk-[A-Za-z0-9]{32}/.test(
2617
+ text
2618
+ )) {
2619
+ nbSecrets++;
2620
+ }
2621
+ if (output.output_type === "error") {
2622
+ nbOutputs++;
2623
+ }
2624
+ }
2625
+ }
2626
+ if (nbOutputs > 0) withOutputs++;
2627
+ if (nbRun) runInOrder++;
2628
+ if (nbImages > 0) embeddedImages++;
2629
+ if (nbSecrets > 0) secretOutputs++;
2630
+ if (nb.size > 300 * 1024) bigNotebooks.push({ rel: nb.rel, size: nb.size });
2631
+ if (nbOutputs > 0) outputFiles.push(nb.rel);
2632
+ }
2633
+ if (withOutputs > 0) {
2634
+ out.push(
2635
+ finding({
2636
+ category: "bloat",
2637
+ severity: withOutputs / notebooks.length > 0.5 ? "MEDIUM" : "LOW",
2638
+ title: `${withOutputs}/${notebooks.length} notebook(s) committed with cell outputs`,
2639
+ detail: "Stored outputs (including base64 images) bloat the repo and can leak data that was printed during the run. Clear outputs before committing, or use nbstripout / a pre-commit hook.",
2640
+ files: outputFiles.slice(0, 8),
2641
+ count: withOutputs,
2642
+ lang: "notebook"
2643
+ })
2644
+ );
2645
+ }
2646
+ if (embeddedImages > 0) {
2647
+ out.push(
2648
+ finding({
2649
+ category: "bloat",
2650
+ severity: "LOW",
2651
+ title: `${embeddedImages} notebook(s) contain embedded base64 image data`,
2652
+ detail: "Plots stored as base64 PNGs. Each one is a permanent, uncompressible blob in git history.",
2653
+ count: embeddedImages,
2654
+ lang: "notebook"
2655
+ })
2656
+ );
2657
+ }
2658
+ if (secretOutputs > 0) {
2659
+ out.push(
2660
+ finding({
2661
+ category: "bloat",
2662
+ severity: "CRITICAL",
2663
+ title: `Credential-looking data in ${secretOutputs} notebook output(s)`,
2664
+ detail: "A cell printed something matching a credential pattern. Outputs are committed and shared. Rotate anything real.",
2665
+ count: secretOutputs,
2666
+ lang: "notebook"
2667
+ })
2668
+ );
2669
+ }
2670
+ if (runInOrder > 0) {
2671
+ out.push(
2672
+ finding({
2673
+ category: "hygiene",
2674
+ severity: "LOW",
2675
+ title: `${runInOrder} notebook(s) saved with execution order intact`,
2676
+ detail: "Non-null execution_count means cells were run top-to-bottom. Restart-and-run-all before sharing to prove the notebook is self-contained.",
2677
+ count: runInOrder,
2678
+ lang: "notebook"
2679
+ })
2680
+ );
2681
+ }
2682
+ for (const b of bigNotebooks) {
2683
+ out.push(
2684
+ finding({
2685
+ category: "bloat",
2686
+ severity: "LOW",
2687
+ title: `Large notebook ${b.rel} (${fmtKB(b.size)})`,
2688
+ detail: "Notebook over 300KB. Split exploratory work from the narrative, or push the data out and keep the notebook as a thin driver.",
2689
+ files: [b.rel],
2690
+ lang: "notebook"
2691
+ })
2692
+ );
2693
+ }
2694
+ return out;
2695
+ }
2696
+
2697
+ // src/analyzers/scaffolding.ts
2698
+ import { join as join5 } from "path";
2699
+ function scaffolding(ctx) {
2700
+ const out = [];
2701
+ const t = ctx.profile.tooling;
2702
+ if (!t.hasReadme && ctx.sourceFiles.length > 0) {
2703
+ out.push(
2704
+ finding({
2705
+ category: "vcs",
2706
+ severity: "MEDIUM",
2707
+ title: "No README found",
2708
+ detail: "A repository with no README forces every new contributor to reverse-engineer how to run it."
2709
+ })
2710
+ );
2711
+ }
2712
+ if (!t.hasLicense) {
2713
+ out.push(
2714
+ finding({
2715
+ category: "vcs",
2716
+ severity: "MEDIUM",
2717
+ title: "No LICENSE file",
2718
+ detail: "No license means nobody may legally use, modify, or redistribute this code."
2719
+ })
2720
+ );
2721
+ }
2722
+ if (!t.hasGitignore && (t.isNode || t.isPython || t.isRust || t.isGo)) {
2723
+ out.push(
2724
+ finding({
2725
+ category: "vcs",
2726
+ severity: "HIGH",
2727
+ title: "No .gitignore",
2728
+ detail: "A code project with no .gitignore will accumulate build output, virtualenvs, and node_modules as untracked noise."
2729
+ })
2730
+ );
2731
+ }
2732
+ if (!t.hasEditorconfig) {
2733
+ out.push(
2734
+ finding({
2735
+ category: "hygiene",
2736
+ severity: "LOW",
2737
+ title: "No .editorconfig",
2738
+ detail: "No shared whitespace/encoding standard. Formatting churns between contributors."
2739
+ })
2740
+ );
2741
+ }
2742
+ if (t.hasEnv && !t.hasEnvExample && (t.isNode || t.isPython)) {
2743
+ out.push(
2744
+ finding({
2745
+ category: "vcs",
2746
+ severity: "MEDIUM",
2747
+ title: ".env present but no .env.example",
2748
+ detail: "Documenting required variables (without values) is the cheapest onboarding win available."
2749
+ })
2750
+ );
2751
+ }
2752
+ if (t.isNode && !t.hasCi) {
2753
+ out.push(
2754
+ finding({
2755
+ category: "build",
2756
+ severity: "MEDIUM",
2757
+ title: "No CI configuration detected",
2758
+ detail: "No .github/workflows, .gitlab-ci.yml, or .circleci. Nothing runs the tests automatically."
2759
+ })
2760
+ );
2761
+ }
2762
+ return out;
2763
+ }
2764
+ function manifests(ctx) {
2765
+ const out = [];
2766
+ const has = (f) => ctx.rootFileNames.has(f);
2767
+ if (has("Gemfile") && !has("Gemfile.lock")) {
2768
+ out.push(
2769
+ finding({
2770
+ category: "deps",
2771
+ severity: "MEDIUM",
2772
+ title: "Gemfile present but no Gemfile.lock",
2773
+ detail: "Without the lockfile, gem versions drift between installs.",
2774
+ files: ["Gemfile"],
2775
+ lang: "ruby"
2776
+ })
2777
+ );
2778
+ }
2779
+ if (has("Gemfile") && !has(".ruby-version")) {
2780
+ out.push(
2781
+ finding({
2782
+ category: "build",
2783
+ severity: "LOW",
2784
+ title: "No .ruby-version",
2785
+ detail: "Ruby version unpinned.",
2786
+ lang: "ruby"
2787
+ })
2788
+ );
2789
+ }
2790
+ if (has("composer.json") && !has("composer.lock")) {
2791
+ out.push(
2792
+ finding({
2793
+ category: "deps",
2794
+ severity: "MEDIUM",
2795
+ title: "composer.json present but no composer.lock",
2796
+ detail: "No lockfile means dependency versions are not pinned.",
2797
+ files: ["composer.json"],
2798
+ lang: "php"
2799
+ })
2800
+ );
2801
+ }
2802
+ if (has("composer.json")) {
2803
+ const pkg = readJSON(join5(ctx.root, "composer.json"));
2804
+ if (pkg && !pkg.require) {
2805
+ out.push(
2806
+ finding({
2807
+ category: "config",
2808
+ severity: "MEDIUM",
2809
+ title: 'composer.json has no "require" section',
2810
+ detail: "A PHP project with no runtime requirements declared.",
2811
+ files: ["composer.json"],
2812
+ lang: "php"
2813
+ })
2814
+ );
2815
+ }
2816
+ }
2817
+ const gradleFiles = ["build.gradle", "build.gradle.kts"].filter((f) => has(f));
2818
+ if (gradleFiles.length > 0 && !has("gradle.lockfile") && !has("gradle/verification-metadata.xml")) {
2819
+ out.push(
2820
+ finding({
2821
+ category: "deps",
2822
+ severity: "LOW",
2823
+ title: "Gradle build with no dependency locking or verification",
2824
+ detail: "gradle.lockfile or dependency verification metadata pins and checksums your dependencies.",
2825
+ files: gradleFiles,
2826
+ lang: "java"
2827
+ })
2828
+ );
2829
+ }
2830
+ if (has("Package.swift") && !has("Package.resolved")) {
2831
+ out.push(
2832
+ finding({
2833
+ category: "deps",
2834
+ severity: "LOW",
2835
+ title: "Swift package with no Package.resolved",
2836
+ detail: "Commit Package.resolved to pin exact dependency versions for apps.",
2837
+ files: ["Package.swift"],
2838
+ lang: "swift"
2839
+ })
2840
+ );
2841
+ }
2842
+ if (has("mix.exs") && !has("mix.lock")) {
2843
+ out.push(
2844
+ finding({
2845
+ category: "deps",
2846
+ severity: "MEDIUM",
2847
+ title: "mix.exs present but no mix.lock",
2848
+ detail: "No lockfile for hex dependencies.",
2849
+ files: ["mix.exs"],
2850
+ lang: "elixir"
2851
+ })
2852
+ );
2853
+ }
2854
+ if (has("pubspec.yaml") && !has("pubspec.lock")) {
2855
+ out.push(
2856
+ finding({
2857
+ category: "deps",
2858
+ severity: "LOW",
2859
+ title: "pubspec.yaml present but no pubspec.lock",
2860
+ detail: "Applications should commit pubspec.lock to pin package versions.",
2861
+ files: ["pubspec.yaml"],
2862
+ lang: "dart"
2863
+ })
2864
+ );
2865
+ }
2866
+ return out;
2867
+ }
2868
+ function sql(ctx) {
2869
+ const out = [];
2870
+ const files = ctx.sourceFiles.filter((f) => f.lang === "sql");
2871
+ if (files.length === 0) return out;
2872
+ const starFiles = files.filter((f) => /\bSELECT\s+\*/i.test(readText(f.abs)));
2873
+ if (starFiles.length > 0) {
2874
+ out.push(
2875
+ finding({
2876
+ category: "structure",
2877
+ severity: "LOW",
2878
+ title: `SELECT * in ${starFiles.length} SQL file(s)`,
2879
+ detail: "Column-list-independent queries break silently when the schema changes. Enumerate columns.",
2880
+ files: starFiles.slice(0, 8).map((f) => f.rel),
2881
+ count: starFiles.length,
2882
+ lang: "sql"
2883
+ })
2884
+ );
2885
+ }
2886
+ return out;
2887
+ }
2888
+ function analyze8(ctx) {
2889
+ return [...scaffolding(ctx), ...manifests(ctx), ...sql(ctx)];
2890
+ }
2891
+
2892
+ // src/analyzers/index.ts
2893
+ var ANALYZERS = [
2894
+ { id: "universal", label: "Universal (secrets, bloat, markers, hygiene)", fn: analyze },
2895
+ { id: "notebooks", label: "Jupyter notebooks", fn: analyze7 },
2896
+ { id: "scaffolding", label: "Project scaffolding & manifests", fn: analyze8 },
2897
+ { id: "testing", label: "Test coverage ratio", fn: analyze2 },
2898
+ { id: "git", label: "Git history & tracked files", fn: analyze3 },
2899
+ { id: "node", label: "JavaScript / TypeScript", fn: analyze4 },
2900
+ { id: "python", label: "Python", fn: analyze5 },
2901
+ { id: "systems", label: "Rust / Go / C / C++ / C# / Zig", fn: analyze6 }
2902
+ ];
2903
+
2904
+ // src/cli.ts
2905
+ var SPEC_VERSION = 1;
2906
+ function parseArgs(argv) {
2907
+ const opts = {
2908
+ root: process.cwd(),
2909
+ format: "summary",
2910
+ out: null,
2911
+ failOn: null,
2912
+ maxFiles: 6e4,
2913
+ commitLimit: 1e3,
2914
+ noGit: false,
2915
+ noLockfileWarning: false,
2916
+ quiet: false,
2917
+ scope: null,
2918
+ listLanguages: false,
2919
+ help: false,
2920
+ color: null,
2921
+ ignoreDirs: []
2922
+ };
2923
+ for (let i = 0; i < argv.length; i++) {
2924
+ const a = argv[i];
2925
+ switch (a) {
2926
+ case "--package":
2927
+ case "--scope":
2928
+ opts.scope = argv[++i];
2929
+ break;
2930
+ case "--ignore-dir":
2931
+ if (argv[i + 1]) opts.ignoreDirs.push(argv[++i]);
2932
+ break;
2933
+ case "--path":
2934
+ opts.root = resolve(argv[++i] || ".");
2935
+ break;
2936
+ case "--format":
2937
+ opts.format = (argv[++i] || "json").toLowerCase();
2938
+ break;
2939
+ case "--out":
2940
+ opts.out = argv[++i];
2941
+ break;
2942
+ case "--fail-on":
2943
+ opts.failOn = (argv[++i] || "").toUpperCase();
2944
+ break;
2945
+ case "--max-files":
2946
+ opts.maxFiles = Number.parseInt(argv[++i] || "", 10) || opts.maxFiles;
2947
+ break;
2948
+ case "--commit-limit":
2949
+ opts.commitLimit = Number.parseInt(argv[++i] || "", 10) || opts.commitLimit;
2950
+ break;
2951
+ case "--no-git":
2952
+ opts.noGit = true;
2953
+ break;
2954
+ case "--no-lockfile-warning":
2955
+ opts.noLockfileWarning = true;
2956
+ break;
2957
+ case "--quiet":
2958
+ opts.quiet = true;
2959
+ break;
2960
+ case "--color":
2961
+ opts.color = true;
2962
+ break;
2963
+ case "--no-color":
2964
+ opts.color = false;
2965
+ break;
2966
+ case "--list-languages":
2967
+ opts.listLanguages = true;
2968
+ break;
2969
+ case "-h":
2970
+ case "--help":
2971
+ opts.help = true;
2972
+ break;
2973
+ default:
2974
+ if (a.startsWith("-")) {
2975
+ process.stderr.write(`unknown option: ${a}
2976
+ Run with --help for the option list.
2977
+ `);
2978
+ process.exit(2);
2979
+ }
2980
+ if (a !== ".") opts.root = resolve(a);
2981
+ break;
2982
+ }
2983
+ }
2984
+ return opts;
2985
+ }
2986
+ function printHelp() {
2987
+ process.stdout.write(`flamer, language-agnostic code health auditor
2988
+
2989
+ Usage:
2990
+ flamer [path] [options]
2991
+
2992
+ Options:
2993
+ --package, --scope <name> analyze only the named sub-project
2994
+ --path <dir> repository root (default: cwd)
2995
+ --format <json|summary> output format (default: summary)
2996
+ --out <file> also write the JSON report to a file
2997
+ --fail-on <SEVERITY> exit 1 if a finding is at/above this severity
2998
+ (${SEVERITIES.join(" | ")})
2999
+ --max-files <n> stop walking after n files (default ${6e4})
3000
+ --commit-limit <n> commits to inspect for git analysis (default 1000)
3001
+ --no-git skip git analysis entirely
3002
+ --no-lockfile-warning skip the "no lockfile" suggestion
3003
+ --ignore-dir <name> also skip this directory name (repeatable)
3004
+ --quiet suppress the human summary
3005
+ --color force ANSI colors
3006
+ --no-color disable ANSI colors
3007
+ --list-languages print the supported language table
3008
+ -h, --help show this help
3009
+ `);
3010
+ }
3011
+ function printLanguages() {
3012
+ process.stdout.write(`flamer understands ${LANGUAGES.length} language/ecosystem profiles:
3013
+
3014
+ `);
3015
+ for (const l of LANGUAGES) {
3016
+ process.stdout.write(` ${l.id.padEnd(12)} ${l.label.padEnd(22)} ${l.exts.join(" ")}
3017
+ `);
3018
+ }
3019
+ process.stdout.write(`
3020
+ ${allExtensions().length} file extensions recognized.
3021
+ `);
3022
+ }
3023
+ function run2(opts) {
3024
+ const started = Date.now();
3025
+ const root = opts.root;
3026
+ if (!existsSync3(root) || !isDir(root)) {
3027
+ const err = {
3028
+ specVersion: SPEC_VERSION,
3029
+ tool: "flamer",
3030
+ version: 2,
3031
+ project: { name: basename(root), kind: [], languages: [], root, scopes: [] },
3032
+ findings: [
3033
+ finding({
3034
+ category: "error",
3035
+ severity: "CRITICAL",
3036
+ title: `Path not found or not a directory: ${root}`,
3037
+ detail: "Point flamer at an existing repository directory."
3038
+ })
3039
+ ],
3040
+ metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), durationSeconds: 0 }
3041
+ };
3042
+ return { report: err, exitCode: 2 };
3043
+ }
3044
+ const detected = detectScopes(root);
3045
+ const walkStart = Date.now();
3046
+ let truncated = false;
3047
+ const collected = [];
3048
+ walkTree(root, {
3049
+ extraIgnores: new Set(opts.ignoreDirs),
3050
+ onFile: (rec) => {
3051
+ if (collected.length >= opts.maxFiles) {
3052
+ truncated = true;
3053
+ return;
3054
+ }
3055
+ collected.push(scanFile(rec));
3056
+ }
3057
+ });
3058
+ const files = collected;
3059
+ const walkMs = Date.now() - walkStart;
3060
+ const profile = buildProfile(root, detected, files);
3061
+ let scopes = profile.scopes;
3062
+ let scopedFiles = files;
3063
+ if (opts.scope) {
3064
+ const match = scopes.find((s) => s.name === opts.scope);
3065
+ if (!match) {
3066
+ const available = scopes.map((s) => s.name || "root").join(", ") || "none";
3067
+ const rep = emptyReport(root, profile, started);
3068
+ rep.findings.push(
3069
+ finding({
3070
+ category: "error",
3071
+ severity: "LOW",
3072
+ title: `Scope "${opts.scope}" not found`,
3073
+ detail: `Available scopes: ${available}.`
3074
+ })
3075
+ );
3076
+ return { report: rep, exitCode: 0 };
3077
+ }
3078
+ scopes = [match];
3079
+ if (match.path) {
3080
+ const prefix = `${match.path}/`;
3081
+ scopedFiles = files.filter((f) => f.rel === match.path || f.rel.startsWith(prefix));
3082
+ }
3083
+ }
3084
+ const sourceFiles = scopedFiles.filter(isSource);
3085
+ const structuredFiles = scopedFiles.filter(isLineStructured);
3086
+ const rootFileNames = new Set(filesIn(root));
3087
+ const repoSourceFileCount = files.filter(isSource).length;
3088
+ let gitTracked = null;
3089
+ if (!opts.noGit) gitTracked = isRepo(root) ? trackedFiles(root) : /* @__PURE__ */ new Set();
3090
+ const ctx = {
3091
+ root,
3092
+ options: opts,
3093
+ files: scopedFiles,
3094
+ sourceFiles,
3095
+ structuredFiles,
3096
+ repoSourceFileCount,
3097
+ rootFileNames,
3098
+ profile: { ...profile, scopes },
3099
+ scopes,
3100
+ gitTracked
3101
+ };
3102
+ const findings = [];
3103
+ const analyzerResults = [];
3104
+ for (const a of ANALYZERS) {
3105
+ const t0 = Date.now();
3106
+ try {
3107
+ const res = a.fn(ctx) || [];
3108
+ findings.push(...res);
3109
+ analyzerResults.push({
3110
+ id: a.id,
3111
+ label: a.label,
3112
+ findings: res.length,
3113
+ ms: Date.now() - t0,
3114
+ error: null
3115
+ });
3116
+ } catch (e) {
3117
+ const msg = e && e.message ? e.message : String(e);
3118
+ findings.push(
3119
+ finding({
3120
+ category: "error",
3121
+ severity: "LOW",
3122
+ title: `Analyzer "${a.id}" failed`,
3123
+ detail: msg
3124
+ })
3125
+ );
3126
+ analyzerResults.push({
3127
+ id: a.id,
3128
+ label: a.label,
3129
+ findings: 0,
3130
+ ms: Date.now() - t0,
3131
+ error: msg
3132
+ });
3133
+ }
3134
+ }
3135
+ const sorted = sortFindings(findings);
3136
+ const report = {
3137
+ specVersion: SPEC_VERSION,
3138
+ tool: "flamer",
3139
+ version: 2,
3140
+ project: {
3141
+ name: basename(root),
3142
+ root,
3143
+ kind: projectKind(profile),
3144
+ layout: profile.layout,
3145
+ languages: profile.languages,
3146
+ dominantLanguage: profile.dominantLanguage,
3147
+ packages: scopes.map((s) => s.name || "root"),
3148
+ totalFiles: scopedFiles.length,
3149
+ totalSourceFiles: sourceFiles.length,
3150
+ totalLines: sourceFiles.reduce((s, f) => s + (f.lines || 0), 0),
3151
+ tooling: profile.tooling
3152
+ },
3153
+ findings: sorted,
3154
+ metadata: {
3155
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3156
+ durationSeconds: (Date.now() - started) / 1e3,
3157
+ walkMs,
3158
+ truncated,
3159
+ severities: severityTally(sorted),
3160
+ worst: worstSeverity(sorted),
3161
+ analyzers: analyzerResults
3162
+ }
3163
+ };
3164
+ let exitCode = 0;
3165
+ if (opts.failOn && report.metadata.worst && rank(report.metadata.worst) <= rank(opts.failOn)) {
3166
+ exitCode = 1;
3167
+ }
3168
+ return { report, exitCode };
3169
+ }
3170
+ function emptyReport(root, profile, started) {
3171
+ return {
3172
+ specVersion: SPEC_VERSION,
3173
+ tool: "flamer",
3174
+ version: 2,
3175
+ project: {
3176
+ name: basename(root),
3177
+ kind: projectKind(profile),
3178
+ languages: profile.languages,
3179
+ packages: []
3180
+ },
3181
+ findings: [],
3182
+ metadata: {
3183
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3184
+ durationSeconds: (Date.now() - started) / 1e3
3185
+ }
3186
+ };
3187
+ }
3188
+ var ANSI = {
3189
+ reset: "\x1B[0m",
3190
+ bold: "\x1B[1m",
3191
+ dim: "\x1B[2m",
3192
+ red: "\x1B[31m",
3193
+ yellow: "\x1B[33m",
3194
+ magenta: "\x1B[35m",
3195
+ cyan: "\x1B[36m",
3196
+ green: "\x1B[32m",
3197
+ gray: "\x1B[90m"
3198
+ };
3199
+ function colorEnabled(force) {
3200
+ if (force !== null) return force;
3201
+ return Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
3202
+ }
3203
+ function paint(text, color, enabled) {
3204
+ return enabled ? `${color}${text}${ANSI.reset}` : text;
3205
+ }
3206
+ function severityColor(severity) {
3207
+ switch (severity) {
3208
+ case "CRITICAL":
3209
+ return ANSI.red;
3210
+ case "HIGH":
3211
+ return ANSI.yellow;
3212
+ case "MEDIUM":
3213
+ return ANSI.magenta;
3214
+ case "LOW":
3215
+ return ANSI.gray;
3216
+ default:
3217
+ return ANSI.cyan;
3218
+ }
3219
+ }
3220
+ function severityIcon(severity) {
3221
+ return severity === "LOW" ? "\xB7" : severity === "INFO" ? "i" : "!";
3222
+ }
3223
+ function printSummary(report, forceColor) {
3224
+ const enabled = colorEnabled(forceColor);
3225
+ const p = report.project;
3226
+ const out = [];
3227
+ const width = 62;
3228
+ const duration = report.metadata.durationSeconds;
3229
+ out.push("");
3230
+ out.push(
3231
+ ` ${paint("FLAMER", ANSI.bold + ANSI.cyan, enabled)} ${paint(p.name, ANSI.bold, enabled)}`
3232
+ );
3233
+ out.push(paint(` ${"\u2500".repeat(width)}`, ANSI.dim, enabled));
3234
+ if (p.languages?.length) {
3235
+ out.push(
3236
+ ` ${p.languages.map((l) => l.label).join(", ")} ${paint("\xB7", ANSI.dim, enabled)} ${(p.totalFiles || 0).toLocaleString()} files ${paint("\xB7", ANSI.dim, enabled)} ${(p.totalLines || 0).toLocaleString()} lines ${paint("\xB7", ANSI.dim, enabled)} ${duration.toFixed(2)}s`
3237
+ );
3238
+ } else {
3239
+ out.push(
3240
+ ` ${p.kind?.join(", ") || "unknown"} ${paint("\xB7", ANSI.dim, enabled)} ${duration.toFixed(2)}s`
3241
+ );
3242
+ }
3243
+ out.push("");
3244
+ const sev = report.metadata.severities || {};
3245
+ for (const s of SEVERITIES) {
3246
+ if (sev[s]) {
3247
+ const label = `${severityIcon(s)} ${sev[s]} ${s.toLowerCase()}`;
3248
+ out.push(` ${paint(label, severityColor(s), enabled)}`);
3249
+ }
3250
+ }
3251
+ if (report.findings.length > 0) {
3252
+ out.push("");
3253
+ for (const severity of SEVERITIES) {
3254
+ const findings = report.findings.filter((f) => f.severity === severity);
3255
+ if (findings.length === 0) continue;
3256
+ out.push(` ${paint(severity, severityColor(severity), enabled)}`);
3257
+ for (const f of findings) {
3258
+ out.push(
3259
+ ` ${paint(severityIcon(f.severity), severityColor(f.severity), enabled)} ${f.title}`
3260
+ );
3261
+ if (f.detail) out.push(` ${paint(f.detail, ANSI.dim, enabled)}`);
3262
+ if (f.files?.length) {
3263
+ out.push(
3264
+ ` ${paint(f.files.slice(0, 5).join(", ") + (f.files.length > 5 ? " ..." : ""), ANSI.gray, enabled)}`
3265
+ );
3266
+ }
3267
+ }
3268
+ }
3269
+ }
3270
+ out.push("");
3271
+ out.push(paint(` ${"\u2500".repeat(width)}`, ANSI.dim, enabled));
3272
+ if (report.findings.length === 0) {
3273
+ out.push(` ${paint("\u2713 No findings \xB7 scan completed", ANSI.green, enabled)}`);
3274
+ } else {
3275
+ out.push(
3276
+ ` ${paint(`${report.findings.length} finding${report.findings.length === 1 ? "" : "s"} \xB7 scan completed`, ANSI.bold, enabled)}`
3277
+ );
3278
+ }
3279
+ process.stdout.write(out.join("\n") + "\n");
3280
+ }
3281
+ for (const stream of [process.stdout, process.stderr]) {
3282
+ stream.on("error", (err) => {
3283
+ if (err && err.code === "EPIPE") process.exit(0);
3284
+ throw err;
3285
+ });
3286
+ }
3287
+ function main() {
3288
+ const opts = parseArgs(process.argv.slice(2));
3289
+ if (opts.help) return printHelp();
3290
+ if (opts.listLanguages) return printLanguages();
3291
+ const { report, exitCode } = run2(opts);
3292
+ const json = JSON.stringify(report, null, 2);
3293
+ if (opts.out) {
3294
+ try {
3295
+ writeFileSync(opts.out, json + "\n");
3296
+ } catch (e) {
3297
+ process.stderr.write(`warning: could not write ${opts.out}: ${e.message}
3298
+ `);
3299
+ }
3300
+ }
3301
+ if (opts.format === "summary") {
3302
+ if (!opts.quiet) printSummary(report, opts.color);
3303
+ } else {
3304
+ process.stdout.write(json + "\n");
3305
+ }
3306
+ process.exitCode = exitCode;
3307
+ }
3308
+ main();