karajan-code 2.16.0 → 2.17.1

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.
@@ -1,110 +1,12 @@
1
- // KJC-TSK-0416: pre-filtro determinístico de falsos positivos Sonar.
1
+ // @deprecated — moved to src/audit/issue-filter.js (v2.17.0). This shim
2
+ // re-exports the generalised filter so existing imports keep working. All
3
+ // Sonar-specific behaviour (default `tool="sonar"`, legacy
4
+ // `// karajan-sonar-ignore: <rule>` markers) is preserved by the new module.
2
5
  //
3
- // Antes de que los issues Sonar lleguen al coder como feedback (sonar-role)
4
- // o al auditor (audit), filtramos los que sabemos que son falsos positivos
5
- // por reglas conocidas. Sin esto el coder quemaba tokens intentando
6
- // "arreglar" cosas que no están rotas.
7
- //
8
- // Dos mecanismos complementarios:
9
- // 1. Rules estáticas: { rule, filePattern, reason } — match por reglaId
10
- // + patrón de path. Cargadas por defecto del catálogo de FALSE_POSITIVES
11
- // abajo + extensibles via config.sonar.false_positives.
12
- // 2. Inline ignore: `// karajan-sonar-ignore: <ruleId>` en la línea del
13
- // issue (o la anterior) → suprime ese hit exacto.
14
-
15
- import fs from "node:fs";
16
-
17
- // Catálogo built-in de patrones conocidos. Cada entrada:
18
- // { rule: "javascript:S2699", filePattern: RegExp, reason: "..." }
19
- // Ampliable via config.sonar.false_positives.
20
- export const DEFAULT_FALSE_POSITIVES = [
21
- {
22
- rule: "javascript:S2699",
23
- filePattern: /tests\/architecture\//,
24
- reason: "Tests structurales fallan vía expect(offenders, msg).toEqual([]) — Sonar no detecta el assert con mensaje custom",
25
- },
26
- ];
27
-
28
- /**
29
- * Comprueba si una línea del file contiene `// karajan-sonar-ignore: <ruleId>`.
30
- * Acepta la línea exacta del issue o la inmediatamente anterior.
31
- *
32
- * @param {string} filePath
33
- * @param {number} line - 1-indexed
34
- * @param {string} ruleId
35
- * @returns {boolean}
36
- */
37
- export function hasInlineIgnore(filePath, line, ruleId) {
38
- if (!filePath || !line || !ruleId) return false;
39
- if (!fs.existsSync(filePath)) return false;
40
- try {
41
- const lines = fs.readFileSync(filePath, "utf8").split("\n");
42
- const idx = line - 1;
43
- const candidates = [lines[idx], lines[idx - 1]].filter(Boolean);
44
- const re = new RegExp(`karajan-sonar-ignore\\s*:\\s*${escapeRegex(ruleId)}\\b`);
45
- return candidates.some((l) => re.test(l));
46
- } catch {
47
- return false;
48
- }
49
- }
50
-
51
- function escapeRegex(s) {
52
- return String(s).replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
53
- }
54
-
55
- /**
56
- * Filtra issues de Sonar separando los reales de los falsos positivos.
57
- *
58
- * @param {object[]} issues - array de Sonar issues (shape: { rule, component, line, message, severity, ... })
59
- * @param {object} [opts]
60
- * @param {object[]} [opts.extraRules] - rules adicionales del config
61
- * @param {string} [opts.projectRoot] - para resolver component → ruta absoluta
62
- * @returns {{ kept: object[], suppressed: object[] }}
63
- */
64
- export function filterFalsePositives(issues, opts = {}) {
65
- if (!Array.isArray(issues)) return { kept: [], suppressed: [] };
66
- const rules = [...DEFAULT_FALSE_POSITIVES, ...(opts.extraRules || [])].map((r) => ({
67
- ...r,
68
- filePattern: r.filePattern instanceof RegExp ? r.filePattern : new RegExp(String(r.filePattern || ".*")),
69
- }));
70
- const projectRoot = opts.projectRoot || process.cwd();
71
- const kept = [];
72
- const suppressed = [];
73
-
74
- for (const issue of issues) {
75
- const path = componentToPath(issue.component);
76
- let matched = null;
77
-
78
- // 1. Rules estáticas.
79
- for (const rule of rules) {
80
- if (issue.rule === rule.rule && rule.filePattern.test(path)) {
81
- matched = { rule: rule.rule, path, reason: rule.reason };
82
- break;
83
- }
84
- }
85
-
86
- // 2. Inline ignore (más caro — solo si la regla estática no matchea).
87
- if (!matched && issue.rule && issue.line && path) {
88
- const absPath = path.startsWith("/") ? path : `${projectRoot}/${path}`;
89
- if (hasInlineIgnore(absPath, issue.line, issue.rule)) {
90
- matched = { rule: issue.rule, path, reason: "inline `// karajan-sonar-ignore`" };
91
- }
92
- }
93
-
94
- if (matched) {
95
- suppressed.push({ ...issue, _suppressedBy: matched });
96
- } else {
97
- kept.push(issue);
98
- }
99
- }
100
- return { kept, suppressed };
101
- }
6
+ // New code should import from "../audit/issue-filter.js" directly.
102
7
 
