dsh-codebase-chat 0.25.6 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -8
- package/dist/cli.js +1040 -82
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.js +941 -66
- package/dist/index.js.map +1 -1
- package/package.json +1 -4
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { parseArgs } from "util";
|
|
5
5
|
import { watch } from "fs";
|
|
6
|
-
import { sep as sep5, basename as
|
|
6
|
+
import { sep as sep5, basename as basename4 } from "path";
|
|
7
7
|
|
|
8
8
|
// src/context.ts
|
|
9
9
|
import { join as join5 } from "path";
|
|
@@ -1652,6 +1652,95 @@ function formatCycle(c, md = false) {
|
|
|
1652
1652
|
const shown = c.path.slice(0, 4).map(q).join(", ");
|
|
1653
1653
|
return `${c.path.length} files: ${shown}, \u2026`;
|
|
1654
1654
|
}
|
|
1655
|
+
var GRADE_ICON = { A: "\u{1F7E2}", B: "\u{1F535}", C: "\u{1F7E1}", D: "\u{1F7E0}", E: "\u{1F534}" };
|
|
1656
|
+
function scoreBar(score) {
|
|
1657
|
+
const filled = Math.round(score / 10);
|
|
1658
|
+
return "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
|
|
1659
|
+
}
|
|
1660
|
+
function formatHealthReportMd(r, lang = "fr") {
|
|
1661
|
+
const t2 = lang === "en" ? {
|
|
1662
|
+
title: "Static analysis",
|
|
1663
|
+
files: "files analyzed",
|
|
1664
|
+
edges: "local imports",
|
|
1665
|
+
cycles: "Circular dependencies",
|
|
1666
|
+
none: "None",
|
|
1667
|
+
unusedFiles: "Unused files (candidates)",
|
|
1668
|
+
unusedExports: "Unused exports (candidates)",
|
|
1669
|
+
dupes: "Duplicate code blocks",
|
|
1670
|
+
hotspots: "Complexity hotspots",
|
|
1671
|
+
score: "Health score",
|
|
1672
|
+
noteUnused: "_candidates \u2014 entry points and framework conventions excluded_",
|
|
1673
|
+
colFile: "File",
|
|
1674
|
+
colSymbol: "Symbol",
|
|
1675
|
+
colSize: "Size",
|
|
1676
|
+
colFiles: "Files",
|
|
1677
|
+
colScore: "Score",
|
|
1678
|
+
more: (n) => `_\u2026and ${n} more_`
|
|
1679
|
+
} : {
|
|
1680
|
+
title: "Analyse statique",
|
|
1681
|
+
files: "fichiers analys\xE9s",
|
|
1682
|
+
edges: "imports locaux",
|
|
1683
|
+
cycles: "D\xE9pendances circulaires",
|
|
1684
|
+
none: "Aucune",
|
|
1685
|
+
unusedFiles: "Fichiers inutilis\xE9s (candidats)",
|
|
1686
|
+
unusedExports: "Exports inutilis\xE9s (candidats)",
|
|
1687
|
+
dupes: "Blocs de code dupliqu\xE9s",
|
|
1688
|
+
hotspots: "Hotspots de complexit\xE9",
|
|
1689
|
+
score: "Score de sant\xE9",
|
|
1690
|
+
noteUnused: "_candidats \u2014 points d\u2019entr\xE9e et conventions exclus_",
|
|
1691
|
+
colFile: "Fichier",
|
|
1692
|
+
colSymbol: "Symbole",
|
|
1693
|
+
colSize: "Taille",
|
|
1694
|
+
colFiles: "Fichiers",
|
|
1695
|
+
colScore: "Score",
|
|
1696
|
+
more: (n) => `_\u2026et ${n} autres_`
|
|
1697
|
+
};
|
|
1698
|
+
const out = [];
|
|
1699
|
+
out.push(`## ${GRADE_ICON[r.grade]} ${t2.title} \u2014 \`${basename(r.projectPath)}\``);
|
|
1700
|
+
out.push("");
|
|
1701
|
+
if (r.analyzedFiles === 0) {
|
|
1702
|
+
out.push(lang === "en" ? "_No code files detected in this project \u2014 check the path or your `.codebase-chat.json` ignore rules._" : "_Aucun fichier de code d\xE9tect\xE9 dans ce projet \u2014 v\xE9rifie le chemin ou les r\xE8gles ignore de `.codebase-chat.json`._");
|
|
1703
|
+
return out.join("\n");
|
|
1704
|
+
}
|
|
1705
|
+
out.push(`**${t2.score} : ${scoreBar(r.score)} ${r.score}/100 (${r.grade})** \xB7 ${r.analyzedFiles} ${t2.files} \xB7 ${r.importEdges} ${t2.edges}`);
|
|
1706
|
+
out.push("");
|
|
1707
|
+
out.push(`### ${t2.cycles} \u2014 ${r.cycles.length}`);
|
|
1708
|
+
if (r.cycles.length === 0) out.push(t2.none);
|
|
1709
|
+
else for (const c of r.cycles.slice(0, 10)) out.push(`- ${formatCycle(c, true)}`);
|
|
1710
|
+
if (r.cycles.length > 10) out.push(t2.more(r.cycles.length - 10));
|
|
1711
|
+
out.push("");
|
|
1712
|
+
out.push(`### ${t2.unusedFiles} \u2014 ${r.unusedFiles.length}`);
|
|
1713
|
+
out.push(t2.noteUnused);
|
|
1714
|
+
if (r.unusedFiles.length) {
|
|
1715
|
+
out.push("", `| ${t2.colFile} |`, "|---|---|");
|
|
1716
|
+
for (const f of r.unusedFiles.slice(0, 15)) out.push(`| \`${f}\` |`);
|
|
1717
|
+
if (r.unusedFiles.length > 15) out.push(`| ${t2.more(r.unusedFiles.length - 15)} |`);
|
|
1718
|
+
}
|
|
1719
|
+
out.push("");
|
|
1720
|
+
out.push(`### ${t2.unusedExports} \u2014 ${r.unusedExports.length}`);
|
|
1721
|
+
if (r.unusedExports.length) {
|
|
1722
|
+
out.push("", `| ${t2.colFile} | ${t2.colSymbol} |`, "|---|---|");
|
|
1723
|
+
for (const e of r.unusedExports.slice(0, 15)) out.push(`| \`${e.file}:${e.line}\` | \`${e.name}\` |`);
|
|
1724
|
+
if (r.unusedExports.length > 15) out.push(`| ${t2.more(r.unusedExports.length - 15)} | |`);
|
|
1725
|
+
} else out.push(t2.none);
|
|
1726
|
+
out.push("");
|
|
1727
|
+
out.push(`### ${t2.dupes} \u2014 ${r.duplicates.length}`);
|
|
1728
|
+
if (r.duplicates.length) {
|
|
1729
|
+
out.push("", `| ${t2.colSize} | ${t2.colFiles} |`, "|---|---|");
|
|
1730
|
+
for (const d of r.duplicates.slice(0, 8)) {
|
|
1731
|
+
out.push(`| ${d.lines} \xD7 ${d.files.length} | ${d.files.map((f) => `\`${f}\``).join(", ")} |`);
|
|
1732
|
+
}
|
|
1733
|
+
if (r.duplicates.length > 8) out.push(`| ${t2.more(r.duplicates.length - 8)} | |`);
|
|
1734
|
+
} else out.push(t2.none);
|
|
1735
|
+
out.push("");
|
|
1736
|
+
out.push(`### ${t2.hotspots} \u2014 ${r.hotspots.length}`);
|
|
1737
|
+
if (r.hotspots.length) {
|
|
1738
|
+
out.push("", `| ${t2.colFile} | ${t2.colScore} |`, "|---|---|");
|
|
1739
|
+
for (const h of r.hotspots.slice(0, 10)) out.push(`| \`${h.file}\` | **${h.score}** |`);
|
|
1740
|
+
if (r.hotspots.length > 10) out.push(`| ${t2.more(r.hotspots.length - 10)} | |`);
|
|
1741
|
+
} else out.push(t2.none);
|
|
1742
|
+
return out.join("\n");
|
|
1743
|
+
}
|
|
1655
1744
|
|
|
1656
1745
|
// src/impact.ts
|
|
1657
1746
|
import { basename as basename2 } from "path";
|
|
@@ -2532,80 +2621,956 @@ function buildToolPrompt(tool, opts) {
|
|
|
2532
2621
|
return normalizeLabels(builder(opts), opts.lang || "fr");
|
|
2533
2622
|
}
|
|
2534
2623
|
|
|
2535
|
-
// src/
|
|
2536
|
-
import {
|
|
2537
|
-
import { join as join8 } from "path";
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
var
|
|
2541
|
-
|
|
2542
|
-
|
|
2624
|
+
// src/report.ts
|
|
2625
|
+
import { access, readFile as readFile6 } from "fs/promises";
|
|
2626
|
+
import { basename as basename3, extname as extname3, join as join8 } from "path";
|
|
2627
|
+
import { execFile as execFile2 } from "child_process";
|
|
2628
|
+
import { promisify as promisify2 } from "util";
|
|
2629
|
+
var run2 = promisify2(execFile2);
|
|
2630
|
+
var EXT_LANG = {
|
|
2631
|
+
".ts": "TypeScript",
|
|
2632
|
+
".tsx": "TypeScript (React)",
|
|
2633
|
+
".js": "JavaScript",
|
|
2634
|
+
".jsx": "JavaScript (React)",
|
|
2635
|
+
".mjs": "JavaScript (ESM)",
|
|
2636
|
+
".cjs": "JavaScript (CJS)",
|
|
2637
|
+
".py": "Python",
|
|
2638
|
+
".rs": "Rust",
|
|
2639
|
+
".go": "Go",
|
|
2640
|
+
".java": "Java",
|
|
2641
|
+
".kt": "Kotlin",
|
|
2642
|
+
".rb": "Ruby",
|
|
2643
|
+
".php": "PHP",
|
|
2644
|
+
".c": "C",
|
|
2645
|
+
".h": "C/C++",
|
|
2646
|
+
".cpp": "C++",
|
|
2647
|
+
".cs": "C#",
|
|
2648
|
+
".swift": "Swift",
|
|
2649
|
+
".vue": "Vue",
|
|
2650
|
+
".svelte": "Svelte",
|
|
2651
|
+
".dart": "Dart",
|
|
2652
|
+
".lua": "Lua"
|
|
2653
|
+
};
|
|
2654
|
+
function bar(score) {
|
|
2655
|
+
const filled = Math.round(score / 10);
|
|
2656
|
+
return "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
|
|
2657
|
+
}
|
|
2658
|
+
var SMELL_PATS = [
|
|
2659
|
+
["todo", /\b(?:TODO|FIXME|HACK|XXX|WIP)\b/i],
|
|
2660
|
+
["console", /\bconsole\.(log|warn|error|debug|info)\s*\(/],
|
|
2661
|
+
["tsIgnore", /@ts-(ignore|expect-error|nocheck)\b/],
|
|
2662
|
+
["any", /:\s*any\b/],
|
|
2663
|
+
["emptyCatch", /catch\s*\([^)]*\)\s*\{\s*\}/],
|
|
2664
|
+
["debugger", /\bdebugger\s*;/],
|
|
2665
|
+
["syncIo", /\b(readFileSync|writeFileSync|appendFileSync|readdirSync|mkdirSync|execSync)\s*\(/]
|
|
2666
|
+
];
|
|
2667
|
+
var SEC_PATS = [
|
|
2668
|
+
["secret", /(?:api[_-]?key|secret|passwd|password|token|private[_-]?key)\s*[:=]\s*['"`][A-Za-z0-9_\/+\-.]{8,}['"`]/i],
|
|
2669
|
+
["eval", /\beval\s*\(|new\s+Function\s*\(/],
|
|
2670
|
+
["exec", /\bexecSync\s*\(|child_process/],
|
|
2671
|
+
["innerHTML", /\.innerHTML\s*=/],
|
|
2672
|
+
["unsafeRegex", /new\s+RegExp\s*\([^'"`]/]
|
|
2673
|
+
];
|
|
2674
|
+
function scanCode(fileTexts, pats, perFileCap = 3) {
|
|
2675
|
+
const out = {};
|
|
2676
|
+
for (const [file, text] of fileTexts) {
|
|
2677
|
+
if (/test|spec|__tests__|\.d\.ts$/i.test(file)) continue;
|
|
2678
|
+
const lines = text.split("\n");
|
|
2679
|
+
for (const [key, re] of pats) {
|
|
2680
|
+
let found = 0;
|
|
2681
|
+
for (let i = 0; i < lines.length && found < perFileCap; i++) {
|
|
2682
|
+
if (re.test(lines[i])) {
|
|
2683
|
+
(out[key] ??= []).push({ file, line: i + 1, sample: lines[i].trim().slice(0, 90) });
|
|
2684
|
+
found++;
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
return out;
|
|
2543
2690
|
}
|
|
2544
|
-
function
|
|
2545
|
-
|
|
2546
|
-
if (!v || v === "1" || v.toLowerCase() === "true") return DEFAULT_MODEL_URI;
|
|
2547
|
-
return v;
|
|
2691
|
+
function countHits(scan) {
|
|
2692
|
+
return Object.values(scan).reduce((s, f) => s + f.length, 0);
|
|
2548
2693
|
}
|
|
2549
|
-
|
|
2694
|
+
var SENSITIVE_PATS = /(^|\/)\.env$|(^|\/)\.env\.(local|prod|production|dev|development)$|\.(pem|key|p12|pfx|keystore)$|id_rsa|id_ed25519|credentials\.json|service-account/i;
|
|
2695
|
+
async function gitActivity(abs) {
|
|
2550
2696
|
try {
|
|
2551
|
-
|
|
2697
|
+
const { stdout } = await run2("git", [
|
|
2698
|
+
"-C",
|
|
2699
|
+
abs,
|
|
2700
|
+
"log",
|
|
2701
|
+
"--numstat",
|
|
2702
|
+
"--format=@@@%an|%ad|%s",
|
|
2703
|
+
"--date=short",
|
|
2704
|
+
"-n",
|
|
2705
|
+
"400"
|
|
2706
|
+
], { maxBuffer: 32 * 1024 * 1024 });
|
|
2707
|
+
const stats = { commits: 0, authors: /* @__PURE__ */ new Map(), lastDate: "", churn: /* @__PURE__ */ new Map(), fileAuthors: /* @__PURE__ */ new Map(), months: /* @__PURE__ */ new Map(), sensitiveTracked: [], fileLastCommit: /* @__PURE__ */ new Map(), subjects: [], commitSizes: [] };
|
|
2708
|
+
let author = "";
|
|
2709
|
+
let date = "";
|
|
2710
|
+
let curFiles = 0, curLines = 0;
|
|
2711
|
+
const flush = () => {
|
|
2712
|
+
if (curFiles || curLines) stats.commitSizes.push({ files: curFiles, lines: curLines });
|
|
2713
|
+
curFiles = 0;
|
|
2714
|
+
curLines = 0;
|
|
2715
|
+
};
|
|
2716
|
+
for (const line of stdout.split("\n")) {
|
|
2717
|
+
if (line.startsWith("@@@")) {
|
|
2718
|
+
flush();
|
|
2719
|
+
stats.commits++;
|
|
2720
|
+
const [a, d, s] = line.slice(3).split("|");
|
|
2721
|
+
author = a;
|
|
2722
|
+
date = d;
|
|
2723
|
+
if (s) stats.subjects.push(s);
|
|
2724
|
+
if (!stats.lastDate) stats.lastDate = d;
|
|
2725
|
+
stats.authors.set(a, (stats.authors.get(a) ?? 0) + 1);
|
|
2726
|
+
const month = d.slice(0, 7);
|
|
2727
|
+
stats.months.set(month, (stats.months.get(month) ?? 0) + 1);
|
|
2728
|
+
continue;
|
|
2729
|
+
}
|
|
2730
|
+
const m = line.match(/^(\d+|-)\t(\d+|-)\t(.+)$/);
|
|
2731
|
+
if (!m || m[1] === "-") continue;
|
|
2732
|
+
const file = m[3].replace(/\\/g, "/");
|
|
2733
|
+
const delta = Number(m[1]) + Number(m[2]);
|
|
2734
|
+
curFiles++;
|
|
2735
|
+
curLines += delta;
|
|
2736
|
+
stats.churn.set(file, (stats.churn.get(file) ?? 0) + delta);
|
|
2737
|
+
if (!stats.fileLastCommit.has(file) && date) stats.fileLastCommit.set(file, date);
|
|
2738
|
+
if (author) (stats.fileAuthors.get(file) ?? stats.fileAuthors.set(file, /* @__PURE__ */ new Set()).get(file)).add(author);
|
|
2739
|
+
}
|
|
2740
|
+
flush();
|
|
2741
|
+
try {
|
|
2742
|
+
const { stdout: tracked } = await run2("git", ["-C", abs, "ls-files"], { maxBuffer: 8 * 1024 * 1024 });
|
|
2743
|
+
stats.sensitiveTracked = tracked.split("\n").map((l) => l.trim()).filter((f) => f && SENSITIVE_PATS.test(f));
|
|
2744
|
+
} catch {
|
|
2745
|
+
}
|
|
2746
|
+
return stats;
|
|
2552
2747
|
} catch {
|
|
2553
|
-
|
|
2554
|
-
}
|
|
2555
|
-
}
|
|
2556
|
-
var
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2748
|
+
return null;
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
var SYSTEM_ENV = /* @__PURE__ */ new Set([
|
|
2752
|
+
"PATH",
|
|
2753
|
+
"PATHEXT",
|
|
2754
|
+
"HOME",
|
|
2755
|
+
"HOMEPATH",
|
|
2756
|
+
"USERPROFILE",
|
|
2757
|
+
"USERNAME",
|
|
2758
|
+
"USER",
|
|
2759
|
+
"APPDATA",
|
|
2760
|
+
"LOCALAPPDATA",
|
|
2761
|
+
"TEMP",
|
|
2762
|
+
"TMP",
|
|
2763
|
+
"TMPDIR",
|
|
2764
|
+
"OS",
|
|
2765
|
+
"COMSPEC",
|
|
2766
|
+
"SYSTEMROOT",
|
|
2767
|
+
"WINDIR",
|
|
2768
|
+
"PROGRAMFILES",
|
|
2769
|
+
"PROGRAMDATA",
|
|
2770
|
+
"NUMBER_OF_PROCESSORS",
|
|
2771
|
+
"PROCESSOR_ARCHITECTURE",
|
|
2772
|
+
"SHELL",
|
|
2773
|
+
"TERM",
|
|
2774
|
+
"PWD",
|
|
2775
|
+
"OLDPWD",
|
|
2776
|
+
"HOME",
|
|
2777
|
+
"LANG",
|
|
2778
|
+
"LC_ALL",
|
|
2779
|
+
"TZ",
|
|
2780
|
+
"NODE_ENV",
|
|
2781
|
+
"NODE_PATH",
|
|
2782
|
+
"npm_config_cache",
|
|
2783
|
+
"CI",
|
|
2784
|
+
"HOSTNAME"
|
|
2785
|
+
]);
|
|
2786
|
+
async function envAudit(abs, fileTexts) {
|
|
2787
|
+
const used = /* @__PURE__ */ new Set();
|
|
2788
|
+
for (const [file, text] of fileTexts) {
|
|
2789
|
+
if (/test|spec|__tests__/i.test(file)) continue;
|
|
2790
|
+
for (const m of text.matchAll(/\bprocess\.env\.([A-Z_][A-Z0-9_]*)/g)) used.add(m[1]);
|
|
2791
|
+
for (const m of text.matchAll(/\bimport\.meta\.env\.([A-Z_][A-Z0-9_]*)/g)) used.add(m[1]);
|
|
2792
|
+
}
|
|
2793
|
+
const declared = /* @__PURE__ */ new Set();
|
|
2794
|
+
for (const envFile of [".env.example", ".env.sample", ".env.template"]) {
|
|
2795
|
+
try {
|
|
2796
|
+
const text = await readFile6(join8(abs, envFile), "utf8");
|
|
2797
|
+
for (const m of text.matchAll(/^\s*([A-Z_][A-Z0-9_]*)\s*=/gm)) declared.add(m[1]);
|
|
2798
|
+
} catch {
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
const projectVars = [...used].filter((v) => !SYSTEM_ENV.has(v));
|
|
2802
|
+
const undocumented = projectVars.filter((v) => !declared.has(v)).sort();
|
|
2803
|
+
return { used: projectVars.sort(), undocumented, hasTemplate: declared.size > 0 };
|
|
2804
|
+
}
|
|
2805
|
+
function unusedDeps(deps, imported) {
|
|
2806
|
+
return deps.filter((d) => ![...imported].some((i) => pkgRoot(i) === d));
|
|
2807
|
+
}
|
|
2808
|
+
async function configAudit(abs, pkg, _indexPaths, isGit) {
|
|
2809
|
+
let tsStrict = null;
|
|
2810
|
+
let gitignore = false;
|
|
2811
|
+
try {
|
|
2812
|
+
const raw = await readFile6(join8(abs, "tsconfig.json"), "utf8");
|
|
2813
|
+
const clean = raw.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "");
|
|
2814
|
+
tsStrict = JSON.parse(clean)?.compilerOptions?.strict === true;
|
|
2815
|
+
} catch {
|
|
2816
|
+
}
|
|
2817
|
+
try {
|
|
2818
|
+
await access(join8(abs, ".gitignore"));
|
|
2819
|
+
gitignore = true;
|
|
2820
|
+
} catch {
|
|
2821
|
+
}
|
|
2822
|
+
const pkgMissing = ["license", "repository", "engines"].filter((k) => !pkg[k]);
|
|
2823
|
+
return { tsStrict, gitignore, pkgMissing, isGit };
|
|
2824
|
+
}
|
|
2825
|
+
function functionHotspots(index) {
|
|
2826
|
+
const out = [];
|
|
2827
|
+
for (const f of Object.values(index.files)) {
|
|
2828
|
+
if (/test|spec|__tests__|\.d\.ts$/i.test(f.relPath)) continue;
|
|
2829
|
+
for (const c of f.chunks) {
|
|
2830
|
+
if ((c.kind === "function" || c.kind === "method" || c.kind === "class") && c.name)
|
|
2831
|
+
out.push({ file: f.relPath, name: c.name, lines: c.endLine - c.startLine + 1 });
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
return out.sort((a, b) => b.lines - a.lines).slice(0, 6);
|
|
2835
|
+
}
|
|
2836
|
+
async function brokenPkgEntries(abs, pkg) {
|
|
2837
|
+
const targets = [];
|
|
2838
|
+
if (typeof pkg.main === "string") targets.push(pkg.main);
|
|
2839
|
+
if (typeof pkg.bin === "string") targets.push(pkg.bin);
|
|
2840
|
+
else if (pkg.bin && typeof pkg.bin === "object") targets.push(...Object.values(pkg.bin).filter((v) => typeof v === "string"));
|
|
2841
|
+
const walkExports = (e) => {
|
|
2842
|
+
if (typeof e === "string" && e.startsWith(".")) targets.push(e);
|
|
2843
|
+
else if (e && typeof e === "object") Object.values(e).forEach(walkExports);
|
|
2844
|
+
};
|
|
2845
|
+
walkExports(pkg.exports);
|
|
2846
|
+
const broken = [];
|
|
2847
|
+
for (const t2 of [...new Set(targets)]) {
|
|
2848
|
+
try {
|
|
2849
|
+
await access(join8(abs, t2));
|
|
2850
|
+
} catch {
|
|
2851
|
+
broken.push(t2);
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
return broken;
|
|
2855
|
+
}
|
|
2856
|
+
function deepImports(fileTexts) {
|
|
2857
|
+
const out = [];
|
|
2858
|
+
const re = /from\s+['"]((?:\.\.\/){3,}[^'"]*)['"]/;
|
|
2859
|
+
for (const [file, text] of fileTexts) {
|
|
2860
|
+
if (/test|spec|__tests__/i.test(file)) continue;
|
|
2861
|
+
const lines = text.split("\n");
|
|
2862
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2863
|
+
const m = lines[i].match(re);
|
|
2864
|
+
if (m) out.push({ file, line: i + 1, sample: m[1] });
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
return out.slice(0, 8);
|
|
2868
|
+
}
|
|
2869
|
+
function codeShape(fileTexts) {
|
|
2870
|
+
let commentLines = 0, codeLines = 0;
|
|
2871
|
+
const deepNest = [];
|
|
2872
|
+
for (const [file, text] of fileTexts) {
|
|
2873
|
+
if (/test|spec|__tests__|\.d\.ts$/i.test(file)) continue;
|
|
2874
|
+
let maxDepth = 0, inBlock = false;
|
|
2875
|
+
for (const line of text.split("\n")) {
|
|
2876
|
+
const t2 = line.trim();
|
|
2877
|
+
if (!t2) continue;
|
|
2878
|
+
codeLines++;
|
|
2879
|
+
if (inBlock) {
|
|
2880
|
+
commentLines++;
|
|
2881
|
+
if (t2.includes("*/")) inBlock = false;
|
|
2882
|
+
continue;
|
|
2571
2883
|
}
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
try {
|
|
2576
|
-
return await llama.loadModel({ modelPath });
|
|
2577
|
-
} catch (err) {
|
|
2578
|
-
if (cpuOnly) throw err;
|
|
2579
|
-
console.error(`[local-llm] GPU load failed (${err instanceof Error ? err.message : err}) \u2014 falling back to CPU. Slower, but works. Set CODEBASE_LOCAL_GPU=off to skip GPU entirely.`);
|
|
2580
|
-
const cpuLlama = await getLlama({ gpu: false });
|
|
2581
|
-
return cpuLlama.loadModel({ modelPath });
|
|
2884
|
+
if (t2.startsWith("//") || t2.startsWith("*")) {
|
|
2885
|
+
commentLines++;
|
|
2886
|
+
continue;
|
|
2582
2887
|
}
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2888
|
+
if (t2.startsWith("/*")) {
|
|
2889
|
+
commentLines++;
|
|
2890
|
+
if (!t2.includes("*/")) inBlock = true;
|
|
2891
|
+
continue;
|
|
2892
|
+
}
|
|
2893
|
+
const indent = line.match(/^[\t ]*/)[0];
|
|
2894
|
+
const depth = indent.replace(/\t/g, " ").length / 4;
|
|
2895
|
+
if (depth > maxDepth) maxDepth = depth;
|
|
2896
|
+
}
|
|
2897
|
+
if (maxDepth >= 6) deepNest.push({ file, depth: Math.round(maxDepth) });
|
|
2587
2898
|
}
|
|
2588
|
-
return
|
|
2899
|
+
return { commentPct: codeLines ? Math.round(commentLines / codeLines * 100) : 0, deepNest: deepNest.sort((a, b) => b.depth - a.depth).slice(0, 5) };
|
|
2589
2900
|
}
|
|
2590
|
-
async function
|
|
2591
|
-
const model = await loadLocalModel();
|
|
2592
|
-
const { LlamaChatSession } = await importLlama();
|
|
2593
|
-
const context = await model.createContext({ contextSize: LOCAL_CONTEXT_SIZE });
|
|
2901
|
+
async function readmeAudit(abs) {
|
|
2594
2902
|
try {
|
|
2595
|
-
const
|
|
2596
|
-
|
|
2597
|
-
|
|
2903
|
+
const text = await readFile6(join8(abs, "README.md"), "utf8");
|
|
2904
|
+
return {
|
|
2905
|
+
install: /^#{1,3}.*(install|installation|getting started|démarrage)/im.test(text),
|
|
2906
|
+
usage: /^#{1,3}.*(usage|utilisation|quickstart|quick start)/im.test(text),
|
|
2907
|
+
codeBlocks: (text.match(/```/g) ?? []).length / 2,
|
|
2908
|
+
badges: (text.match(/!\[/g) ?? []).length
|
|
2909
|
+
};
|
|
2910
|
+
} catch {
|
|
2911
|
+
return null;
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
function commitQuality(subjects) {
|
|
2915
|
+
if (!subjects.length) return null;
|
|
2916
|
+
const CONV = /^(feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert|tweak|release|hotfix|init|merge|wip)(\(.+\))?!?:\s/i;
|
|
2917
|
+
const conv = subjects.filter((s) => CONV.test(s)).length;
|
|
2918
|
+
const avgLen = Math.round(subjects.reduce((s, x) => s + x.length, 0) / subjects.length);
|
|
2919
|
+
return { conventionalPct: Math.round(conv / subjects.length * 100), avgLen };
|
|
2920
|
+
}
|
|
2921
|
+
var NODE_BUILTINS = /* @__PURE__ */ new Set([
|
|
2922
|
+
"assert",
|
|
2923
|
+
"buffer",
|
|
2924
|
+
"child_process",
|
|
2925
|
+
"cluster",
|
|
2926
|
+
"console",
|
|
2927
|
+
"constants",
|
|
2928
|
+
"crypto",
|
|
2929
|
+
"dgram",
|
|
2930
|
+
"dns",
|
|
2931
|
+
"domain",
|
|
2932
|
+
"events",
|
|
2933
|
+
"fs",
|
|
2934
|
+
"http",
|
|
2935
|
+
"http2",
|
|
2936
|
+
"https",
|
|
2937
|
+
"inspector",
|
|
2938
|
+
"module",
|
|
2939
|
+
"net",
|
|
2940
|
+
"os",
|
|
2941
|
+
"path",
|
|
2942
|
+
"perf_hooks",
|
|
2943
|
+
"process",
|
|
2944
|
+
"punycode",
|
|
2945
|
+
"querystring",
|
|
2946
|
+
"readline",
|
|
2947
|
+
"repl",
|
|
2948
|
+
"stream",
|
|
2949
|
+
"string_decoder",
|
|
2950
|
+
"sys",
|
|
2951
|
+
"timers",
|
|
2952
|
+
"tls",
|
|
2953
|
+
"tty",
|
|
2954
|
+
"url",
|
|
2955
|
+
"util",
|
|
2956
|
+
"v8",
|
|
2957
|
+
"vm",
|
|
2958
|
+
"worker_threads",
|
|
2959
|
+
"zlib"
|
|
2960
|
+
]);
|
|
2961
|
+
function pkgRoot(spec) {
|
|
2962
|
+
const s = spec.startsWith("node:") ? spec.slice(5) : spec;
|
|
2963
|
+
if (s.startsWith("@")) return s.split("/").slice(0, 2).join("/");
|
|
2964
|
+
return s.split("/")[0];
|
|
2965
|
+
}
|
|
2966
|
+
function importedPackages(fileTexts) {
|
|
2967
|
+
const imported = /* @__PURE__ */ new Set();
|
|
2968
|
+
const IMPORT_RE2 = /(?:\bfrom\s+|\bimport\s*\(|\bimport\s+|\brequire\s*\()\s*['"]([^'"./][^'"]*)['"]/g;
|
|
2969
|
+
for (const text of fileTexts.values())
|
|
2970
|
+
for (const m of text.matchAll(IMPORT_RE2)) imported.add(m[1]);
|
|
2971
|
+
return imported;
|
|
2972
|
+
}
|
|
2973
|
+
async function missingDeps(abs, fileTexts, pkg, indexPaths) {
|
|
2974
|
+
const declared = new Set([pkg.name].filter(Boolean));
|
|
2975
|
+
const addDeps = (p) => ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"].forEach((k) => Object.keys(p[k] ?? {}).forEach((d) => declared.add(d)));
|
|
2976
|
+
addDeps(pkg);
|
|
2977
|
+
for (const p of indexPaths) {
|
|
2978
|
+
if (!/(^|\/)package\.json$/.test(p) || p === "package.json") continue;
|
|
2979
|
+
try {
|
|
2980
|
+
addDeps(JSON.parse(await readFile6(join8(abs, p), "utf8")));
|
|
2981
|
+
} catch {
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
const missing = /* @__PURE__ */ new Set();
|
|
2985
|
+
for (const spec of importedPackages(fileTexts)) {
|
|
2986
|
+
const root = pkgRoot(spec);
|
|
2987
|
+
if (!NODE_BUILTINS.has(root) && !declared.has(root)) missing.add(root);
|
|
2988
|
+
}
|
|
2989
|
+
return [...missing].sort();
|
|
2990
|
+
}
|
|
2991
|
+
async function lockfileDrift(abs, deps) {
|
|
2992
|
+
for (const lf of ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lock"]) {
|
|
2993
|
+
try {
|
|
2994
|
+
const text = await readFile6(join8(abs, lf), "utf8");
|
|
2995
|
+
return deps.filter((d) => !text.includes(d));
|
|
2996
|
+
} catch {
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
return [];
|
|
3000
|
+
}
|
|
3001
|
+
function functionComplexity(index) {
|
|
3002
|
+
const BRANCH = /\b(if|for|while|case|catch)\b|&&|\|\||\?/g;
|
|
3003
|
+
const out = [];
|
|
3004
|
+
for (const f of Object.values(index.files)) {
|
|
3005
|
+
if (/test|spec|__tests__|\.d\.ts$/i.test(f.relPath)) continue;
|
|
3006
|
+
for (const c of f.chunks) {
|
|
3007
|
+
if ((c.kind === "function" || c.kind === "method") && c.name)
|
|
3008
|
+
out.push({ file: f.relPath, name: c.name, score: (c.content.match(BRANCH) ?? []).length });
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
return out.sort((a, b) => b.score - a.score).slice(0, 6);
|
|
3012
|
+
}
|
|
3013
|
+
function duplicateNames(codeFiles) {
|
|
3014
|
+
const byName = /* @__PURE__ */ new Map();
|
|
3015
|
+
for (const f of codeFiles) {
|
|
3016
|
+
const b = basename3(f).toLowerCase();
|
|
3017
|
+
(byName.get(b) ?? byName.set(b, []).get(b)).push(f);
|
|
3018
|
+
}
|
|
3019
|
+
return [...byName.entries()].filter(([n, fs]) => fs.length > 1 && !/^(index|types?|constants?|config)\./.test(n)).map(([name, files]) => ({ name, files })).slice(0, 5);
|
|
3020
|
+
}
|
|
3021
|
+
function asyncWithoutAwait(index) {
|
|
3022
|
+
const out = [];
|
|
3023
|
+
for (const f of Object.values(index.files)) {
|
|
3024
|
+
if (/test|spec|__tests__|\.d\.ts$/i.test(f.relPath)) continue;
|
|
3025
|
+
for (const c of f.chunks) {
|
|
3026
|
+
if ((c.kind === "function" || c.kind === "method") && c.name && /\basync\b/.test(c.content.split("\n")[0]) && !/\bawait\b/.test(c.content))
|
|
3027
|
+
out.push({ file: f.relPath, name: c.name });
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
return out.slice(0, 6);
|
|
3031
|
+
}
|
|
3032
|
+
function graphDepth(edges, entryPoints) {
|
|
3033
|
+
const adj = /* @__PURE__ */ new Map();
|
|
3034
|
+
for (const e of edges) (adj.get(e.from) ?? adj.set(e.from, []).get(e.from)).push(e.to);
|
|
3035
|
+
let max = 0;
|
|
3036
|
+
const memo = /* @__PURE__ */ new Map();
|
|
3037
|
+
const dfs = (f, seen) => {
|
|
3038
|
+
if (memo.has(f)) return memo.get(f);
|
|
3039
|
+
if (seen.has(f)) return 0;
|
|
3040
|
+
seen.add(f);
|
|
3041
|
+
let d = 0;
|
|
3042
|
+
for (const t2 of adj.get(f) ?? []) d = Math.max(d, dfs(t2, seen) + 1);
|
|
3043
|
+
seen.delete(f);
|
|
3044
|
+
memo.set(f, d);
|
|
3045
|
+
return d;
|
|
3046
|
+
};
|
|
3047
|
+
for (const e of entryPoints) max = Math.max(max, dfs(e, /* @__PURE__ */ new Set()));
|
|
3048
|
+
return max;
|
|
3049
|
+
}
|
|
3050
|
+
function docCoverage(fileTexts) {
|
|
3051
|
+
let documented = 0, total = 0;
|
|
3052
|
+
const EXPORT_LINE = /^\s*export\s+(?:async\s+)?(?:function|class|const|let|interface|type|enum|default)\b/;
|
|
3053
|
+
for (const [file, text] of fileTexts) {
|
|
3054
|
+
if (/test|spec|__tests__|\.d\.ts$/i.test(file)) continue;
|
|
3055
|
+
const lines = text.split("\n");
|
|
3056
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3057
|
+
if (!EXPORT_LINE.test(lines[i])) continue;
|
|
3058
|
+
total++;
|
|
3059
|
+
let j = i - 1;
|
|
3060
|
+
while (j >= 0 && !lines[j].trim()) j--;
|
|
3061
|
+
if (j >= 0 && /^\s*(\/\/|\/\*|\*)/.test(lines[j])) documented++;
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
return { documented, total };
|
|
3065
|
+
}
|
|
3066
|
+
function detectInfra(indexPaths) {
|
|
3067
|
+
const found = [];
|
|
3068
|
+
const has = (p) => indexPaths.has(p) || [...indexPaths].some((f) => f.startsWith(p));
|
|
3069
|
+
if (has(".github/workflows")) found.push("CI (GitHub Actions)");
|
|
3070
|
+
if (has("dockerfile") || has("docker-compose.yml")) found.push("Docker");
|
|
3071
|
+
if (has("tsconfig.json")) found.push("TypeScript config");
|
|
3072
|
+
if (has("pnpm-lock.yaml") || has("package-lock.json") || has("yarn.lock")) found.push("lockfile");
|
|
3073
|
+
if (has("vitest.config") || has("jest.config")) found.push("test runner config");
|
|
3074
|
+
if (has(".env.example") || has(".env.sample")) found.push(".env template");
|
|
3075
|
+
if (has("dockerfile")) found.push("container");
|
|
3076
|
+
if (has("vercel.json") || has("netlify.toml")) found.push("deploy config");
|
|
3077
|
+
if (has("eslint.config") || has(".eslintrc")) found.push("linter config");
|
|
3078
|
+
if (has(".prettierrc") || has("prettier.config")) found.push("formatter config");
|
|
3079
|
+
return [...new Set(found)];
|
|
3080
|
+
}
|
|
3081
|
+
function recommendations(r, hasTests, smells, sec, git2, infra, riskFiles, extras, lang) {
|
|
3082
|
+
const en = lang === "en";
|
|
3083
|
+
const out = [];
|
|
3084
|
+
if (extras.missingDeps.length) out.push({
|
|
3085
|
+
severity: "Critique",
|
|
3086
|
+
text: en ? `${extras.missingDeps.length} package${extras.missingDeps.length > 1 ? "s" : ""} imported but absent from package.json: ${extras.missingDeps.map((d) => `\`${d}\``).join(", ")} \u2014 installs will break for everyone else.` : `${extras.missingDeps.length} package${extras.missingDeps.length > 1 ? "s" : ""} import\xE9${extras.missingDeps.length > 1 ? "s" : ""} mais absent${extras.missingDeps.length > 1 ? "s" : ""} de package.json : ${extras.missingDeps.map((d) => `\`${d}\``).join(", ")} \u2014 l\u2019install cassera chez les autres.`
|
|
3087
|
+
});
|
|
3088
|
+
if (extras.brokenEntries.length) out.push({
|
|
3089
|
+
severity: "Critique",
|
|
3090
|
+
text: en ? `package.json points to missing files: ${extras.brokenEntries.map((e) => `\`${e}\``).join(", ")} \u2014 the package is broken for consumers.` : `package.json pointe vers des fichiers absents : ${extras.brokenEntries.map((e) => `\`${e}\``).join(", ")} \u2014 le package est cass\xE9 pour les consommateurs.`
|
|
3091
|
+
});
|
|
3092
|
+
if (extras.sensitive.length) out.push({
|
|
3093
|
+
severity: "Critique",
|
|
3094
|
+
text: en ? `Sensitive file${extras.sensitive.length > 1 ? "s" : ""} in the repo \u2014 e.g. \`${extras.sensitive[0]}\`${git2?.sensitiveTracked.includes(extras.sensitive[0]) ? " (tracked by git \u2014 purge history + rotate secrets)" : ""}. Add to .gitignore.` : `Fichier${extras.sensitive.length > 1 ? "s" : ""} sensible${extras.sensitive.length > 1 ? "s" : ""} dans le d\xE9p\xF4t \u2014 ex. \`${extras.sensitive[0]}\`${git2?.sensitiveTracked.includes(extras.sensitive[0]) ? " (suivi par git \u2014 purger l\u2019historique + r\xE9voquer les secrets)" : ""}. Ajouter au .gitignore.`
|
|
3095
|
+
});
|
|
3096
|
+
if (sec.secret?.length) out.push({
|
|
3097
|
+
severity: "Critique",
|
|
3098
|
+
text: en ? `${sec.secret.length} potential hardcoded secret${sec.secret.length > 1 ? "s" : ""} \u2014 e.g. \`${sec.secret[0].file}:${sec.secret[0].line}\`. Move to env vars, rotate if ever committed.` : `${sec.secret.length} secret${sec.secret.length > 1 ? "s" : ""} potentiellement cod\xE9${sec.secret.length > 1 ? "s" : ""} en dur \u2014 ex. \`${sec.secret[0].file}:${sec.secret[0].line}\`. D\xE9placer en variables d'env, r\xE9voquer si d\xE9j\xE0 commit\xE9.`
|
|
3099
|
+
});
|
|
3100
|
+
if (sec.eval?.length || sec.exec?.length || sec.innerHTML?.length) {
|
|
3101
|
+
const f = [...sec.eval ?? [], ...sec.exec ?? [], ...sec.innerHTML ?? []][0];
|
|
3102
|
+
out.push({
|
|
3103
|
+
severity: "\xC9lev\xE9e",
|
|
3104
|
+
text: en ? `Dangerous sinks detected (eval/exec/innerHTML) \u2014 e.g. \`${f.file}:${f.line}\`. Audit each call site.` : `Sinks dangereux d\xE9tect\xE9s (eval/exec/innerHTML) \u2014 ex. \`${f.file}:${f.line}\`. Auditer chaque site d'appel.`
|
|
2598
3105
|
});
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
3106
|
+
}
|
|
3107
|
+
if (riskFiles.length) out.push({
|
|
3108
|
+
severity: "\xC9lev\xE9e",
|
|
3109
|
+
text: en ? `\`${riskFiles[0].file}\` changes constantly AND is complex (churn ${riskFiles[0].churn}, complexity ${riskFiles[0].score})${extras.untestedRisk.includes(riskFiles[0].file) ? " and has no dedicated test" : ""} \u2014 the classic defect magnet. Cover it with tests before touching it.` : `\`${riskFiles[0].file}\` change sans cesse ET est complexe (churn ${riskFiles[0].churn}, complexit\xE9 ${riskFiles[0].score})${extras.untestedRisk.includes(riskFiles[0].file) ? " et n\u2019a pas de test d\xE9di\xE9" : ""} \u2014 l'aimant \xE0 bugs classique. Couvrir de tests avant d'y toucher.`
|
|
3110
|
+
});
|
|
3111
|
+
if (extras.envUndoc.length) out.push({
|
|
3112
|
+
severity: "Moyenne",
|
|
3113
|
+
text: en ? `${extras.envUndoc.length} env var${extras.envUndoc.length > 1 ? "s" : ""} used but absent from .env.example (e.g. \`${extras.envUndoc[0]}\`) \u2014 document them or setup will break for the next dev.` : `${extras.envUndoc.length} variable${extras.envUndoc.length > 1 ? "s" : ""} d\u2019env utilis\xE9e${extras.envUndoc.length > 1 ? "s" : ""} mais absente${extras.envUndoc.length > 1 ? "s" : ""} de .env.example (ex. \`${extras.envUndoc[0]}\`) \u2014 les documenter sinon le setup cassera pour le prochain dev.`
|
|
3114
|
+
});
|
|
3115
|
+
if (extras.lockDrift.length) out.push({
|
|
3116
|
+
severity: "Moyenne",
|
|
3117
|
+
text: en ? `${extras.lockDrift.length} declared dep${extras.lockDrift.length > 1 ? "s" : ""} absent from the lockfile (${extras.lockDrift.map((d) => `\`${d}\``).join(", ")}) \u2014 run the package manager to resync.` : `${extras.lockDrift.length} d\xE9pendance${extras.lockDrift.length > 1 ? "s" : ""} d\xE9clar\xE9e${extras.lockDrift.length > 1 ? "s" : ""} absente${extras.lockDrift.length > 1 ? "s" : ""} du lockfile (${extras.lockDrift.map((d) => `\`${d}\``).join(", ")}) \u2014 relancer le package manager pour resynchroniser.`
|
|
3118
|
+
});
|
|
3119
|
+
if (extras.tsStrict === false) out.push({
|
|
3120
|
+
severity: "Moyenne",
|
|
3121
|
+
text: en ? "TypeScript `strict` is off \u2014 enable it progressively (`strict: true` or `strictNullChecks` first)." : "Le `strict` TypeScript est d\xE9sactiv\xE9 \u2014 l\u2019activer progressivement (`strict: true` ou `strictNullChecks` d\u2019abord)."
|
|
3122
|
+
});
|
|
3123
|
+
if (extras.deepRel > 3) out.push({
|
|
3124
|
+
severity: "Moyenne",
|
|
3125
|
+
text: en ? `${extras.deepRel} deep relative imports (\`../../..\` 3+ levels) \u2014 expose a public barrel or move the module closer.` : `${extras.deepRel} imports relatifs profonds (\`../../..\` 3+ niveaux) \u2014 exposer un barrel public ou rapprocher le module.`
|
|
3126
|
+
});
|
|
3127
|
+
if (extras.deepNest.length) out.push({
|
|
3128
|
+
severity: "Moyenne",
|
|
3129
|
+
text: en ? `Nesting \u22656 levels in ${extras.deepNest.map((d) => `\`${d}\``).join(", ")} \u2014 early returns / extraction will flatten it.` : `Imbrication \u22656 niveaux dans ${extras.deepNest.map((d) => `\`${d}\``).join(", ")} \u2014 early returns / extraction pour aplatir.`
|
|
3130
|
+
});
|
|
3131
|
+
if (extras.commitConv !== null && extras.commitConv < 50) out.push({
|
|
3132
|
+
severity: "Faible",
|
|
3133
|
+
text: en ? `Only ${extras.commitConv}% of commits are conventional \u2014 a shared format makes history machine-readable.` : `Seulement ${extras.commitConv}% des commits sont conventionnels \u2014 un format partag\xE9 rend l'historique lisible par machine.`
|
|
3134
|
+
});
|
|
3135
|
+
if (extras.deadDeps.length) out.push({
|
|
3136
|
+
severity: "Faible",
|
|
3137
|
+
text: en ? `${extras.deadDeps.length} declared dependenc${extras.deadDeps.length > 1 ? "ies are" : "y is"} never imported (e.g. \`${extras.deadDeps[0]}\`) \u2014 remove to shrink install + audit surface.` : `${extras.deadDeps.length} d\xE9pendance${extras.deadDeps.length > 1 ? "s" : ""} d\xE9clar\xE9e${extras.deadDeps.length > 1 ? "s" : ""} jamais import\xE9e${extras.deadDeps.length > 1 ? "s" : ""} (ex. \`${extras.deadDeps[0]}\`) \u2014 retirer pour r\xE9duire l\u2019install + la surface d\u2019audit.`
|
|
3138
|
+
});
|
|
3139
|
+
if (git2 && git2.fileAuthors.size) {
|
|
3140
|
+
const soloHubs = riskFiles.filter((f) => (git2.fileAuthors.get(f.file)?.size ?? 0) <= 1);
|
|
3141
|
+
const solo = [...git2.fileAuthors.entries()].filter(([, a]) => a.size === 1).length;
|
|
3142
|
+
if (soloHubs.length || git2.churn.size && solo / git2.fileAuthors.size > 0.7) out.push({
|
|
3143
|
+
severity: "Moyenne",
|
|
3144
|
+
text: en ? `Bus factor: ${solo} file${solo > 1 ? "s" : ""} touched by a single author${soloHubs.length ? `, including hot \`${soloHubs[0].file}\`` : ""} \u2014 spread knowledge via reviews/pairing.` : `Bus factor : ${solo} fichier${solo > 1 ? "s" : ""} touch\xE9${solo > 1 ? "s" : ""} par un seul auteur${soloHubs.length ? `, dont le chaud \`${soloHubs[0].file}\`` : ""} \u2014 diffuser la connaissance via reviews/pairing.`
|
|
2605
3145
|
});
|
|
2606
|
-
} finally {
|
|
2607
|
-
await context.dispose();
|
|
2608
3146
|
}
|
|
3147
|
+
if (!infra.some((i) => i.startsWith("CI"))) out.push({
|
|
3148
|
+
severity: "Moyenne",
|
|
3149
|
+
text: en ? "No CI pipeline detected \u2014 add one (tests + typecheck on every push)." : "Aucune CI d\xE9tect\xE9e \u2014 en ajouter une (tests + typecheck \xE0 chaque push)."
|
|
3150
|
+
});
|
|
3151
|
+
if (r.cycles.length) out.push({
|
|
3152
|
+
severity: "Critique",
|
|
3153
|
+
text: en ? `Break ${r.cycles.length} circular dependenc${r.cycles.length > 1 ? "ies" : "y"} \u2014 e.g. \`${r.cycles[0].path[0]}\` \u2194 \`${r.cycles[0].path[1] ?? r.cycles[0].path[0]}\`. Extract the shared contract into a leaf module.` : `Casser ${r.cycles.length} d\xE9pendance${r.cycles.length > 1 ? "s" : ""} circulaire${r.cycles.length > 1 ? "s" : ""} \u2014 ex. \`${r.cycles[0].path[0]}\` \u2194 \`${r.cycles[0].path[1] ?? r.cycles[0].path[0]}\`. Extraire le contrat partag\xE9 dans un module feuille.`
|
|
3154
|
+
});
|
|
3155
|
+
for (const h of r.hotspots.slice(0, 3)) out.push({
|
|
3156
|
+
severity: "\xC9lev\xE9e",
|
|
3157
|
+
text: en ? `Split \`${h.file}\` (complexity ${h.score}) \u2014 extract independent blocks into focused modules.` : `D\xE9couper \`${h.file}\` (complexit\xE9 ${h.score}) \u2014 extraire les blocs ind\xE9pendants dans des modules cibl\xE9s.`
|
|
3158
|
+
});
|
|
3159
|
+
if (r.duplicates.length) out.push({
|
|
3160
|
+
severity: "Moyenne",
|
|
3161
|
+
text: en ? `Factor ${r.duplicates.length} duplicated block${r.duplicates.length > 1 ? "s" : ""} \u2014 e.g. ${r.duplicates[0].files.map((f) => `\`${f}\``).join(" / ")} share ${r.duplicates[0].lines} identical lines.` : `Factoriser ${r.duplicates.length} bloc${r.duplicates.length > 1 ? "s" : ""} dupliqu\xE9${r.duplicates.length > 1 ? "s" : ""} \u2014 ex. ${r.duplicates[0].files.map((f) => `\`${f}\``).join(" / ")} partagent ${r.duplicates[0].lines} lignes identiques.`
|
|
3162
|
+
});
|
|
3163
|
+
if (r.unusedFiles.length) out.push({
|
|
3164
|
+
severity: "Moyenne",
|
|
3165
|
+
text: en ? `Review ${r.unusedFiles.length} unreferenced file${r.unusedFiles.length > 1 ? "s" : ""} \u2014 delete or wire them in (e.g. \`${r.unusedFiles[0]}\`).` : `V\xE9rifier ${r.unusedFiles.length} fichier${r.unusedFiles.length > 1 ? "s" : ""} non r\xE9f\xE9renc\xE9${r.unusedFiles.length > 1 ? "s" : ""} \u2014 supprimer ou brancher (ex. \`${r.unusedFiles[0]}\`).`
|
|
3166
|
+
});
|
|
3167
|
+
if (r.unusedExports.length > 5) out.push({
|
|
3168
|
+
severity: "Faible",
|
|
3169
|
+
text: en ? `Prune ${r.unusedExports.length} exports nobody imports \u2014 shrink the public surface.` : `Nettoyer ${r.unusedExports.length} exports que personne n'importe \u2014 r\xE9duire la surface publique.`
|
|
3170
|
+
});
|
|
3171
|
+
if (!hasTests) out.push({
|
|
3172
|
+
severity: "\xC9lev\xE9e",
|
|
3173
|
+
text: en ? "No test files detected \u2014 add a test suite before refactoring." : "Aucun fichier de test d\xE9tect\xE9 \u2014 ajouter une suite de tests avant de refactorer."
|
|
3174
|
+
});
|
|
3175
|
+
if (smells.console && smells.console.length > 5) out.push({
|
|
3176
|
+
severity: "Faible",
|
|
3177
|
+
text: en ? `${smells.console.length} console.* calls in production code \u2014 route through a logger (e.g. \`${smells.console[0].file}:${smells.console[0].line}\`).` : `${smells.console.length} appels console.* dans le code de prod \u2014 passer par un logger (ex. \`${smells.console[0].file}:${smells.console[0].line}\`).`
|
|
3178
|
+
});
|
|
3179
|
+
if (smells.todo && smells.todo.length > 5) out.push({
|
|
3180
|
+
severity: "Faible",
|
|
3181
|
+
text: en ? `${smells.todo.length} TODO/FIXME markers \u2014 triage into tracked issues.` : `${smells.todo.length} marqueurs TODO/FIXME \u2014 trier en tickets suivis.`
|
|
3182
|
+
});
|
|
3183
|
+
if (!out.length) out.push({
|
|
3184
|
+
severity: "Faible",
|
|
3185
|
+
text: en ? "Nothing structural to fix \u2014 keep the hygiene rules that got this score." : "Rien de structurel \xE0 corriger \u2014 garder les r\xE8gles d\u2019hygi\xE8ne qui ont produit ce score."
|
|
3186
|
+
});
|
|
3187
|
+
return out;
|
|
3188
|
+
}
|
|
3189
|
+
async function buildDeterministicReport(projectPath, lang = "fr") {
|
|
3190
|
+
const en = lang === "en";
|
|
3191
|
+
const [index, graph, health] = await Promise.all([
|
|
3192
|
+
getIndex(projectPath),
|
|
3193
|
+
collectImportGraph(projectPath),
|
|
3194
|
+
analyzeProject(projectPath)
|
|
3195
|
+
]);
|
|
3196
|
+
const name = basename3(health.projectPath);
|
|
3197
|
+
let pkg = {};
|
|
3198
|
+
try {
|
|
3199
|
+
pkg = JSON.parse(await readFile6(join8(health.projectPath, "package.json"), "utf8"));
|
|
3200
|
+
} catch {
|
|
3201
|
+
}
|
|
3202
|
+
const langCount = /* @__PURE__ */ new Map();
|
|
3203
|
+
for (const rel of Object.keys(index.files)) {
|
|
3204
|
+
const l = EXT_LANG[extname3(rel).toLowerCase()];
|
|
3205
|
+
if (!l) continue;
|
|
3206
|
+
langCount.set(l, (langCount.get(l) ?? 0) + 1);
|
|
3207
|
+
}
|
|
3208
|
+
const langs = [...langCount.entries()].sort((a, b) => b[1] - a[1]);
|
|
3209
|
+
const deps = Object.keys(pkg.dependencies ?? {});
|
|
3210
|
+
const devDeps = Object.keys(pkg.devDependencies ?? {});
|
|
3211
|
+
const scripts = Object.keys(pkg.scripts ?? {});
|
|
3212
|
+
const symbols = Object.values(index.files).reduce((s, f) => s + f.chunks.filter((c) => c.name).length, 0);
|
|
3213
|
+
const hubs = [...graph.inDegree.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10);
|
|
3214
|
+
const entryPoints = graph.codeFiles.filter((f) => looksLikeEntry(f, pkg)).slice(0, 10);
|
|
3215
|
+
const leaves = graph.codeFiles.filter((f) => !graph.edges.some((e) => e.from === f)).length;
|
|
3216
|
+
const testFiles = Object.keys(index.files).filter((f) => /test|spec|__tests__/i.test(f));
|
|
3217
|
+
const srcFiles = graph.codeFiles.filter((f) => !/test|spec|__tests__/i.test(f));
|
|
3218
|
+
const testRatio = srcFiles.length ? Math.round(testFiles.length / srcFiles.length * 100) : 0;
|
|
3219
|
+
const largest = graph.codeFiles.map((f) => ({ f, lines: graph.fileTexts.get(f).split("\n").length })).sort((a, b) => b.lines - a.lines).slice(0, 5);
|
|
3220
|
+
const indexPaths = new Set(Object.keys(index.files).map((f) => f.toLowerCase()));
|
|
3221
|
+
const docs = ["readme.md", "license", "license.md", "changelog.md", "contributing.md", "security.md", "agents.md"].filter((d) => indexPaths.has(d));
|
|
3222
|
+
const smells = scanCode(graph.fileTexts, SMELL_PATS);
|
|
3223
|
+
const sec = scanCode(graph.fileTexts, SEC_PATS, 5);
|
|
3224
|
+
const hasTests = testFiles.length > 0;
|
|
3225
|
+
const git2 = await gitActivity(health.projectPath);
|
|
3226
|
+
const infra = detectInfra(indexPaths);
|
|
3227
|
+
const env = await envAudit(health.projectPath, graph.fileTexts);
|
|
3228
|
+
const imported = importedPackages(graph.fileTexts);
|
|
3229
|
+
const deadDeps = unusedDeps(deps, imported);
|
|
3230
|
+
const missing = await missingDeps(health.projectPath, graph.fileTexts, pkg, indexPaths);
|
|
3231
|
+
const lockDrift = await lockfileDrift(health.projectPath, deps);
|
|
3232
|
+
const fnComplex = functionComplexity(index);
|
|
3233
|
+
const dupNames = duplicateNames(graph.codeFiles);
|
|
3234
|
+
const asyncNoAwait = asyncWithoutAwait(index);
|
|
3235
|
+
const maxDepth = graphDepth(graph.edges, entryPoints);
|
|
3236
|
+
const cfg = await configAudit(health.projectPath, pkg, indexPaths, !!git2);
|
|
3237
|
+
const longFns = functionHotspots(index);
|
|
3238
|
+
const brokenEntries = await brokenPkgEntries(health.projectPath, pkg);
|
|
3239
|
+
const deepRel = deepImports(graph.fileTexts);
|
|
3240
|
+
const shape = codeShape(graph.fileTexts);
|
|
3241
|
+
const readme = await readmeAudit(health.projectPath);
|
|
3242
|
+
const commitQ = git2 ? commitQuality(git2.subjects) : null;
|
|
3243
|
+
const typedFiles = graph.codeFiles.filter((f) => /\.(ts|tsx)$/.test(f)).length;
|
|
3244
|
+
const typedPct = graph.codeFiles.length ? Math.round(typedFiles / graph.codeFiles.length * 100) : 0;
|
|
3245
|
+
const staleHubs = git2 ? hubs.filter(([f]) => git2.fileLastCommit.has(f)).map(([f, n]) => ({ f, n, last: git2.fileLastCommit.get(f) })).sort((a, b) => a.last.localeCompare(b.last)).slice(0, 5) : [];
|
|
3246
|
+
const testBases = new Set(testFiles.map((f) => basename3(f).replace(/\.(test|spec)\.[^.]+$/i, "").toLowerCase()));
|
|
3247
|
+
const sensitive = [...git2?.sensitiveTracked ?? []];
|
|
3248
|
+
if (!git2) {
|
|
3249
|
+
for (const rel of [".env", ".env.local", ".env.production"]) {
|
|
3250
|
+
try {
|
|
3251
|
+
await access(join8(health.projectPath, rel));
|
|
3252
|
+
sensitive.push(rel);
|
|
3253
|
+
} catch {
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
const docCov = docCoverage(graph.fileTexts);
|
|
3258
|
+
const docPct = docCov.total ? Math.round(docCov.documented / docCov.total * 100) : 0;
|
|
3259
|
+
const complexityByFile = new Map(health.hotspots.map((h) => [h.file, h.score]));
|
|
3260
|
+
const riskFiles = git2 ? [...git2.churn.entries()].filter(([f]) => complexityByFile.has(f)).map(([file, churn]) => ({ file, churn, score: complexityByFile.get(file) })).sort((a, b) => b.churn * b.score - a.churn * a.score).slice(0, 5) : [];
|
|
3261
|
+
const untestedRisk = riskFiles.filter((r) => !testBases.has(basename3(r.file).replace(/\.[^.]+$/, "").toLowerCase()));
|
|
3262
|
+
const topChurn = git2 ? [...git2.churn.entries()].filter(([f]) => !/lock|\.min\.|dist\/|generated/i.test(f)).sort((a, b) => b[1] - a[1]).slice(0, 8) : [];
|
|
3263
|
+
const t2 = en ? {
|
|
3264
|
+
title: "Deterministic report",
|
|
3265
|
+
genBy: "generated by static analysis \u2014 no LLM, no network",
|
|
3266
|
+
summary: "Executive summary",
|
|
3267
|
+
stack: "Stack & structure",
|
|
3268
|
+
lang: "Languages",
|
|
3269
|
+
deps: "Runtime deps",
|
|
3270
|
+
devDeps: "Dev deps",
|
|
3271
|
+
scripts: "Scripts",
|
|
3272
|
+
arch: "Module graph",
|
|
3273
|
+
hubs: "Hub modules (most imported)",
|
|
3274
|
+
entries: "Entry points",
|
|
3275
|
+
leaves: "leaf modules",
|
|
3276
|
+
syms: "symbols extracted",
|
|
3277
|
+
constraints: "Product constraints",
|
|
3278
|
+
none: "None declared",
|
|
3279
|
+
debt: "Debt & smells",
|
|
3280
|
+
secu: "Security signals",
|
|
3281
|
+
secuNone: "No risky pattern detected in scanned code.",
|
|
3282
|
+
tests: "test/src file ratio",
|
|
3283
|
+
largest: "Largest files",
|
|
3284
|
+
docs: "Docs present",
|
|
3285
|
+
infra: "Infra detected",
|
|
3286
|
+
gitTitle: "Git activity & risk",
|
|
3287
|
+
gitCommits: "commits",
|
|
3288
|
+
gitAuthors: "authors",
|
|
3289
|
+
gitLast: "last commit",
|
|
3290
|
+
gitChurn: "Most churned files",
|
|
3291
|
+
gitRisk: "Risk hotspots (churn \xD7 complexity)",
|
|
3292
|
+
gitSolo: "single-author files",
|
|
3293
|
+
docCov: "docstring coverage",
|
|
3294
|
+
timeline: "Activity (commits/month)",
|
|
3295
|
+
longestFns: "Longest functions",
|
|
3296
|
+
untested: "Untested risk hotspots",
|
|
3297
|
+
envUsed: "Env vars used",
|
|
3298
|
+
envUndoc: "not documented in .env.example",
|
|
3299
|
+
deadDeps: "Dependencies never imported",
|
|
3300
|
+
sensitive: "Sensitive files present",
|
|
3301
|
+
cfg: "Config hygiene",
|
|
3302
|
+
cfgStrict: "tsconfig strict: off",
|
|
3303
|
+
cfgGitignore: "no .gitignore",
|
|
3304
|
+
cfgPkg: "package.json missing",
|
|
3305
|
+
pkgBroken: "broken package entries",
|
|
3306
|
+
typed: "typed files",
|
|
3307
|
+
comments: "comment density",
|
|
3308
|
+
deepImports: "Deep relative imports (3+ levels)",
|
|
3309
|
+
deepNest: "Deep nesting (6+ levels)",
|
|
3310
|
+
readmeTitle: "README audit",
|
|
3311
|
+
readmeInstall: "install section",
|
|
3312
|
+
readmeUsage: "usage section",
|
|
3313
|
+
readmeCode: "code blocks",
|
|
3314
|
+
readmeBadges: "badges",
|
|
3315
|
+
commitQ: "Commit messages",
|
|
3316
|
+
commitConv: "conventional",
|
|
3317
|
+
staleHubs: "Stable core (hubs untouched longest)",
|
|
3318
|
+
missingDeps: "imported but undeclared",
|
|
3319
|
+
lockDrift: "absent from lockfile",
|
|
3320
|
+
bigCommits: "Largest commits",
|
|
3321
|
+
fnComplex: "Most complex functions",
|
|
3322
|
+
dupNames: "Duplicate file names",
|
|
3323
|
+
asyncNoAwait: "async without await",
|
|
3324
|
+
graphDepth: "Max import chain depth",
|
|
3325
|
+
reco: "Recommendations",
|
|
3326
|
+
sev: "Severity",
|
|
3327
|
+
action: "Action",
|
|
3328
|
+
labels: {
|
|
3329
|
+
todo: "TODO/FIXME markers",
|
|
3330
|
+
console: "console.* calls",
|
|
3331
|
+
tsIgnore: "@ts-ignore/-expect-error",
|
|
3332
|
+
any: "`any` types",
|
|
3333
|
+
emptyCatch: "empty catch blocks",
|
|
3334
|
+
debugger: "debugger statements",
|
|
3335
|
+
secret: "hardcoded secrets (suspected)",
|
|
3336
|
+
eval: "eval / new Function",
|
|
3337
|
+
exec: "child_process / execSync",
|
|
3338
|
+
innerHTML: "innerHTML assignments",
|
|
3339
|
+
unsafeRegex: "dynamic RegExp"
|
|
3340
|
+
},
|
|
3341
|
+
verdict: (g) => ({ A: "Excellent health \u2014 clean structure.", B: "Good health \u2014 minor debt.", C: "Correct \u2014 visible debt to watch.", D: "Fragile \u2014 refactor before growing.", E: "Critical \u2014 structural debt blocking." })[g] ?? ""
|
|
3342
|
+
} : {
|
|
3343
|
+
title: "Rapport d\xE9terministe",
|
|
3344
|
+
genBy: "g\xE9n\xE9r\xE9 par analyse statique \u2014 aucun LLM, aucun r\xE9seau",
|
|
3345
|
+
summary: "R\xE9sum\xE9 ex\xE9cutif",
|
|
3346
|
+
stack: "Stack & structure",
|
|
3347
|
+
lang: "Langages",
|
|
3348
|
+
deps: "D\xE9pendances runtime",
|
|
3349
|
+
devDeps: "D\xE9pendances dev",
|
|
3350
|
+
scripts: "Scripts",
|
|
3351
|
+
arch: "Graphe de modules",
|
|
3352
|
+
hubs: "Modules hubs (les plus import\xE9s)",
|
|
3353
|
+
entries: "Points d\u2019entr\xE9e",
|
|
3354
|
+
leaves: "modules feuilles",
|
|
3355
|
+
syms: "symboles extraits",
|
|
3356
|
+
constraints: "Contraintes produit",
|
|
3357
|
+
none: "Aucune d\xE9clar\xE9e",
|
|
3358
|
+
debt: "Dette & smells",
|
|
3359
|
+
secu: "Signaux s\xE9curit\xE9",
|
|
3360
|
+
secuNone: "Aucun pattern risqu\xE9 d\xE9tect\xE9 dans le code scann\xE9.",
|
|
3361
|
+
tests: "ratio tests/src",
|
|
3362
|
+
largest: "Plus gros fichiers",
|
|
3363
|
+
docs: "Docs pr\xE9sentes",
|
|
3364
|
+
infra: "Infra d\xE9tect\xE9e",
|
|
3365
|
+
gitTitle: "Activit\xE9 Git & risque",
|
|
3366
|
+
gitCommits: "commits",
|
|
3367
|
+
gitAuthors: "auteurs",
|
|
3368
|
+
gitLast: "dernier commit",
|
|
3369
|
+
gitChurn: "Fichiers les plus modifi\xE9s",
|
|
3370
|
+
gitRisk: "Hotspots de risque (churn \xD7 complexit\xE9)",
|
|
3371
|
+
gitSolo: "fichiers mono-auteur",
|
|
3372
|
+
docCov: "couverture docstrings",
|
|
3373
|
+
timeline: "Activit\xE9 (commits/mois)",
|
|
3374
|
+
longestFns: "Fonctions les plus longues",
|
|
3375
|
+
untested: "Hotspots \xE0 risque non test\xE9s",
|
|
3376
|
+
envUsed: "Variables d\u2019env utilis\xE9es",
|
|
3377
|
+
envUndoc: "non document\xE9es dans .env.example",
|
|
3378
|
+
deadDeps: "D\xE9pendances jamais import\xE9es",
|
|
3379
|
+
sensitive: "Fichiers sensibles pr\xE9sents",
|
|
3380
|
+
cfg: "Hygi\xE8ne de config",
|
|
3381
|
+
cfgStrict: "tsconfig strict : off",
|
|
3382
|
+
cfgGitignore: "pas de .gitignore",
|
|
3383
|
+
cfgPkg: "package.json incomplet",
|
|
3384
|
+
pkgBroken: "entr\xE9es package cass\xE9es",
|
|
3385
|
+
typed: "fichiers typ\xE9s",
|
|
3386
|
+
comments: "densit\xE9 de commentaires",
|
|
3387
|
+
deepImports: "Imports relatifs profonds (3+ niveaux)",
|
|
3388
|
+
deepNest: "Imbrication profonde (6+ niveaux)",
|
|
3389
|
+
readmeTitle: "Audit README",
|
|
3390
|
+
readmeInstall: "section install",
|
|
3391
|
+
readmeUsage: "section usage",
|
|
3392
|
+
readmeCode: "blocs de code",
|
|
3393
|
+
readmeBadges: "badges",
|
|
3394
|
+
commitQ: "Messages de commit",
|
|
3395
|
+
commitConv: "conventionnels",
|
|
3396
|
+
staleHubs: "Noyau stable (hubs les plus anciens)",
|
|
3397
|
+
missingDeps: "import\xE9s mais non d\xE9clar\xE9s",
|
|
3398
|
+
lockDrift: "absentes du lockfile",
|
|
3399
|
+
bigCommits: "Plus gros commits",
|
|
3400
|
+
fnComplex: "Fonctions les plus complexes",
|
|
3401
|
+
dupNames: "Noms de fichiers dupliqu\xE9s",
|
|
3402
|
+
asyncNoAwait: "async sans await",
|
|
3403
|
+
graphDepth: "Profondeur max des cha\xEEnes d\u2019imports",
|
|
3404
|
+
reco: "Recommandations",
|
|
3405
|
+
sev: "S\xE9v\xE9rit\xE9",
|
|
3406
|
+
action: "Action",
|
|
3407
|
+
labels: {
|
|
3408
|
+
todo: "Marqueurs TODO/FIXME",
|
|
3409
|
+
console: "Appels console.*",
|
|
3410
|
+
tsIgnore: "@ts-ignore/-expect-error",
|
|
3411
|
+
any: "Types `any`",
|
|
3412
|
+
emptyCatch: "catch vides",
|
|
3413
|
+
debugger: "Instructions debugger",
|
|
3414
|
+
secret: "Secrets en dur (suspect\xE9s)",
|
|
3415
|
+
eval: "eval / new Function",
|
|
3416
|
+
exec: "child_process / execSync",
|
|
3417
|
+
innerHTML: "Affectations innerHTML",
|
|
3418
|
+
unsafeRegex: "RegExp dynamiques"
|
|
3419
|
+
},
|
|
3420
|
+
verdict: (g) => ({ A: "Excellente sant\xE9 \u2014 structure propre.", B: "Bonne sant\xE9 \u2014 dette mineure.", C: "Correct \u2014 dette visible \xE0 surveiller.", D: "Fragile \u2014 refactorer avant de grossir.", E: "Critique \u2014 dette structurelle bloquante." })[g] ?? ""
|
|
3421
|
+
};
|
|
3422
|
+
const out = [];
|
|
3423
|
+
out.push(`# \u{1F4CA} ${t2.title} \u2014 \`${name}\``);
|
|
3424
|
+
out.push(`_${t2.genBy}_`, "");
|
|
3425
|
+
out.push(`## 1. ${t2.summary}`);
|
|
3426
|
+
out.push(`**${bar(health.score)} ${health.score}/100 (${health.grade})** \u2014 ${t2.verdict(health.grade)}`);
|
|
3427
|
+
out.push("");
|
|
3428
|
+
out.push(`- ${health.analyzedFiles} ${en ? "code files" : "fichiers de code"} \xB7 ${symbols} ${t2.syms} \xB7 ${health.importEdges} ${en ? "local imports" : "imports locaux"} \xB7 ${leaves} ${t2.leaves}`);
|
|
3429
|
+
out.push(`- ${testFiles.length} ${en ? "test files" : "fichiers de test"} (${testRatio}% ${t2.tests}) \xB7 ${countHits(smells)} ${en ? "smell hits" : "smells d\xE9tect\xE9s"} \xB7 ${countHits(sec)} ${en ? "security signals" : "signaux s\xE9curit\xE9"}`);
|
|
3430
|
+
out.push(`- ${docCov.documented}/${docCov.total} ${en ? "exports documented" : "exports document\xE9s"} (${docPct}% ${t2.docCov})`);
|
|
3431
|
+
if (git2) out.push(`- ${git2.commits} ${t2.gitCommits} \xB7 ${git2.authors.size} ${en ? "author(s)" : "auteur(s)"} \xB7 ${t2.gitLast} : ${git2.lastDate}`);
|
|
3432
|
+
out.push("");
|
|
3433
|
+
out.push(`## 2. ${t2.stack}`);
|
|
3434
|
+
if (pkg.name) out.push(`- **${en ? "Package" : "Package"}** : \`${pkg.name}${pkg.version ? `@${pkg.version}` : ""}\``);
|
|
3435
|
+
if (langs.length) out.push(`- **${t2.lang}** : ${langs.map(([l, n]) => `${l} (${n})`).join(", ")}`);
|
|
3436
|
+
if (deps.length) out.push(`- **${t2.deps}** (${deps.length}) : ${deps.slice(0, 12).map((d) => `\`${d}\``).join(", ")}${deps.length > 12 ? " \u2026" : ""}`);
|
|
3437
|
+
if (devDeps.length) out.push(`- **${t2.devDeps}** (${devDeps.length}) : ${devDeps.slice(0, 8).map((d) => `\`${d}\``).join(", ")}${devDeps.length > 8 ? " \u2026" : ""}`);
|
|
3438
|
+
if (scripts.length) out.push(`- **${t2.scripts}** : ${scripts.map((s) => `\`${s}\``).join(", ")}`);
|
|
3439
|
+
if (docs.length) out.push(`- **${t2.docs}** : ${docs.map((d) => `\`${d}\``).join(", ")}`);
|
|
3440
|
+
if (infra.length) out.push(`- **${t2.infra}** : ${infra.map((i) => `\`${i}\``).join(", ")}`);
|
|
3441
|
+
if (env.used.length) out.push(`- **${t2.envUsed}** (${env.used.length}) : ${env.used.slice(0, 10).map((v) => `\`${v}\``).join(", ")}${env.used.length > 10 ? " \u2026" : ""}`);
|
|
3442
|
+
if (env.undocumented.length && env.hasTemplate) out.push(` - \u26A0\uFE0F ${env.undocumented.length} ${t2.envUndoc} : ${env.undocumented.slice(0, 8).map((v) => `\`${v}\``).join(", ")}`);
|
|
3443
|
+
if (deadDeps.length) out.push(`- **${t2.deadDeps}** : ${deadDeps.map((d) => `\`${d}\``).join(", ")}`);
|
|
3444
|
+
if (missing.length) out.push(`- \u26A0\uFE0F **${en ? "Deps" : "Deps"} ${t2.missingDeps}** : ${missing.map((d) => `\`${d}\``).join(", ")}`);
|
|
3445
|
+
if (lockDrift.length) out.push(`- \u26A0\uFE0F ${lockDrift.length} ${en ? "deps" : "deps"} ${t2.lockDrift} : ${lockDrift.map((d) => `\`${d}\``).join(", ")}`);
|
|
3446
|
+
const cfgNotes = [];
|
|
3447
|
+
if (cfg.tsStrict === false) cfgNotes.push(t2.cfgStrict);
|
|
3448
|
+
if (cfg.isGit && !cfg.gitignore) cfgNotes.push(t2.cfgGitignore);
|
|
3449
|
+
if (cfg.pkgMissing.length) cfgNotes.push(`${t2.cfgPkg} : ${cfg.pkgMissing.map((k) => `\`${k}\``).join(", ")}`);
|
|
3450
|
+
if (cfgNotes.length) out.push(`- **${t2.cfg}** : ${cfgNotes.join(" \xB7 ")}`);
|
|
3451
|
+
if (brokenEntries.length) out.push(`- \u26A0\uFE0F **${t2.pkgBroken}** : ${brokenEntries.map((e) => `\`${e}\``).join(", ")}`);
|
|
3452
|
+
out.push(`- ${typedPct}% ${t2.typed} (${typedFiles}/${graph.codeFiles.length}) \xB7 ${shape.commentPct}% ${t2.comments}`);
|
|
3453
|
+
if (readme) {
|
|
3454
|
+
const ok = (b) => b ? "\u2705" : "\u274C";
|
|
3455
|
+
out.push(`- **${t2.readmeTitle}** : ${t2.readmeInstall} ${ok(readme.install)} \xB7 ${t2.readmeUsage} ${ok(readme.usage)} \xB7 ${readme.codeBlocks} ${t2.readmeCode} \xB7 ${readme.badges} ${t2.readmeBadges}`);
|
|
3456
|
+
}
|
|
3457
|
+
out.push("");
|
|
3458
|
+
out.push(`## 3. ${t2.arch}`);
|
|
3459
|
+
if (hubs.length) {
|
|
3460
|
+
out.push(`**${t2.hubs}** :`, "");
|
|
3461
|
+
for (const [f, n] of hubs) out.push(`- \`${f}\` \u2190 ${n} ${en ? "importers" : "importeurs"}`);
|
|
3462
|
+
out.push("");
|
|
3463
|
+
}
|
|
3464
|
+
if (entryPoints.length) {
|
|
3465
|
+
out.push(`**${t2.entries}** : ${entryPoints.map((f) => `\`${f}\``).join(", ")}`, "");
|
|
3466
|
+
}
|
|
3467
|
+
if (largest.length) {
|
|
3468
|
+
out.push(`**${t2.largest}** : ${largest.map((x) => `\`${x.f}\` (${x.lines}l)`).join(", ")}`, "");
|
|
3469
|
+
}
|
|
3470
|
+
if (longFns.length) {
|
|
3471
|
+
out.push(`**${t2.longestFns}** :`, "");
|
|
3472
|
+
for (const f of longFns) out.push(`- \`${f.file}\` \u2192 \`${f.name}\` (${f.lines}l)`);
|
|
3473
|
+
out.push("");
|
|
3474
|
+
}
|
|
3475
|
+
if (deepRel.length) {
|
|
3476
|
+
out.push(`**${t2.deepImports}** \u2014 ${deepRel.length} :`, "");
|
|
3477
|
+
for (const d of deepRel.slice(0, 5)) out.push(`- \`${d.file}:${d.line}\` \u2192 \`${d.sample}\``);
|
|
3478
|
+
out.push("");
|
|
3479
|
+
}
|
|
3480
|
+
if (shape.deepNest.length) {
|
|
3481
|
+
out.push(`**${t2.deepNest}** : ${shape.deepNest.map((d) => `\`${d.file}\` (${d.depth})`).join(", ")}`, "");
|
|
3482
|
+
}
|
|
3483
|
+
if (maxDepth > 0) out.push(`- **${t2.graphDepth}** : ${maxDepth}`);
|
|
3484
|
+
if (fnComplex.length) {
|
|
3485
|
+
out.push(`**${t2.fnComplex}** :`, "");
|
|
3486
|
+
for (const f of fnComplex) out.push(`- \`${f.file}\` \u2192 \`${f.name}\` (${f.score} ${en ? "branches" : "branchements"})`);
|
|
3487
|
+
out.push("");
|
|
3488
|
+
}
|
|
3489
|
+
if (dupNames.length) {
|
|
3490
|
+
out.push(`**${t2.dupNames}** :`, "");
|
|
3491
|
+
for (const d of dupNames) out.push(`- \`${d.name}\` \u2192 ${d.files.map((f) => `\`${f}\``).join(", ")}`);
|
|
3492
|
+
out.push("");
|
|
3493
|
+
}
|
|
3494
|
+
if (git2) {
|
|
3495
|
+
out.push(`## 4. ${t2.gitTitle}`, "");
|
|
3496
|
+
const topAuthors = [...git2.authors.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([a, n]) => `${a} (${n})`).join(", ");
|
|
3497
|
+
const soloCount = [...git2.fileAuthors.values()].filter((a) => a.size === 1).length;
|
|
3498
|
+
out.push(`- ${git2.commits} ${t2.gitCommits} \xB7 **${t2.gitAuthors}** : ${topAuthors}`);
|
|
3499
|
+
out.push(`- ${soloCount} ${t2.gitSolo}`);
|
|
3500
|
+
if (commitQ) out.push(`- ${t2.commitQ} : ${commitQ.conventionalPct}% ${t2.commitConv} \xB7 ~${commitQ.avgLen} ${en ? "chars" : "car."}`);
|
|
3501
|
+
out.push("");
|
|
3502
|
+
out.push(`**${t2.gitChurn}** :`, "");
|
|
3503
|
+
for (const [f, c] of topChurn) {
|
|
3504
|
+
const n = git2.fileAuthors.get(f)?.size ?? 0;
|
|
3505
|
+
out.push(`- \`${f}\` \u2014 ${c} ${en ? "lines changed" : "lignes modifi\xE9es"} \xB7 ${n} ${en ? "author(s)" : "auteur(s)"}`);
|
|
3506
|
+
}
|
|
3507
|
+
out.push("");
|
|
3508
|
+
if (riskFiles.length) {
|
|
3509
|
+
out.push(`**${t2.gitRisk}** :`, "");
|
|
3510
|
+
for (const r of riskFiles) {
|
|
3511
|
+
const tested = testBases.has(basename3(r.file).replace(/\.[^.]+$/, "").toLowerCase());
|
|
3512
|
+
out.push(`- \`${r.file}\` \u2014 churn ${r.churn} \xD7 ${en ? "complexity" : "complexit\xE9"} ${r.score}${tested ? "" : en ? " \xB7 \u26A0\uFE0F no test" : " \xB7 \u26A0\uFE0F sans test"}`);
|
|
3513
|
+
}
|
|
3514
|
+
out.push("");
|
|
3515
|
+
if (untestedRisk.length) out.push(`_${t2.untested} : ${untestedRisk.map((r) => `\`${r.file}\``).join(", ")}_`, "");
|
|
3516
|
+
}
|
|
3517
|
+
if (git2.months.size > 1) {
|
|
3518
|
+
const months = [...git2.months.entries()].sort().slice(-12);
|
|
3519
|
+
const max = Math.max(...months.map(([, n]) => n));
|
|
3520
|
+
out.push(`**${t2.timeline}** :`, "");
|
|
3521
|
+
out.push("```");
|
|
3522
|
+
for (const [m, n] of months) out.push(`${m} ${"\u2587".repeat(Math.max(1, Math.round(n / max * 20)))} ${n}`);
|
|
3523
|
+
out.push("```", "");
|
|
3524
|
+
}
|
|
3525
|
+
if (staleHubs.length) {
|
|
3526
|
+
out.push(`**${t2.staleHubs}** :`, "");
|
|
3527
|
+
for (const s of staleHubs) out.push(`- \`${s.f}\` \u2190 ${s.n} ${en ? "importers" : "importeurs"} \xB7 ${en ? "last change" : "derni\xE8re modif"} ${s.last}`);
|
|
3528
|
+
out.push("");
|
|
3529
|
+
}
|
|
3530
|
+
if (git2.commitSizes.length) {
|
|
3531
|
+
const big = [...git2.commitSizes].sort((a, b) => b.lines - a.lines).slice(0, 3);
|
|
3532
|
+
out.push(`**${t2.bigCommits}** : ${big.map((c) => `${c.lines} ${en ? "lines" : "lignes"} / ${c.files} ${en ? "files" : "fichiers"}`).join(" \xB7 ")}`, "");
|
|
3533
|
+
}
|
|
3534
|
+
}
|
|
3535
|
+
out.push(`## 5. ${t2.debt}`);
|
|
3536
|
+
const smellKeys = Object.keys(smells);
|
|
3537
|
+
if (!smellKeys.length) out.push(en ? "_Nothing detected._" : "_Rien d\xE9tect\xE9._");
|
|
3538
|
+
for (const key of smellKeys) {
|
|
3539
|
+
const hits = smells[key];
|
|
3540
|
+
out.push(`- **${t2.labels[key] ?? key}** \u2014 ${hits.length}${en ? " hit" + (hits.length > 1 ? "s" : "") : ""}`);
|
|
3541
|
+
for (const h of hits.slice(0, 4)) out.push(` - \`${h.file}:${h.line}\` \u2014 ${h.sample}`);
|
|
3542
|
+
if (hits.length > 4) out.push(` - _\u2026${hits.length - 4} ${en ? "more" : "autres"}_`);
|
|
3543
|
+
}
|
|
3544
|
+
if (asyncNoAwait.length) {
|
|
3545
|
+
out.push(`- **${t2.asyncNoAwait}** \u2014 ${asyncNoAwait.length}`);
|
|
3546
|
+
for (const a of asyncNoAwait.slice(0, 5)) out.push(` - \`${a.file}\` \u2192 \`${a.name}\``);
|
|
3547
|
+
}
|
|
3548
|
+
out.push("");
|
|
3549
|
+
out.push(`## 6. ${t2.secu}`);
|
|
3550
|
+
if (sensitive.length) {
|
|
3551
|
+
out.push(`- **${t2.sensitive}** \u2014 ${sensitive.length}`);
|
|
3552
|
+
for (const f of sensitive.slice(0, 6)) out.push(` - \`${f}\`${git2?.sensitiveTracked.includes(f) ? en ? " (tracked by git!)" : " (suivi par git !)" : ""}`);
|
|
3553
|
+
}
|
|
3554
|
+
const secKeys = Object.keys(sec);
|
|
3555
|
+
if (!secKeys.length && !sensitive.length) out.push(`_${t2.secuNone}_`);
|
|
3556
|
+
for (const key of secKeys) {
|
|
3557
|
+
const hits = sec[key];
|
|
3558
|
+
out.push(`- **${t2.labels[key] ?? key}** \u2014 ${hits.length}`);
|
|
3559
|
+
for (const h of hits.slice(0, 5)) out.push(` - \`${h.file}:${h.line}\` \u2014 ${h.sample}`);
|
|
3560
|
+
if (hits.length > 5) out.push(` - _\u2026${hits.length - 5} ${en ? "more" : "autres"}_`);
|
|
3561
|
+
}
|
|
3562
|
+
out.push("");
|
|
3563
|
+
out.push(`## 7. ${t2.constraints}`);
|
|
3564
|
+
out.push(index.constraints.length ? index.constraints.map((c) => `- ${c}`).join("\n") : t2.none, "");
|
|
3565
|
+
out.push(formatHealthReportMd(health, lang).replace(/^## /, "## 8. ").replace(/\n### /g, "\n#### "), "");
|
|
3566
|
+
out.push(`## 9. ${t2.reco}`, "");
|
|
3567
|
+
out.push(`| ${t2.sev} | ${t2.action} |`, "|---|---|");
|
|
3568
|
+
const SEV_ICON = { Critique: "\u{1F534}", "\xC9lev\xE9e": "\u{1F7E0}", Moyenne: "\u{1F7E1}", Faible: "\u{1F535}" };
|
|
3569
|
+
for (const r of recommendations(health, hasTests, smells, sec, git2, infra, riskFiles, { sensitive, envUndoc: env.undocumented, deadDeps, tsStrict: cfg.tsStrict, untestedRisk: untestedRisk.map((r2) => r2.file), brokenEntries, deepRel: deepRel.length, deepNest: shape.deepNest.map((d) => d.file), commitConv: commitQ?.conventionalPct ?? null, missingDeps: missing, lockDrift }, lang)) out.push(`| ${SEV_ICON[r.severity]} ${r.severity} | ${r.text} |`);
|
|
3570
|
+
out.push("");
|
|
3571
|
+
out.push("---");
|
|
3572
|
+
out.push(`_${en ? "Made with passion by shinzarou-eng" : "Fait avec passion par shinzarou-eng"} \u2014 dsh-codebase-chat \xB7 ${en ? "deterministic mode" : "mode d\xE9terministe"}_`);
|
|
3573
|
+
return out.join("\n");
|
|
2609
3574
|
}
|
|
2610
3575
|
|
|
2611
3576
|
// src/cli.ts
|
|
@@ -2659,7 +3624,7 @@ Usage:
|
|
|
2659
3624
|
npx dsh-codebase-chat --project <path> --watch
|
|
2660
3625
|
npx dsh-codebase-chat --project <path> --prompt intelligence
|
|
2661
3626
|
npx dsh-codebase-chat --project <path> --prompt intelligence --call # answered via DEEPSEEK_API_KEY
|
|
2662
|
-
npx dsh-codebase-chat --project <path> --prompt intelligence --
|
|
3627
|
+
npx dsh-codebase-chat --project <path> --prompt intelligence --no-llm # deterministic report, zero model
|
|
2663
3628
|
|
|
2664
3629
|
Options:
|
|
2665
3630
|
-p, --project <path> Project directory (default: current directory)
|
|
@@ -2680,8 +3645,8 @@ Options:
|
|
|
2680
3645
|
--call With --prompt: send it to the API (needs DEEPSEEK_API_KEY
|
|
2681
3646
|
or OPENAI_API_KEY; DEEPSEEK_BASE_URL / CODEBASE_MODEL
|
|
2682
3647
|
customize endpoint/model) instead of printing it.
|
|
2683
|
-
--
|
|
2684
|
-
|
|
3648
|
+
--no-llm With --prompt: deterministic full report \u2014 pure static
|
|
3649
|
+
analysis, no model, no key, no network.
|
|
2685
3650
|
-w, --watch Keep the index hot \u2014 rebuild incrementally on file changes
|
|
2686
3651
|
-e, --embed Enable local semantic embeddings (slower, more relevant)
|
|
2687
3652
|
--lang <en|fr> Language for headings (default: .codebase-chat.json lang, else fr)
|
|
@@ -2711,7 +3676,7 @@ async function main() {
|
|
|
2711
3676
|
focus: { type: "string" },
|
|
2712
3677
|
style: { type: "string" },
|
|
2713
3678
|
call: { type: "boolean", default: false },
|
|
2714
|
-
|
|
3679
|
+
"no-llm": { type: "boolean", default: false },
|
|
2715
3680
|
watch: { type: "boolean", short: "w", default: false },
|
|
2716
3681
|
embed: { type: "boolean", short: "e", default: false },
|
|
2717
3682
|
lang: { type: "string" },
|
|
@@ -2809,7 +3774,11 @@ async function main() {
|
|
|
2809
3774
|
console.error(lang === "en" ? `--prompt ${mode} needs a query: add --ask/--search/--file (or --focus)` : `--prompt ${mode} n\xE9cessite une requ\xEAte : ajoute --ask/--search/--file (ou --focus)`);
|
|
2810
3775
|
exit(1);
|
|
2811
3776
|
}
|
|
2812
|
-
|
|
3777
|
+
if (values["no-llm"]) {
|
|
3778
|
+
const report = await buildDeterministicReport(values.project, lang);
|
|
3779
|
+
console.log(process.stdout.isTTY ? renderAnswerTerminal(report) : report);
|
|
3780
|
+
exit(0);
|
|
3781
|
+
}
|
|
2813
3782
|
const result = await buildContext({
|
|
2814
3783
|
project: values.project,
|
|
2815
3784
|
query,
|
|
@@ -2817,8 +3786,7 @@ async function main() {
|
|
|
2817
3786
|
filePath: values.file,
|
|
2818
3787
|
lang,
|
|
2819
3788
|
embed: values.embed,
|
|
2820
|
-
diff: values.diff
|
|
2821
|
-
maxTokens: useLocal ? 3500 : void 0
|
|
3789
|
+
diff: values.diff
|
|
2822
3790
|
});
|
|
2823
3791
|
let staticSection = "";
|
|
2824
3792
|
if ((/* @__PURE__ */ new Set(["intelligence", "report", "audit", "tasks", "ceo"])).has(mode)) {
|
|
@@ -2831,14 +3799,10 @@ ${formatHealthReport(report, lang)}`;
|
|
|
2831
3799
|
} catch {
|
|
2832
3800
|
}
|
|
2833
3801
|
}
|
|
2834
|
-
const projectName =
|
|
3802
|
+
const projectName = basename4(result.absProject);
|
|
2835
3803
|
let prompt;
|
|
2836
3804
|
try {
|
|
2837
|
-
prompt =
|
|
2838
|
-
|
|
2839
|
-
${lang === "en" ? `Answer based only on the codebase context above (${mode} mode). Structure your answer in short sections with bullet points, and cite each fact as [source: path:line].${query ? ` Question: ${query}` : ""}` : `R\xE9ponds en t'appuyant uniquement sur le contexte du codebase ci-dessus (mode ${mode}). Structure ta r\xE9ponse en sections courtes avec des puces, et cite chaque fait avec [source: fichier:ligne].${query ? ` Question : ${query}` : ""}`}
|
|
2840
|
-
|
|
2841
|
-
${lang === "en" ? "Answer" : "R\xE9ponse"} :` : buildToolPrompt(tool, {
|
|
3805
|
+
prompt = buildToolPrompt(tool, {
|
|
2842
3806
|
context: `${result.context}${staticSection}`,
|
|
2843
3807
|
projectName,
|
|
2844
3808
|
lang,
|
|
@@ -2852,12 +3816,6 @@ ${lang === "en" ? "Answer" : "R\xE9ponse"} :` : buildToolPrompt(tool, {
|
|
|
2852
3816
|
console.error(lang === "en" ? `unknown prompt mode "${mode}" \u2014 expected: intelligence, report, audit, tasks, ceo, player, chat, search, explain, refactor, crea` : `mode de prompt inconnu "${mode}" \u2014 attendu : intelligence, report, audit, tasks, ceo, player, chat, search, explain, refactor, crea`);
|
|
2853
3817
|
exit(1);
|
|
2854
3818
|
}
|
|
2855
|
-
if (values.local || isLocalLlmEnabled()) {
|
|
2856
|
-
console.error(lang === "en" ? "Answering with the embedded local model (first run downloads ~1 GB)..." : "R\xE9ponse via le mod\xE8le local embarqu\xE9 (premier lancement : ~1 Go de t\xE9l\xE9chargement)...");
|
|
2857
|
-
const answer = await callLocalLlm(prompt, lang);
|
|
2858
|
-
console.log(process.stdout.isTTY ? renderAnswerTerminal(answer) : answer);
|
|
2859
|
-
exit(0);
|
|
2860
|
-
}
|
|
2861
3819
|
if (values.call) {
|
|
2862
3820
|
const apiKey = process.env.DEEPSEEK_API_KEY || process.env.OPENAI_API_KEY || "";
|
|
2863
3821
|
if (!apiKey) {
|