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/dist/index.js CHANGED
@@ -2786,80 +2786,956 @@ function buildToolPrompt(tool, opts) {
2786
2786
  return normalizeLabels(builder(opts), opts.lang || "fr");
2787
2787
  }
2788
2788
 
2789
- // src/local-llm.ts
2790
- import { existsSync as existsSync2 } from "fs";
2791
- import { join as join8 } from "path";
2792
- var DEFAULT_MODEL_URI = "hf:Qwen/Qwen2.5-1.5B-Instruct-GGUF:Q4_K_M";
2793
- var LOCAL_CONTEXT_SIZE = 8192;
2794
- var LOCAL_MAX_TOKENS = 1536;
2795
- function isLocalLlmEnabled() {
2796
- return !!(process.env.CODEBASE_LOCAL_LLM || "").trim();
2797
- }
2798
- function configuredModel() {
2799
- const v = (process.env.CODEBASE_LOCAL_LLM || "").trim();
2800
- if (!v || v === "1" || v.toLowerCase() === "true") return DEFAULT_MODEL_URI;
2801
- return v;
2802
- }
2803
- async function importLlama() {
2789
+ // src/report.ts
2790
+ import { access, readFile as readFile6 } from "fs/promises";
2791
+ import { basename as basename3, extname as extname3, join as join8 } from "path";
2792
+ import { execFile as execFile2 } from "child_process";
2793
+ import { promisify as promisify2 } from "util";
2794
+ var run2 = promisify2(execFile2);
2795
+ var EXT_LANG = {
2796
+ ".ts": "TypeScript",
2797
+ ".tsx": "TypeScript (React)",
2798
+ ".js": "JavaScript",
2799
+ ".jsx": "JavaScript (React)",
2800
+ ".mjs": "JavaScript (ESM)",
2801
+ ".cjs": "JavaScript (CJS)",
2802
+ ".py": "Python",
2803
+ ".rs": "Rust",
2804
+ ".go": "Go",
2805
+ ".java": "Java",
2806
+ ".kt": "Kotlin",
2807
+ ".rb": "Ruby",
2808
+ ".php": "PHP",
2809
+ ".c": "C",
2810
+ ".h": "C/C++",
2811
+ ".cpp": "C++",
2812
+ ".cs": "C#",
2813
+ ".swift": "Swift",
2814
+ ".vue": "Vue",
2815
+ ".svelte": "Svelte",
2816
+ ".dart": "Dart",
2817
+ ".lua": "Lua"
2818
+ };
2819
+ function bar(score) {
2820
+ const filled = Math.round(score / 10);
2821
+ return "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
2822
+ }
2823
+ var SMELL_PATS = [
2824
+ ["todo", /\b(?:TODO|FIXME|HACK|XXX|WIP)\b/i],
2825
+ ["console", /\bconsole\.(log|warn|error|debug|info)\s*\(/],
2826
+ ["tsIgnore", /@ts-(ignore|expect-error|nocheck)\b/],
2827
+ ["any", /:\s*any\b/],
2828
+ ["emptyCatch", /catch\s*\([^)]*\)\s*\{\s*\}/],
2829
+ ["debugger", /\bdebugger\s*;/],
2830
+ ["syncIo", /\b(readFileSync|writeFileSync|appendFileSync|readdirSync|mkdirSync|execSync)\s*\(/]
2831
+ ];
2832
+ var SEC_PATS = [
2833
+ ["secret", /(?:api[_-]?key|secret|passwd|password|token|private[_-]?key)\s*[:=]\s*['"`][A-Za-z0-9_\/+\-.]{8,}['"`]/i],
2834
+ ["eval", /\beval\s*\(|new\s+Function\s*\(/],
2835
+ ["exec", /\bexecSync\s*\(|child_process/],
2836
+ ["innerHTML", /\.innerHTML\s*=/],
2837
+ ["unsafeRegex", /new\s+RegExp\s*\([^'"`]/]
2838
+ ];
2839
+ function scanCode(fileTexts, pats, perFileCap = 3) {
2840
+ const out = {};
2841
+ for (const [file, text] of fileTexts) {
2842
+ if (/test|spec|__tests__|\.d\.ts$/i.test(file)) continue;
2843
+ const lines = text.split("\n");
2844
+ for (const [key, re] of pats) {
2845
+ let found = 0;
2846
+ for (let i = 0; i < lines.length && found < perFileCap; i++) {
2847
+ if (re.test(lines[i])) {
2848
+ (out[key] ??= []).push({ file, line: i + 1, sample: lines[i].trim().slice(0, 90) });
2849
+ found++;
2850
+ }
2851
+ }
2852
+ }
2853
+ }
2854
+ return out;
2855
+ }
2856
+ function countHits(scan) {
2857
+ return Object.values(scan).reduce((s, f) => s + f.length, 0);
2858
+ }
2859
+ var SENSITIVE_PATS = /(^|\/)\.env$|(^|\/)\.env\.(local|prod|production|dev|development)$|\.(pem|key|p12|pfx|keystore)$|id_rsa|id_ed25519|credentials\.json|service-account/i;
2860
+ async function gitActivity(abs) {
2804
2861
  try {
2805
- return await import("node-llama-cpp");
2862
+ const { stdout } = await run2("git", [
2863
+ "-C",
2864
+ abs,
2865
+ "log",
2866
+ "--numstat",
2867
+ "--format=@@@%an|%ad|%s",
2868
+ "--date=short",
2869
+ "-n",
2870
+ "400"
2871
+ ], { maxBuffer: 32 * 1024 * 1024 });
2872
+ 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: [] };
2873
+ let author = "";
2874
+ let date = "";
2875
+ let curFiles = 0, curLines = 0;
2876
+ const flush = () => {
2877
+ if (curFiles || curLines) stats.commitSizes.push({ files: curFiles, lines: curLines });
2878
+ curFiles = 0;
2879
+ curLines = 0;
2880
+ };
2881
+ for (const line of stdout.split("\n")) {
2882
+ if (line.startsWith("@@@")) {
2883
+ flush();
2884
+ stats.commits++;
2885
+ const [a, d, s] = line.slice(3).split("|");
2886
+ author = a;
2887
+ date = d;
2888
+ if (s) stats.subjects.push(s);
2889
+ if (!stats.lastDate) stats.lastDate = d;
2890
+ stats.authors.set(a, (stats.authors.get(a) ?? 0) + 1);
2891
+ const month = d.slice(0, 7);
2892
+ stats.months.set(month, (stats.months.get(month) ?? 0) + 1);
2893
+ continue;
2894
+ }
2895
+ const m = line.match(/^(\d+|-)\t(\d+|-)\t(.+)$/);
2896
+ if (!m || m[1] === "-") continue;
2897
+ const file = m[3].replace(/\\/g, "/");
2898
+ const delta = Number(m[1]) + Number(m[2]);
2899
+ curFiles++;
2900
+ curLines += delta;
2901
+ stats.churn.set(file, (stats.churn.get(file) ?? 0) + delta);
2902
+ if (!stats.fileLastCommit.has(file) && date) stats.fileLastCommit.set(file, date);
2903
+ if (author) (stats.fileAuthors.get(file) ?? stats.fileAuthors.set(file, /* @__PURE__ */ new Set()).get(file)).add(author);
2904
+ }
2905
+ flush();
2906
+ try {
2907
+ const { stdout: tracked } = await run2("git", ["-C", abs, "ls-files"], { maxBuffer: 8 * 1024 * 1024 });
2908
+ stats.sensitiveTracked = tracked.split("\n").map((l) => l.trim()).filter((f) => f && SENSITIVE_PATS.test(f));
2909
+ } catch {
2910
+ }
2911
+ return stats;
2806
2912
  } catch {
2807
- throw new Error("Local LLM needs the optional dependency: npm i node-llama-cpp");
2808
- }
2809
- }
2810
- var modelPromise = null;
2811
- function loadLocalModel() {
2812
- if (!modelPromise) {
2813
- modelPromise = (async () => {
2814
- const spec = configuredModel();
2815
- let modelPath = spec;
2816
- if (spec.startsWith("hf:")) {
2817
- const { createModelDownloader } = await importLlama();
2818
- const downloader = await createModelDownloader({
2819
- modelUri: spec,
2820
- dirPath: join8(getCacheDir(), "models")
2821
- });
2822
- modelPath = await downloader.download();
2823
- } else if (!existsSync2(spec)) {
2824
- throw new Error(`Local model not found: ${spec}`);
2913
+ return null;
2914
+ }
2915
+ }
2916
+ var SYSTEM_ENV = /* @__PURE__ */ new Set([
2917
+ "PATH",
2918
+ "PATHEXT",
2919
+ "HOME",
2920
+ "HOMEPATH",
2921
+ "USERPROFILE",
2922
+ "USERNAME",
2923
+ "USER",
2924
+ "APPDATA",
2925
+ "LOCALAPPDATA",
2926
+ "TEMP",
2927
+ "TMP",
2928
+ "TMPDIR",
2929
+ "OS",
2930
+ "COMSPEC",
2931
+ "SYSTEMROOT",
2932
+ "WINDIR",
2933
+ "PROGRAMFILES",
2934
+ "PROGRAMDATA",
2935
+ "NUMBER_OF_PROCESSORS",
2936
+ "PROCESSOR_ARCHITECTURE",
2937
+ "SHELL",
2938
+ "TERM",
2939
+ "PWD",
2940
+ "OLDPWD",
2941
+ "HOME",
2942
+ "LANG",
2943
+ "LC_ALL",
2944
+ "TZ",
2945
+ "NODE_ENV",
2946
+ "NODE_PATH",
2947
+ "npm_config_cache",
2948
+ "CI",
2949
+ "HOSTNAME"
2950
+ ]);
2951
+ async function envAudit(abs, fileTexts) {
2952
+ const used = /* @__PURE__ */ new Set();
2953
+ for (const [file, text] of fileTexts) {
2954
+ if (/test|spec|__tests__/i.test(file)) continue;
2955
+ for (const m of text.matchAll(/\bprocess\.env\.([A-Z_][A-Z0-9_]*)/g)) used.add(m[1]);
2956
+ for (const m of text.matchAll(/\bimport\.meta\.env\.([A-Z_][A-Z0-9_]*)/g)) used.add(m[1]);
2957
+ }
2958
+ const declared = /* @__PURE__ */ new Set();
2959
+ for (const envFile of [".env.example", ".env.sample", ".env.template"]) {
2960
+ try {
2961
+ const text = await readFile6(join8(abs, envFile), "utf8");
2962
+ for (const m of text.matchAll(/^\s*([A-Z_][A-Z0-9_]*)\s*=/gm)) declared.add(m[1]);
2963
+ } catch {
2964
+ }
2965
+ }
2966
+ const projectVars = [...used].filter((v) => !SYSTEM_ENV.has(v));
2967
+ const undocumented = projectVars.filter((v) => !declared.has(v)).sort();
2968
+ return { used: projectVars.sort(), undocumented, hasTemplate: declared.size > 0 };
2969
+ }
2970
+ function unusedDeps(deps, imported) {
2971
+ return deps.filter((d) => ![...imported].some((i) => pkgRoot(i) === d));
2972
+ }
2973
+ async function configAudit(abs, pkg, _indexPaths, isGit) {
2974
+ let tsStrict = null;
2975
+ let gitignore = false;
2976
+ try {
2977
+ const raw = await readFile6(join8(abs, "tsconfig.json"), "utf8");
2978
+ const clean = raw.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "");
2979
+ tsStrict = JSON.parse(clean)?.compilerOptions?.strict === true;
2980
+ } catch {
2981
+ }
2982
+ try {
2983
+ await access(join8(abs, ".gitignore"));
2984
+ gitignore = true;
2985
+ } catch {
2986
+ }
2987
+ const pkgMissing = ["license", "repository", "engines"].filter((k) => !pkg[k]);
2988
+ return { tsStrict, gitignore, pkgMissing, isGit };
2989
+ }
2990
+ function functionHotspots(index) {
2991
+ const out = [];
2992
+ for (const f of Object.values(index.files)) {
2993
+ if (/test|spec|__tests__|\.d\.ts$/i.test(f.relPath)) continue;
2994
+ for (const c of f.chunks) {
2995
+ if ((c.kind === "function" || c.kind === "method" || c.kind === "class") && c.name)
2996
+ out.push({ file: f.relPath, name: c.name, lines: c.endLine - c.startLine + 1 });
2997
+ }
2998
+ }
2999
+ return out.sort((a, b) => b.lines - a.lines).slice(0, 6);
3000
+ }
3001
+ async function brokenPkgEntries(abs, pkg) {
3002
+ const targets = [];
3003
+ if (typeof pkg.main === "string") targets.push(pkg.main);
3004
+ if (typeof pkg.bin === "string") targets.push(pkg.bin);
3005
+ else if (pkg.bin && typeof pkg.bin === "object") targets.push(...Object.values(pkg.bin).filter((v) => typeof v === "string"));
3006
+ const walkExports = (e) => {
3007
+ if (typeof e === "string" && e.startsWith(".")) targets.push(e);
3008
+ else if (e && typeof e === "object") Object.values(e).forEach(walkExports);
3009
+ };
3010
+ walkExports(pkg.exports);
3011
+ const broken = [];
3012
+ for (const t2 of [...new Set(targets)]) {
3013
+ try {
3014
+ await access(join8(abs, t2));
3015
+ } catch {
3016
+ broken.push(t2);
3017
+ }
3018
+ }
3019
+ return broken;
3020
+ }
3021
+ function deepImports(fileTexts) {
3022
+ const out = [];
3023
+ const re = /from\s+['"]((?:\.\.\/){3,}[^'"]*)['"]/;
3024
+ for (const [file, text] of fileTexts) {
3025
+ if (/test|spec|__tests__/i.test(file)) continue;
3026
+ const lines = text.split("\n");
3027
+ for (let i = 0; i < lines.length; i++) {
3028
+ const m = lines[i].match(re);
3029
+ if (m) out.push({ file, line: i + 1, sample: m[1] });
3030
+ }
3031
+ }
3032
+ return out.slice(0, 8);
3033
+ }
3034
+ function codeShape(fileTexts) {
3035
+ let commentLines = 0, codeLines = 0;
3036
+ const deepNest = [];
3037
+ for (const [file, text] of fileTexts) {
3038
+ if (/test|spec|__tests__|\.d\.ts$/i.test(file)) continue;
3039
+ let maxDepth = 0, inBlock = false;
3040
+ for (const line of text.split("\n")) {
3041
+ const t2 = line.trim();
3042
+ if (!t2) continue;
3043
+ codeLines++;
3044
+ if (inBlock) {
3045
+ commentLines++;
3046
+ if (t2.includes("*/")) inBlock = false;
3047
+ continue;
2825
3048
  }
2826
- const { getLlama } = await importLlama();
2827
- const cpuOnly = /^(0|off|false|cpu)$/i.test(process.env.CODEBASE_LOCAL_GPU || "");
2828
- const llama = await getLlama(cpuOnly ? { gpu: false } : {});
2829
- try {
2830
- return await llama.loadModel({ modelPath });
2831
- } catch (err) {
2832
- if (cpuOnly) throw err;
2833
- 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.`);
2834
- const cpuLlama = await getLlama({ gpu: false });
2835
- return cpuLlama.loadModel({ modelPath });
3049
+ if (t2.startsWith("//") || t2.startsWith("*")) {
3050
+ commentLines++;
3051
+ continue;
2836
3052
  }
2837
- })();
2838
- modelPromise.catch(() => {
2839
- modelPromise = null;
2840
- });
3053
+ if (t2.startsWith("/*")) {
3054
+ commentLines++;
3055
+ if (!t2.includes("*/")) inBlock = true;
3056
+ continue;
3057
+ }
3058
+ const indent = line.match(/^[\t ]*/)[0];
3059
+ const depth = indent.replace(/\t/g, " ").length / 4;
3060
+ if (depth > maxDepth) maxDepth = depth;
3061
+ }
3062
+ if (maxDepth >= 6) deepNest.push({ file, depth: Math.round(maxDepth) });
2841
3063
  }
2842
- return modelPromise;
3064
+ return { commentPct: codeLines ? Math.round(commentLines / codeLines * 100) : 0, deepNest: deepNest.sort((a, b) => b.depth - a.depth).slice(0, 5) };
2843
3065
  }
2844
- async function callLocalLlm(prompt, lang = "fr") {
2845
- const model = await loadLocalModel();
2846
- const { LlamaChatSession } = await importLlama();
2847
- const context = await model.createContext({ contextSize: LOCAL_CONTEXT_SIZE });
3066
+ async function readmeAudit(abs) {
2848
3067
  try {
2849
- const session = new LlamaChatSession({
2850
- contextSequence: context.getSequence(),
2851
- systemPrompt: lang === "en" ? "You are a senior codebase analyst. Be precise and cite files with [source: path:line]." : "Tu es un analyste codebase senior. Sois precis et cite les fichiers avec [source: chemin:ligne]."
3068
+ const text = await readFile6(join8(abs, "README.md"), "utf8");
3069
+ return {
3070
+ install: /^#{1,3}.*(install|installation|getting started|démarrage)/im.test(text),
3071
+ usage: /^#{1,3}.*(usage|utilisation|quickstart|quick start)/im.test(text),
3072
+ codeBlocks: (text.match(/```/g) ?? []).length / 2,
3073
+ badges: (text.match(/!\[/g) ?? []).length
3074
+ };
3075
+ } catch {
3076
+ return null;
3077
+ }
3078
+ }
3079
+ function commitQuality(subjects) {
3080
+ if (!subjects.length) return null;
3081
+ const CONV = /^(feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert|tweak|release|hotfix|init|merge|wip)(\(.+\))?!?:\s/i;
3082
+ const conv = subjects.filter((s) => CONV.test(s)).length;
3083
+ const avgLen = Math.round(subjects.reduce((s, x) => s + x.length, 0) / subjects.length);
3084
+ return { conventionalPct: Math.round(conv / subjects.length * 100), avgLen };
3085
+ }
3086
+ var NODE_BUILTINS = /* @__PURE__ */ new Set([
3087
+ "assert",
3088
+ "buffer",
3089
+ "child_process",
3090
+ "cluster",
3091
+ "console",
3092
+ "constants",
3093
+ "crypto",
3094
+ "dgram",
3095
+ "dns",
3096
+ "domain",
3097
+ "events",
3098
+ "fs",
3099
+ "http",
3100
+ "http2",
3101
+ "https",
3102
+ "inspector",
3103
+ "module",
3104
+ "net",
3105
+ "os",
3106
+ "path",
3107
+ "perf_hooks",
3108
+ "process",
3109
+ "punycode",
3110
+ "querystring",
3111
+ "readline",
3112
+ "repl",
3113
+ "stream",
3114
+ "string_decoder",
3115
+ "sys",
3116
+ "timers",
3117
+ "tls",
3118
+ "tty",
3119
+ "url",
3120
+ "util",
3121
+ "v8",
3122
+ "vm",
3123
+ "worker_threads",
3124
+ "zlib"
3125
+ ]);
3126
+ function pkgRoot(spec) {
3127
+ const s = spec.startsWith("node:") ? spec.slice(5) : spec;
3128
+ if (s.startsWith("@")) return s.split("/").slice(0, 2).join("/");
3129
+ return s.split("/")[0];
3130
+ }
3131
+ function importedPackages(fileTexts) {
3132
+ const imported = /* @__PURE__ */ new Set();
3133
+ const IMPORT_RE2 = /(?:\bfrom\s+|\bimport\s*\(|\bimport\s+|\brequire\s*\()\s*['"]([^'"./][^'"]*)['"]/g;
3134
+ for (const text of fileTexts.values())
3135
+ for (const m of text.matchAll(IMPORT_RE2)) imported.add(m[1]);
3136
+ return imported;
3137
+ }
3138
+ async function missingDeps(abs, fileTexts, pkg, indexPaths) {
3139
+ const declared = new Set([pkg.name].filter(Boolean));
3140
+ const addDeps = (p) => ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"].forEach((k) => Object.keys(p[k] ?? {}).forEach((d) => declared.add(d)));
3141
+ addDeps(pkg);
3142
+ for (const p of indexPaths) {
3143
+ if (!/(^|\/)package\.json$/.test(p) || p === "package.json") continue;
3144
+ try {
3145
+ addDeps(JSON.parse(await readFile6(join8(abs, p), "utf8")));
3146
+ } catch {
3147
+ }
3148
+ }
3149
+ const missing = /* @__PURE__ */ new Set();
3150
+ for (const spec of importedPackages(fileTexts)) {
3151
+ const root = pkgRoot(spec);
3152
+ if (!NODE_BUILTINS.has(root) && !declared.has(root)) missing.add(root);
3153
+ }
3154
+ return [...missing].sort();
3155
+ }
3156
+ async function lockfileDrift(abs, deps) {
3157
+ for (const lf of ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lock"]) {
3158
+ try {
3159
+ const text = await readFile6(join8(abs, lf), "utf8");
3160
+ return deps.filter((d) => !text.includes(d));
3161
+ } catch {
3162
+ }
3163
+ }
3164
+ return [];
3165
+ }
3166
+ function functionComplexity(index) {
3167
+ const BRANCH = /\b(if|for|while|case|catch)\b|&&|\|\||\?/g;
3168
+ const out = [];
3169
+ for (const f of Object.values(index.files)) {
3170
+ if (/test|spec|__tests__|\.d\.ts$/i.test(f.relPath)) continue;
3171
+ for (const c of f.chunks) {
3172
+ if ((c.kind === "function" || c.kind === "method") && c.name)
3173
+ out.push({ file: f.relPath, name: c.name, score: (c.content.match(BRANCH) ?? []).length });
3174
+ }
3175
+ }
3176
+ return out.sort((a, b) => b.score - a.score).slice(0, 6);
3177
+ }
3178
+ function duplicateNames(codeFiles) {
3179
+ const byName = /* @__PURE__ */ new Map();
3180
+ for (const f of codeFiles) {
3181
+ const b = basename3(f).toLowerCase();
3182
+ (byName.get(b) ?? byName.set(b, []).get(b)).push(f);
3183
+ }
3184
+ return [...byName.entries()].filter(([n, fs]) => fs.length > 1 && !/^(index|types?|constants?|config)\./.test(n)).map(([name, files]) => ({ name, files })).slice(0, 5);
3185
+ }
3186
+ function asyncWithoutAwait(index) {
3187
+ const out = [];
3188
+ for (const f of Object.values(index.files)) {
3189
+ if (/test|spec|__tests__|\.d\.ts$/i.test(f.relPath)) continue;
3190
+ for (const c of f.chunks) {
3191
+ if ((c.kind === "function" || c.kind === "method") && c.name && /\basync\b/.test(c.content.split("\n")[0]) && !/\bawait\b/.test(c.content))
3192
+ out.push({ file: f.relPath, name: c.name });
3193
+ }
3194
+ }
3195
+ return out.slice(0, 6);
3196
+ }
3197
+ function graphDepth(edges, entryPoints) {
3198
+ const adj = /* @__PURE__ */ new Map();
3199
+ for (const e of edges) (adj.get(e.from) ?? adj.set(e.from, []).get(e.from)).push(e.to);
3200
+ let max = 0;
3201
+ const memo = /* @__PURE__ */ new Map();
3202
+ const dfs = (f, seen) => {
3203
+ if (memo.has(f)) return memo.get(f);
3204
+ if (seen.has(f)) return 0;
3205
+ seen.add(f);
3206
+ let d = 0;
3207
+ for (const t2 of adj.get(f) ?? []) d = Math.max(d, dfs(t2, seen) + 1);
3208
+ seen.delete(f);
3209
+ memo.set(f, d);
3210
+ return d;
3211
+ };
3212
+ for (const e of entryPoints) max = Math.max(max, dfs(e, /* @__PURE__ */ new Set()));
3213
+ return max;
3214
+ }
3215
+ function docCoverage(fileTexts) {
3216
+ let documented = 0, total = 0;
3217
+ const EXPORT_LINE = /^\s*export\s+(?:async\s+)?(?:function|class|const|let|interface|type|enum|default)\b/;
3218
+ for (const [file, text] of fileTexts) {
3219
+ if (/test|spec|__tests__|\.d\.ts$/i.test(file)) continue;
3220
+ const lines = text.split("\n");
3221
+ for (let i = 0; i < lines.length; i++) {
3222
+ if (!EXPORT_LINE.test(lines[i])) continue;
3223
+ total++;
3224
+ let j = i - 1;
3225
+ while (j >= 0 && !lines[j].trim()) j--;
3226
+ if (j >= 0 && /^\s*(\/\/|\/\*|\*)/.test(lines[j])) documented++;
3227
+ }
3228
+ }
3229
+ return { documented, total };
3230
+ }
3231
+ function detectInfra(indexPaths) {
3232
+ const found = [];
3233
+ const has = (p) => indexPaths.has(p) || [...indexPaths].some((f) => f.startsWith(p));
3234
+ if (has(".github/workflows")) found.push("CI (GitHub Actions)");
3235
+ if (has("dockerfile") || has("docker-compose.yml")) found.push("Docker");
3236
+ if (has("tsconfig.json")) found.push("TypeScript config");
3237
+ if (has("pnpm-lock.yaml") || has("package-lock.json") || has("yarn.lock")) found.push("lockfile");
3238
+ if (has("vitest.config") || has("jest.config")) found.push("test runner config");
3239
+ if (has(".env.example") || has(".env.sample")) found.push(".env template");
3240
+ if (has("dockerfile")) found.push("container");
3241
+ if (has("vercel.json") || has("netlify.toml")) found.push("deploy config");
3242
+ if (has("eslint.config") || has(".eslintrc")) found.push("linter config");
3243
+ if (has(".prettierrc") || has("prettier.config")) found.push("formatter config");
3244
+ return [...new Set(found)];
3245
+ }
3246
+ function recommendations(r, hasTests, smells, sec, git2, infra, riskFiles, extras, lang) {
3247
+ const en = lang === "en";
3248
+ const out = [];
3249
+ if (extras.missingDeps.length) out.push({
3250
+ severity: "Critique",
3251
+ 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.`
3252
+ });
3253
+ if (extras.brokenEntries.length) out.push({
3254
+ severity: "Critique",
3255
+ 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.`
3256
+ });
3257
+ if (extras.sensitive.length) out.push({
3258
+ severity: "Critique",
3259
+ 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.`
3260
+ });
3261
+ if (sec.secret?.length) out.push({
3262
+ severity: "Critique",
3263
+ 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.`
3264
+ });
3265
+ if (sec.eval?.length || sec.exec?.length || sec.innerHTML?.length) {
3266
+ const f = [...sec.eval ?? [], ...sec.exec ?? [], ...sec.innerHTML ?? []][0];
3267
+ out.push({
3268
+ severity: "\xC9lev\xE9e",
3269
+ 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.`
2852
3270
  });
2853
- return await session.prompt(prompt, {
2854
- temperature: 0.2,
2855
- maxTokens: LOCAL_MAX_TOKENS,
2856
- // Small models degenerate into loops at low temperature penalize
2857
- // repeated tokens so the answer moves forward instead of echoing.
2858
- repeatPenalty: { lastTokens: 128, penalty: 1.2, penalizeNewLine: false }
3271
+ }
3272
+ if (riskFiles.length) out.push({
3273
+ severity: "\xC9lev\xE9e",
3274
+ 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.`
3275
+ });
3276
+ if (extras.envUndoc.length) out.push({
3277
+ severity: "Moyenne",
3278
+ 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.`
3279
+ });
3280
+ if (extras.lockDrift.length) out.push({
3281
+ severity: "Moyenne",
3282
+ 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.`
3283
+ });
3284
+ if (extras.tsStrict === false) out.push({
3285
+ severity: "Moyenne",
3286
+ 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)."
3287
+ });
3288
+ if (extras.deepRel > 3) out.push({
3289
+ severity: "Moyenne",
3290
+ 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.`
3291
+ });
3292
+ if (extras.deepNest.length) out.push({
3293
+ severity: "Moyenne",
3294
+ 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.`
3295
+ });
3296
+ if (extras.commitConv !== null && extras.commitConv < 50) out.push({
3297
+ severity: "Faible",
3298
+ 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.`
3299
+ });
3300
+ if (extras.deadDeps.length) out.push({
3301
+ severity: "Faible",
3302
+ 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.`
3303
+ });
3304
+ if (git2 && git2.fileAuthors.size) {
3305
+ const soloHubs = riskFiles.filter((f) => (git2.fileAuthors.get(f.file)?.size ?? 0) <= 1);
3306
+ const solo = [...git2.fileAuthors.entries()].filter(([, a]) => a.size === 1).length;
3307
+ if (soloHubs.length || git2.churn.size && solo / git2.fileAuthors.size > 0.7) out.push({
3308
+ severity: "Moyenne",
3309
+ 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.`
2859
3310
  });
2860
- } finally {
2861
- await context.dispose();
2862
3311
  }
3312
+ if (!infra.some((i) => i.startsWith("CI"))) out.push({
3313
+ severity: "Moyenne",
3314
+ 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)."
3315
+ });
3316
+ if (r.cycles.length) out.push({
3317
+ severity: "Critique",
3318
+ 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.`
3319
+ });
3320
+ for (const h of r.hotspots.slice(0, 3)) out.push({
3321
+ severity: "\xC9lev\xE9e",
3322
+ 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.`
3323
+ });
3324
+ if (r.duplicates.length) out.push({
3325
+ severity: "Moyenne",
3326
+ 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.`
3327
+ });
3328
+ if (r.unusedFiles.length) out.push({
3329
+ severity: "Moyenne",
3330
+ 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]}\`).`
3331
+ });
3332
+ if (r.unusedExports.length > 5) out.push({
3333
+ severity: "Faible",
3334
+ 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.`
3335
+ });
3336
+ if (!hasTests) out.push({
3337
+ severity: "\xC9lev\xE9e",
3338
+ 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."
3339
+ });
3340
+ if (smells.console && smells.console.length > 5) out.push({
3341
+ severity: "Faible",
3342
+ 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}\`).`
3343
+ });
3344
+ if (smells.todo && smells.todo.length > 5) out.push({
3345
+ severity: "Faible",
3346
+ text: en ? `${smells.todo.length} TODO/FIXME markers \u2014 triage into tracked issues.` : `${smells.todo.length} marqueurs TODO/FIXME \u2014 trier en tickets suivis.`
3347
+ });
3348
+ if (!out.length) out.push({
3349
+ severity: "Faible",
3350
+ 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."
3351
+ });
3352
+ return out;
3353
+ }
3354
+ async function buildDeterministicReport(projectPath, lang = "fr") {
3355
+ const en = lang === "en";
3356
+ const [index, graph, health] = await Promise.all([
3357
+ getIndex(projectPath),
3358
+ collectImportGraph(projectPath),
3359
+ analyzeProject(projectPath)
3360
+ ]);
3361
+ const name = basename3(health.projectPath);
3362
+ let pkg = {};
3363
+ try {
3364
+ pkg = JSON.parse(await readFile6(join8(health.projectPath, "package.json"), "utf8"));
3365
+ } catch {
3366
+ }
3367
+ const langCount = /* @__PURE__ */ new Map();
3368
+ for (const rel of Object.keys(index.files)) {
3369
+ const l = EXT_LANG[extname3(rel).toLowerCase()];
3370
+ if (!l) continue;
3371
+ langCount.set(l, (langCount.get(l) ?? 0) + 1);
3372
+ }
3373
+ const langs = [...langCount.entries()].sort((a, b) => b[1] - a[1]);
3374
+ const deps = Object.keys(pkg.dependencies ?? {});
3375
+ const devDeps = Object.keys(pkg.devDependencies ?? {});
3376
+ const scripts = Object.keys(pkg.scripts ?? {});
3377
+ const symbols = Object.values(index.files).reduce((s, f) => s + f.chunks.filter((c) => c.name).length, 0);
3378
+ const hubs = [...graph.inDegree.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10);
3379
+ const entryPoints = graph.codeFiles.filter((f) => looksLikeEntry(f, pkg)).slice(0, 10);
3380
+ const leaves = graph.codeFiles.filter((f) => !graph.edges.some((e) => e.from === f)).length;
3381
+ const testFiles = Object.keys(index.files).filter((f) => /test|spec|__tests__/i.test(f));
3382
+ const srcFiles = graph.codeFiles.filter((f) => !/test|spec|__tests__/i.test(f));
3383
+ const testRatio = srcFiles.length ? Math.round(testFiles.length / srcFiles.length * 100) : 0;
3384
+ 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);
3385
+ const indexPaths = new Set(Object.keys(index.files).map((f) => f.toLowerCase()));
3386
+ const docs = ["readme.md", "license", "license.md", "changelog.md", "contributing.md", "security.md", "agents.md"].filter((d) => indexPaths.has(d));
3387
+ const smells = scanCode(graph.fileTexts, SMELL_PATS);
3388
+ const sec = scanCode(graph.fileTexts, SEC_PATS, 5);
3389
+ const hasTests = testFiles.length > 0;
3390
+ const git2 = await gitActivity(health.projectPath);
3391
+ const infra = detectInfra(indexPaths);
3392
+ const env = await envAudit(health.projectPath, graph.fileTexts);
3393
+ const imported = importedPackages(graph.fileTexts);
3394
+ const deadDeps = unusedDeps(deps, imported);
3395
+ const missing = await missingDeps(health.projectPath, graph.fileTexts, pkg, indexPaths);
3396
+ const lockDrift = await lockfileDrift(health.projectPath, deps);
3397
+ const fnComplex = functionComplexity(index);
3398
+ const dupNames = duplicateNames(graph.codeFiles);
3399
+ const asyncNoAwait = asyncWithoutAwait(index);
3400
+ const maxDepth = graphDepth(graph.edges, entryPoints);
3401
+ const cfg = await configAudit(health.projectPath, pkg, indexPaths, !!git2);
3402
+ const longFns = functionHotspots(index);
3403
+ const brokenEntries = await brokenPkgEntries(health.projectPath, pkg);
3404
+ const deepRel = deepImports(graph.fileTexts);
3405
+ const shape = codeShape(graph.fileTexts);
3406
+ const readme = await readmeAudit(health.projectPath);
3407
+ const commitQ = git2 ? commitQuality(git2.subjects) : null;
3408
+ const typedFiles = graph.codeFiles.filter((f) => /\.(ts|tsx)$/.test(f)).length;
3409
+ const typedPct = graph.codeFiles.length ? Math.round(typedFiles / graph.codeFiles.length * 100) : 0;
3410
+ 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) : [];
3411
+ const testBases = new Set(testFiles.map((f) => basename3(f).replace(/\.(test|spec)\.[^.]+$/i, "").toLowerCase()));
3412
+ const sensitive = [...git2?.sensitiveTracked ?? []];
3413
+ if (!git2) {
3414
+ for (const rel of [".env", ".env.local", ".env.production"]) {
3415
+ try {
3416
+ await access(join8(health.projectPath, rel));
3417
+ sensitive.push(rel);
3418
+ } catch {
3419
+ }
3420
+ }
3421
+ }
3422
+ const docCov = docCoverage(graph.fileTexts);
3423
+ const docPct = docCov.total ? Math.round(docCov.documented / docCov.total * 100) : 0;
3424
+ const complexityByFile = new Map(health.hotspots.map((h) => [h.file, h.score]));
3425
+ 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) : [];
3426
+ const untestedRisk = riskFiles.filter((r) => !testBases.has(basename3(r.file).replace(/\.[^.]+$/, "").toLowerCase()));
3427
+ 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) : [];
3428
+ const t2 = en ? {
3429
+ title: "Deterministic report",
3430
+ genBy: "generated by static analysis \u2014 no LLM, no network",
3431
+ summary: "Executive summary",
3432
+ stack: "Stack & structure",
3433
+ lang: "Languages",
3434
+ deps: "Runtime deps",
3435
+ devDeps: "Dev deps",
3436
+ scripts: "Scripts",
3437
+ arch: "Module graph",
3438
+ hubs: "Hub modules (most imported)",
3439
+ entries: "Entry points",
3440
+ leaves: "leaf modules",
3441
+ syms: "symbols extracted",
3442
+ constraints: "Product constraints",
3443
+ none: "None declared",
3444
+ debt: "Debt & smells",
3445
+ secu: "Security signals",
3446
+ secuNone: "No risky pattern detected in scanned code.",
3447
+ tests: "test/src file ratio",
3448
+ largest: "Largest files",
3449
+ docs: "Docs present",
3450
+ infra: "Infra detected",
3451
+ gitTitle: "Git activity & risk",
3452
+ gitCommits: "commits",
3453
+ gitAuthors: "authors",
3454
+ gitLast: "last commit",
3455
+ gitChurn: "Most churned files",
3456
+ gitRisk: "Risk hotspots (churn \xD7 complexity)",
3457
+ gitSolo: "single-author files",
3458
+ docCov: "docstring coverage",
3459
+ timeline: "Activity (commits/month)",
3460
+ longestFns: "Longest functions",
3461
+ untested: "Untested risk hotspots",
3462
+ envUsed: "Env vars used",
3463
+ envUndoc: "not documented in .env.example",
3464
+ deadDeps: "Dependencies never imported",
3465
+ sensitive: "Sensitive files present",
3466
+ cfg: "Config hygiene",
3467
+ cfgStrict: "tsconfig strict: off",
3468
+ cfgGitignore: "no .gitignore",
3469
+ cfgPkg: "package.json missing",
3470
+ pkgBroken: "broken package entries",
3471
+ typed: "typed files",
3472
+ comments: "comment density",
3473
+ deepImports: "Deep relative imports (3+ levels)",
3474
+ deepNest: "Deep nesting (6+ levels)",
3475
+ readmeTitle: "README audit",
3476
+ readmeInstall: "install section",
3477
+ readmeUsage: "usage section",
3478
+ readmeCode: "code blocks",
3479
+ readmeBadges: "badges",
3480
+ commitQ: "Commit messages",
3481
+ commitConv: "conventional",
3482
+ staleHubs: "Stable core (hubs untouched longest)",
3483
+ missingDeps: "imported but undeclared",
3484
+ lockDrift: "absent from lockfile",
3485
+ bigCommits: "Largest commits",
3486
+ fnComplex: "Most complex functions",
3487
+ dupNames: "Duplicate file names",
3488
+ asyncNoAwait: "async without await",
3489
+ graphDepth: "Max import chain depth",
3490
+ reco: "Recommendations",
3491
+ sev: "Severity",
3492
+ action: "Action",
3493
+ labels: {
3494
+ todo: "TODO/FIXME markers",
3495
+ console: "console.* calls",
3496
+ tsIgnore: "@ts-ignore/-expect-error",
3497
+ any: "`any` types",
3498
+ emptyCatch: "empty catch blocks",
3499
+ debugger: "debugger statements",
3500
+ secret: "hardcoded secrets (suspected)",
3501
+ eval: "eval / new Function",
3502
+ exec: "child_process / execSync",
3503
+ innerHTML: "innerHTML assignments",
3504
+ unsafeRegex: "dynamic RegExp"
3505
+ },
3506
+ 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] ?? ""
3507
+ } : {
3508
+ title: "Rapport d\xE9terministe",
3509
+ genBy: "g\xE9n\xE9r\xE9 par analyse statique \u2014 aucun LLM, aucun r\xE9seau",
3510
+ summary: "R\xE9sum\xE9 ex\xE9cutif",
3511
+ stack: "Stack & structure",
3512
+ lang: "Langages",
3513
+ deps: "D\xE9pendances runtime",
3514
+ devDeps: "D\xE9pendances dev",
3515
+ scripts: "Scripts",
3516
+ arch: "Graphe de modules",
3517
+ hubs: "Modules hubs (les plus import\xE9s)",
3518
+ entries: "Points d\u2019entr\xE9e",
3519
+ leaves: "modules feuilles",
3520
+ syms: "symboles extraits",
3521
+ constraints: "Contraintes produit",
3522
+ none: "Aucune d\xE9clar\xE9e",
3523
+ debt: "Dette & smells",
3524
+ secu: "Signaux s\xE9curit\xE9",
3525
+ secuNone: "Aucun pattern risqu\xE9 d\xE9tect\xE9 dans le code scann\xE9.",
3526
+ tests: "ratio tests/src",
3527
+ largest: "Plus gros fichiers",
3528
+ docs: "Docs pr\xE9sentes",
3529
+ infra: "Infra d\xE9tect\xE9e",
3530
+ gitTitle: "Activit\xE9 Git & risque",
3531
+ gitCommits: "commits",
3532
+ gitAuthors: "auteurs",
3533
+ gitLast: "dernier commit",
3534
+ gitChurn: "Fichiers les plus modifi\xE9s",
3535
+ gitRisk: "Hotspots de risque (churn \xD7 complexit\xE9)",
3536
+ gitSolo: "fichiers mono-auteur",
3537
+ docCov: "couverture docstrings",
3538
+ timeline: "Activit\xE9 (commits/mois)",
3539
+ longestFns: "Fonctions les plus longues",
3540
+ untested: "Hotspots \xE0 risque non test\xE9s",
3541
+ envUsed: "Variables d\u2019env utilis\xE9es",
3542
+ envUndoc: "non document\xE9es dans .env.example",
3543
+ deadDeps: "D\xE9pendances jamais import\xE9es",
3544
+ sensitive: "Fichiers sensibles pr\xE9sents",
3545
+ cfg: "Hygi\xE8ne de config",
3546
+ cfgStrict: "tsconfig strict : off",
3547
+ cfgGitignore: "pas de .gitignore",
3548
+ cfgPkg: "package.json incomplet",
3549
+ pkgBroken: "entr\xE9es package cass\xE9es",
3550
+ typed: "fichiers typ\xE9s",
3551
+ comments: "densit\xE9 de commentaires",
3552
+ deepImports: "Imports relatifs profonds (3+ niveaux)",
3553
+ deepNest: "Imbrication profonde (6+ niveaux)",
3554
+ readmeTitle: "Audit README",
3555
+ readmeInstall: "section install",
3556
+ readmeUsage: "section usage",
3557
+ readmeCode: "blocs de code",
3558
+ readmeBadges: "badges",
3559
+ commitQ: "Messages de commit",
3560
+ commitConv: "conventionnels",
3561
+ staleHubs: "Noyau stable (hubs les plus anciens)",
3562
+ missingDeps: "import\xE9s mais non d\xE9clar\xE9s",
3563
+ lockDrift: "absentes du lockfile",
3564
+ bigCommits: "Plus gros commits",
3565
+ fnComplex: "Fonctions les plus complexes",
3566
+ dupNames: "Noms de fichiers dupliqu\xE9s",
3567
+ asyncNoAwait: "async sans await",
3568
+ graphDepth: "Profondeur max des cha\xEEnes d\u2019imports",
3569
+ reco: "Recommandations",
3570
+ sev: "S\xE9v\xE9rit\xE9",
3571
+ action: "Action",
3572
+ labels: {
3573
+ todo: "Marqueurs TODO/FIXME",
3574
+ console: "Appels console.*",
3575
+ tsIgnore: "@ts-ignore/-expect-error",
3576
+ any: "Types `any`",
3577
+ emptyCatch: "catch vides",
3578
+ debugger: "Instructions debugger",
3579
+ secret: "Secrets en dur (suspect\xE9s)",
3580
+ eval: "eval / new Function",
3581
+ exec: "child_process / execSync",
3582
+ innerHTML: "Affectations innerHTML",
3583
+ unsafeRegex: "RegExp dynamiques"
3584
+ },
3585
+ 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] ?? ""
3586
+ };
3587
+ const out = [];
3588
+ out.push(`# \u{1F4CA} ${t2.title} \u2014 \`${name}\``);
3589
+ out.push(`_${t2.genBy}_`, "");
3590
+ out.push(`## 1. ${t2.summary}`);
3591
+ out.push(`**${bar(health.score)} ${health.score}/100 (${health.grade})** \u2014 ${t2.verdict(health.grade)}`);
3592
+ out.push("");
3593
+ 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}`);
3594
+ 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"}`);
3595
+ out.push(`- ${docCov.documented}/${docCov.total} ${en ? "exports documented" : "exports document\xE9s"} (${docPct}% ${t2.docCov})`);
3596
+ if (git2) out.push(`- ${git2.commits} ${t2.gitCommits} \xB7 ${git2.authors.size} ${en ? "author(s)" : "auteur(s)"} \xB7 ${t2.gitLast} : ${git2.lastDate}`);
3597
+ out.push("");
3598
+ out.push(`## 2. ${t2.stack}`);
3599
+ if (pkg.name) out.push(`- **${en ? "Package" : "Package"}** : \`${pkg.name}${pkg.version ? `@${pkg.version}` : ""}\``);
3600
+ if (langs.length) out.push(`- **${t2.lang}** : ${langs.map(([l, n]) => `${l} (${n})`).join(", ")}`);
3601
+ if (deps.length) out.push(`- **${t2.deps}** (${deps.length}) : ${deps.slice(0, 12).map((d) => `\`${d}\``).join(", ")}${deps.length > 12 ? " \u2026" : ""}`);
3602
+ if (devDeps.length) out.push(`- **${t2.devDeps}** (${devDeps.length}) : ${devDeps.slice(0, 8).map((d) => `\`${d}\``).join(", ")}${devDeps.length > 8 ? " \u2026" : ""}`);
3603
+ if (scripts.length) out.push(`- **${t2.scripts}** : ${scripts.map((s) => `\`${s}\``).join(", ")}`);
3604
+ if (docs.length) out.push(`- **${t2.docs}** : ${docs.map((d) => `\`${d}\``).join(", ")}`);
3605
+ if (infra.length) out.push(`- **${t2.infra}** : ${infra.map((i) => `\`${i}\``).join(", ")}`);
3606
+ 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" : ""}`);
3607
+ if (env.undocumented.length && env.hasTemplate) out.push(` - \u26A0\uFE0F ${env.undocumented.length} ${t2.envUndoc} : ${env.undocumented.slice(0, 8).map((v) => `\`${v}\``).join(", ")}`);
3608
+ if (deadDeps.length) out.push(`- **${t2.deadDeps}** : ${deadDeps.map((d) => `\`${d}\``).join(", ")}`);
3609
+ if (missing.length) out.push(`- \u26A0\uFE0F **${en ? "Deps" : "Deps"} ${t2.missingDeps}** : ${missing.map((d) => `\`${d}\``).join(", ")}`);
3610
+ if (lockDrift.length) out.push(`- \u26A0\uFE0F ${lockDrift.length} ${en ? "deps" : "deps"} ${t2.lockDrift} : ${lockDrift.map((d) => `\`${d}\``).join(", ")}`);
3611
+ const cfgNotes = [];
3612
+ if (cfg.tsStrict === false) cfgNotes.push(t2.cfgStrict);
3613
+ if (cfg.isGit && !cfg.gitignore) cfgNotes.push(t2.cfgGitignore);
3614
+ if (cfg.pkgMissing.length) cfgNotes.push(`${t2.cfgPkg} : ${cfg.pkgMissing.map((k) => `\`${k}\``).join(", ")}`);
3615
+ if (cfgNotes.length) out.push(`- **${t2.cfg}** : ${cfgNotes.join(" \xB7 ")}`);
3616
+ if (brokenEntries.length) out.push(`- \u26A0\uFE0F **${t2.pkgBroken}** : ${brokenEntries.map((e) => `\`${e}\``).join(", ")}`);
3617
+ out.push(`- ${typedPct}% ${t2.typed} (${typedFiles}/${graph.codeFiles.length}) \xB7 ${shape.commentPct}% ${t2.comments}`);
3618
+ if (readme) {
3619
+ const ok = (b) => b ? "\u2705" : "\u274C";
3620
+ 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}`);
3621
+ }
3622
+ out.push("");
3623
+ out.push(`## 3. ${t2.arch}`);
3624
+ if (hubs.length) {
3625
+ out.push(`**${t2.hubs}** :`, "");
3626
+ for (const [f, n] of hubs) out.push(`- \`${f}\` \u2190 ${n} ${en ? "importers" : "importeurs"}`);
3627
+ out.push("");
3628
+ }
3629
+ if (entryPoints.length) {
3630
+ out.push(`**${t2.entries}** : ${entryPoints.map((f) => `\`${f}\``).join(", ")}`, "");
3631
+ }
3632
+ if (largest.length) {
3633
+ out.push(`**${t2.largest}** : ${largest.map((x) => `\`${x.f}\` (${x.lines}l)`).join(", ")}`, "");
3634
+ }
3635
+ if (longFns.length) {
3636
+ out.push(`**${t2.longestFns}** :`, "");
3637
+ for (const f of longFns) out.push(`- \`${f.file}\` \u2192 \`${f.name}\` (${f.lines}l)`);
3638
+ out.push("");
3639
+ }
3640
+ if (deepRel.length) {
3641
+ out.push(`**${t2.deepImports}** \u2014 ${deepRel.length} :`, "");
3642
+ for (const d of deepRel.slice(0, 5)) out.push(`- \`${d.file}:${d.line}\` \u2192 \`${d.sample}\``);
3643
+ out.push("");
3644
+ }
3645
+ if (shape.deepNest.length) {
3646
+ out.push(`**${t2.deepNest}** : ${shape.deepNest.map((d) => `\`${d.file}\` (${d.depth})`).join(", ")}`, "");
3647
+ }
3648
+ if (maxDepth > 0) out.push(`- **${t2.graphDepth}** : ${maxDepth}`);
3649
+ if (fnComplex.length) {
3650
+ out.push(`**${t2.fnComplex}** :`, "");
3651
+ for (const f of fnComplex) out.push(`- \`${f.file}\` \u2192 \`${f.name}\` (${f.score} ${en ? "branches" : "branchements"})`);
3652
+ out.push("");
3653
+ }
3654
+ if (dupNames.length) {
3655
+ out.push(`**${t2.dupNames}** :`, "");
3656
+ for (const d of dupNames) out.push(`- \`${d.name}\` \u2192 ${d.files.map((f) => `\`${f}\``).join(", ")}`);
3657
+ out.push("");
3658
+ }
3659
+ if (git2) {
3660
+ out.push(`## 4. ${t2.gitTitle}`, "");
3661
+ const topAuthors = [...git2.authors.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([a, n]) => `${a} (${n})`).join(", ");
3662
+ const soloCount = [...git2.fileAuthors.values()].filter((a) => a.size === 1).length;
3663
+ out.push(`- ${git2.commits} ${t2.gitCommits} \xB7 **${t2.gitAuthors}** : ${topAuthors}`);
3664
+ out.push(`- ${soloCount} ${t2.gitSolo}`);
3665
+ if (commitQ) out.push(`- ${t2.commitQ} : ${commitQ.conventionalPct}% ${t2.commitConv} \xB7 ~${commitQ.avgLen} ${en ? "chars" : "car."}`);
3666
+ out.push("");
3667
+ out.push(`**${t2.gitChurn}** :`, "");
3668
+ for (const [f, c] of topChurn) {
3669
+ const n = git2.fileAuthors.get(f)?.size ?? 0;
3670
+ out.push(`- \`${f}\` \u2014 ${c} ${en ? "lines changed" : "lignes modifi\xE9es"} \xB7 ${n} ${en ? "author(s)" : "auteur(s)"}`);
3671
+ }
3672
+ out.push("");
3673
+ if (riskFiles.length) {
3674
+ out.push(`**${t2.gitRisk}** :`, "");
3675
+ for (const r of riskFiles) {
3676
+ const tested = testBases.has(basename3(r.file).replace(/\.[^.]+$/, "").toLowerCase());
3677
+ 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"}`);
3678
+ }
3679
+ out.push("");
3680
+ if (untestedRisk.length) out.push(`_${t2.untested} : ${untestedRisk.map((r) => `\`${r.file}\``).join(", ")}_`, "");
3681
+ }
3682
+ if (git2.months.size > 1) {
3683
+ const months = [...git2.months.entries()].sort().slice(-12);
3684
+ const max = Math.max(...months.map(([, n]) => n));
3685
+ out.push(`**${t2.timeline}** :`, "");
3686
+ out.push("```");
3687
+ for (const [m, n] of months) out.push(`${m} ${"\u2587".repeat(Math.max(1, Math.round(n / max * 20)))} ${n}`);
3688
+ out.push("```", "");
3689
+ }
3690
+ if (staleHubs.length) {
3691
+ out.push(`**${t2.staleHubs}** :`, "");
3692
+ for (const s of staleHubs) out.push(`- \`${s.f}\` \u2190 ${s.n} ${en ? "importers" : "importeurs"} \xB7 ${en ? "last change" : "derni\xE8re modif"} ${s.last}`);
3693
+ out.push("");
3694
+ }
3695
+ if (git2.commitSizes.length) {
3696
+ const big = [...git2.commitSizes].sort((a, b) => b.lines - a.lines).slice(0, 3);
3697
+ out.push(`**${t2.bigCommits}** : ${big.map((c) => `${c.lines} ${en ? "lines" : "lignes"} / ${c.files} ${en ? "files" : "fichiers"}`).join(" \xB7 ")}`, "");
3698
+ }
3699
+ }
3700
+ out.push(`## 5. ${t2.debt}`);
3701
+ const smellKeys = Object.keys(smells);
3702
+ if (!smellKeys.length) out.push(en ? "_Nothing detected._" : "_Rien d\xE9tect\xE9._");
3703
+ for (const key of smellKeys) {
3704
+ const hits = smells[key];
3705
+ out.push(`- **${t2.labels[key] ?? key}** \u2014 ${hits.length}${en ? " hit" + (hits.length > 1 ? "s" : "") : ""}`);
3706
+ for (const h of hits.slice(0, 4)) out.push(` - \`${h.file}:${h.line}\` \u2014 ${h.sample}`);
3707
+ if (hits.length > 4) out.push(` - _\u2026${hits.length - 4} ${en ? "more" : "autres"}_`);
3708
+ }
3709
+ if (asyncNoAwait.length) {
3710
+ out.push(`- **${t2.asyncNoAwait}** \u2014 ${asyncNoAwait.length}`);
3711
+ for (const a of asyncNoAwait.slice(0, 5)) out.push(` - \`${a.file}\` \u2192 \`${a.name}\``);
3712
+ }
3713
+ out.push("");
3714
+ out.push(`## 6. ${t2.secu}`);
3715
+ if (sensitive.length) {
3716
+ out.push(`- **${t2.sensitive}** \u2014 ${sensitive.length}`);
3717
+ for (const f of sensitive.slice(0, 6)) out.push(` - \`${f}\`${git2?.sensitiveTracked.includes(f) ? en ? " (tracked by git!)" : " (suivi par git !)" : ""}`);
3718
+ }
3719
+ const secKeys = Object.keys(sec);
3720
+ if (!secKeys.length && !sensitive.length) out.push(`_${t2.secuNone}_`);
3721
+ for (const key of secKeys) {
3722
+ const hits = sec[key];
3723
+ out.push(`- **${t2.labels[key] ?? key}** \u2014 ${hits.length}`);
3724
+ for (const h of hits.slice(0, 5)) out.push(` - \`${h.file}:${h.line}\` \u2014 ${h.sample}`);
3725
+ if (hits.length > 5) out.push(` - _\u2026${hits.length - 5} ${en ? "more" : "autres"}_`);
3726
+ }
3727
+ out.push("");
3728
+ out.push(`## 7. ${t2.constraints}`);
3729
+ out.push(index.constraints.length ? index.constraints.map((c) => `- ${c}`).join("\n") : t2.none, "");
3730
+ out.push(formatHealthReportMd(health, lang).replace(/^## /, "## 8. ").replace(/\n### /g, "\n#### "), "");
3731
+ out.push(`## 9. ${t2.reco}`, "");
3732
+ out.push(`| ${t2.sev} | ${t2.action} |`, "|---|---|");
3733
+ const SEV_ICON = { Critique: "\u{1F534}", "\xC9lev\xE9e": "\u{1F7E0}", Moyenne: "\u{1F7E1}", Faible: "\u{1F535}" };
3734
+ 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} |`);
3735
+ out.push("");
3736
+ out.push("---");
3737
+ 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"}_`);
3738
+ return out.join("\n");
2863
3739
  }
2864
3740
  export {
2865
3741
  CONFIG_FILE,
@@ -2874,6 +3750,7 @@ export {
2874
3750
  buildChatPrompt,
2875
3751
  buildContext,
2876
3752
  buildCreaPrompt,
3753
+ buildDeterministicReport,
2877
3754
  buildExplainPrompt,
2878
3755
  buildGitPrompt,
2879
3756
  buildIndex,
@@ -2884,7 +3761,6 @@ export {
2884
3761
  buildSearchPrompt,
2885
3762
  buildTasksPrompt,
2886
3763
  buildToolPrompt,
2887
- callLocalLlm,
2888
3764
  chunkByTokens,
2889
3765
  clearConfigCache,
2890
3766
  cosineSimilarity,
@@ -2906,7 +3782,6 @@ export {
2906
3782
  getIndex,
2907
3783
  globToRegExp,
2908
3784
  initTreeSitter,
2909
- isLocalLlmEnabled,
2910
3785
  langInstruction,
2911
3786
  loadIndex,
2912
3787
  loadProjectConfig,