103
- /**
104
- * Sonar reporta component como `<projectKey>:<relativePath>`. Extrae el path.
105
- */
106
- function componentToPath(component) {
107
- if (typeof component !== "string") return "";
108
- const colonIdx = component.indexOf(":");
109
- return colonIdx >= 0 ? component.slice(colonIdx + 1) : component;
110
- }
8
+ export {
9
+ filterFalsePositives,
10
+ hasInlineIgnore,
11
+ DEFAULT_FALSE_POSITIVES,
12
+ } from "../audit/issue-filter.js";
@@ -0,0 +1,136 @@
1
+ // Platform-aware install hints for external tools that `kj audit` /
2
+ // `kj webperf` use (semgrep, osv-scanner, lighthouse).
3
+ //
4
+ // `kj doctor` calls these to render hint strings ("Install with:
5
+ // pipx install semgrep") that match what's actually available on the
6
+ // user's machine, rather than a generic "see docs" message. The same
7
+ // metadata drives `kj install-tools` (HU2) — it decides which package
8
+ // manager to invoke per tool.
9
+ //
10
+ // We probe each package manager once with `which`/`where`-style checks
11
+ // and cache the result for the lifetime of the process.
12
+
13
+ import { execFile } from "node:child_process";
14
+ import { promisify } from "node:util";
15
+
16
+ const execFileAsync = promisify(execFile);
17
+
18
+ /**
19
+ * @typedef {"pipx"|"brew"|"go"|"npm"|"pip"|"apt"|"dnf"|"choco"|"scoop"|"docker"} PackageManager
20
+ */
21
+
22
+ /** @type {Record<PackageManager, boolean>|null} */
23
+ let cachedAvailability = null;
24
+
25
+ const PROBES = [
26
+ ["pipx", ["--version"]],
27
+ ["brew", ["--version"]],
28
+ ["go", ["version"]],
29
+ ["npm", ["--version"]],
30
+ ["pip", ["--version"]],
31
+ ["apt", ["--version"]],
32
+ ["dnf", ["--version"]],
33
+ ["choco", ["--version"]],
34
+ ["scoop", ["--version"]],
35
+ ["docker", ["--version"]],
36
+ ];
37
+
38
+ async function probeOne(bin, args) {
39
+ try {
40
+ await execFileAsync(bin, args, { timeout: 3000 });
41
+ return true;
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Detect which package managers are reachable on PATH. Cached on first
49
+ * call. Exported for tests; production callers go through getInstallHint.
50
+ */
51
+ export async function detectPackageManagers() {
52
+ if (cachedAvailability) return cachedAvailability;
53
+ const entries = await Promise.all(
54
+ PROBES.map(async ([bin, args]) => [bin, await probeOne(bin, args)]),
55
+ );
56
+ cachedAvailability = Object.fromEntries(entries);
57
+ return cachedAvailability;
58
+ }
59
+
60
+ /** Test helper — reset the cache between unit cases. */
61
+ export function resetPackageManagerCache() {
62
+ cachedAvailability = null;
63
+ }
64
+
65
+ /**
66
+ * Returns the install command for a tool, preferring the highest-priority
67
+ * package manager available on the current system.
68
+ *
69
+ * The order encodes our recommendation when multiple managers can install
70
+ * the same tool. We prefer isolated installs (pipx, brew) over global
71
+ * `pip install` so we don't pollute the user's site-packages.
72
+ *
73
+ * @param {"semgrep"|"osv-scanner"|"lighthouse"|"docker"} tool
74
+ * @param {Record<PackageManager, boolean>} [available] — pre-computed availability; if omitted, detects.
75
+ * @returns {Promise<{ command: string|null, manager: PackageManager|null, manualUrl: string }>}
76
+ */
77
+ export async function getInstallHint(tool, available = null) {
78
+ const avail = available || (await detectPackageManagers());
79
+ const candidates = INSTALL_CANDIDATES[tool] || [];
80
+ for (const { manager, command } of candidates) {
81
+ if (avail[manager]) return { command, manager, manualUrl: MANUAL_URLS[tool] };
82
+ }
83
+ // Nothing matched — fall back to the first candidate's command as a
84
+ // suggestion, so the user at least sees a working recipe.
85
+ const first = candidates[0];
86
+ return {
87
+ command: first ? first.command : null,
88
+ manager: null,
89
+ manualUrl: MANUAL_URLS[tool],
90
+ };
91
+ }
92
+
93
+ const INSTALL_CANDIDATES = {
94
+ semgrep: [
95
+ { manager: "pipx", command: "pipx install semgrep" },
96
+ { manager: "brew", command: "brew install semgrep" },
97
+ { manager: "pip", command: "pip install semgrep" },
98
+ ],
99
+ "osv-scanner": [
100
+ { manager: "go", command: "go install github.com/google/osv-scanner@latest" },
101
+ { manager: "brew", command: "brew install osv-scanner" },
102
+ ],
103
+ lighthouse: [
104
+ { manager: "npm", command: "npm install -g lighthouse" },
105
+ ],
106
+ docker: [
107
+ // Docker Desktop / Engine cannot be installed by a package manager
108
+ // reliably across platforms — always point users to the official docs.
109
+ // Listing brew anyway as a hint for macOS users.
110
+ { manager: "brew", command: "brew install --cask docker" },
111
+ ],
112
+ };
113
+
114
+ const MANUAL_URLS = {
115
+ semgrep: "https://semgrep.dev/docs/getting-started",
116
+ "osv-scanner": "https://google.github.io/osv-scanner/installation/",
117
+ lighthouse: "https://github.com/GoogleChrome/lighthouse#using-the-cli",
118
+ docker: "https://docs.docker.com/get-docker/",
119
+ };
120
+
121
+ /**
122
+ * Tools whose hint depends on the project stack. lighthouse is only
123
+ * relevant for frontend projects; suppressing the hint elsewhere keeps
124
+ * `kj doctor` output free of irrelevant noise.
125
+ *
126
+ * @param {string} tool
127
+ * @param {Object} stack — shape returned by detectProjectStack()
128
+ */
129
+ export function appliesToStack(tool, stack) {
130
+ if (tool === "lighthouse") {
131
+ if (!stack) return false;
132
+ return Boolean(stack.isFrontend || stack.isFullstack);
133
+ }
134
+ // semgrep, osv-scanner, docker, sonar — always relevant.
135
+ return true;
136
+ }