karajan-code 2.7.4 → 2.9.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 +1 -1
- package/package.json +24 -4
- package/scripts/diet-baseline.mjs +170 -0
- package/scripts/regen-arch-stats.sh +71 -0
- package/src/agents/availability.js +15 -9
- package/src/agents/claude-agent.js +143 -45
- package/src/agents/model-registry.js +1 -1
- package/src/audit/agent-readiness.js +240 -0
- package/src/audit/basal-cost.js +92 -9
- package/src/audit/deterministic-summary.js +200 -0
- package/src/audit/osv-findings.js +162 -0
- package/src/audit/semgrep-findings.js +117 -0
- package/src/audit/sonar-findings.js +81 -0
- package/src/audit/webperf-input.js +90 -0
- package/src/bootstrap.js +26 -1
- package/src/brain/decision-tracker.js +3 -1
- package/src/brain/decisor.js +1 -1
- package/src/canvas/parse-md.js +238 -0
- package/src/canvas/schema.js +101 -0
- package/src/canvas/to-flat.js +85 -0
- package/src/canvas/validate.js +163 -0
- package/src/checks/binaries.js +3 -3
- package/src/checks/ci.js +2 -2
- package/src/checks/config-files.js +6 -6
- package/src/checks/dir-setup.js +4 -0
- package/src/checks/mcp-health.js +6 -0
- package/src/checks/ports.js +91 -29
- package/src/checks/rtk.js +1 -1
- package/src/checks/skills.js +5 -1
- package/src/checks/sonar.js +2 -2
- package/src/checks/system.js +3 -3
- package/src/checks/tokens.js +1 -1
- package/src/checks/types.js +0 -12
- package/src/cli/_shared.js +30 -0
- package/src/cli/register-meta.js +218 -0
- package/src/cli/register-pipeline.js +212 -0
- package/src/cli/register-plan.js +152 -0
- package/src/cli/register-roles-skills.js +38 -0
- package/src/cli/register-sonar.js +28 -0
- package/src/cli.js +83 -468
- package/src/commands/agents.js +18 -6
- package/src/commands/architect.js +29 -2
- package/src/commands/audit.js +265 -30
- package/src/commands/board.js +160 -30
- package/src/commands/clean.js +83 -0
- package/src/commands/code.js +7 -2
- package/src/commands/discover.js +7 -2
- package/src/commands/doctor.js +1 -1
- package/src/commands/init.js +2 -2
- package/src/commands/plan/_shared.js +40 -0
- package/src/commands/plan/add-hu.js +21 -0
- package/src/commands/plan/delete.js +9 -0
- package/src/commands/plan/generate.js +284 -0
- package/src/commands/plan/index.js +14 -0
- package/src/commands/plan/list.js +21 -0
- package/src/commands/plan/ready.js +46 -0
- package/src/commands/plan/remove-hu.js +17 -0
- package/src/commands/plan/show.js +81 -0
- package/src/commands/plan/validate.js +20 -0
- package/src/commands/plan.js +14 -291
- package/src/commands/researcher.js +7 -2
- package/src/commands/resume.js +24 -1
- package/src/commands/review.js +7 -2
- package/src/commands/run.js +52 -1
- package/src/commands/status.js +1 -1
- package/src/commands/sync.js +75 -0
- package/src/commands/triage.js +7 -2
- package/src/commands/undo.js +1 -1
- package/src/config/defaults.js +213 -0
- package/src/config/loader.js +137 -0
- package/src/config/overrides.js +199 -0
- package/src/config/role-resolver.js +159 -0
- package/src/config/test-harness.js +78 -0
- package/src/config.js +30 -632
- package/src/git/automation.js +2 -2
- package/src/guards/output-guard.js +0 -2
- package/src/hu/acceptance-runner.js +52 -10
- package/src/hu/store.js +11 -0
- package/src/mcp/handlers/direct-handlers.js +2 -5
- package/src/mcp/handlers/management-handlers.js +48 -4
- package/src/mcp/handlers/run-handler.js +9 -8
- package/src/mcp/preflight.js +32 -13
- package/src/mcp/server-handlers.js +4 -3
- package/src/mcp/suggest-handler.js +3 -2
- package/src/mcp/tools.js +15 -0
- package/src/orchestrator/agent-fallback.js +1 -1
- package/src/orchestrator/ci-integration.js +11 -11
- package/src/orchestrator/config-init.js +25 -7
- package/src/orchestrator/direct-actions.js +7 -3
- package/src/orchestrator/drivers/error-recovery.js +287 -0
- package/src/orchestrator/drivers/init-context.js +222 -0
- package/src/orchestrator/drivers/iteration-loop.js +301 -0
- package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +48 -0
- package/src/orchestrator/drivers/iteration-phases/guards.js +84 -0
- package/src/orchestrator/drivers/iteration-phases/handle-approved.js +29 -0
- package/src/orchestrator/drivers/iteration-phases/quality-gates.js +76 -0
- package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +60 -0
- package/src/orchestrator/drivers/post-loop.js +370 -0
- package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +116 -0
- package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +47 -0
- package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +84 -0
- package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +268 -0
- package/src/orchestrator/drivers/pre-loop.js +431 -0
- package/src/orchestrator/drivers/run-hu-batch.js +367 -0
- package/src/orchestrator/feedback-queue.js +0 -1
- package/src/orchestrator/flow-control.js +4 -3
- package/src/orchestrator/flow-runner.js +121 -2077
- package/src/orchestrator/fs-leak-detector.js +156 -0
- package/src/orchestrator/hu-sub-pipeline.js +197 -9
- package/src/orchestrator/integrations.js +81 -0
- package/src/orchestrator/post-loop-stages.js +35 -8
- package/src/orchestrator/preflight-checks.js +20 -7
- package/src/orchestrator/repair/repair-detector.js +117 -0
- package/src/orchestrator/repair/repair-runner.js +142 -0
- package/src/orchestrator/reviewer-fallback.js +1 -1
- package/src/orchestrator/role-output-compressor.js +0 -1
- package/src/orchestrator/session-journal.js +4 -41
- package/src/orchestrator/solomon-escalation.js +3 -2
- package/src/orchestrator/solomon-rules.js +7 -3
- package/src/orchestrator/stages/architect-stage.js +12 -11
- package/src/orchestrator/stages/coder-stage.js +56 -7
- package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +127 -0
- package/src/orchestrator/stages/hu-reviewer-stage.js +6 -79
- package/src/orchestrator/stages/planner-stage.js +1 -1
- package/src/orchestrator/stages/research-stage.js +1 -1
- package/src/orchestrator/stages/reviewer-stage.js +7 -7
- package/src/orchestrator/stages/sonar-stage.js +94 -10
- package/src/orchestrator/stages/stage-classes.js +84 -0
- package/src/orchestrator/stages/stage-executor.js +1 -1
- package/src/orchestrator/stages/triage-stage.js +1 -1
- package/src/orchestrator/standby.js +3 -2
- package/src/orchestrator/verification-gate.js +20 -12
- package/src/plan/adr-generator.js +170 -0
- package/src/plan/adr-loader.js +60 -0
- package/src/plan/plan-executor.js +19 -1
- package/src/plan/plan-hu-ops.js +170 -1
- package/src/plan/plan-reviewer.js +199 -0
- package/src/plan/plan-schema.js +69 -1
- package/src/plan/plan-store.js +40 -17
- package/src/plan/spec-detector.js +61 -0
- package/src/plan/tests-synthesizer.js +116 -0
- package/src/planning-game/client.js +3 -28
- package/src/planning-game/decomposition.js +1 -1
- package/src/planning-game/pipeline-adapter.js +37 -6
- package/src/prompts/audit.js +206 -6
- package/src/prompts/coder.js +172 -1
- package/src/prompts/hu-reviewer.js +0 -1
- package/src/prompts/planner.js +81 -4
- package/src/prompts/rtk-snippet.js +1 -1
- package/src/review/diff-generator.js +35 -22
- package/src/review/snapshot-diff.js +1 -1
- package/src/roles/audit-role.js +125 -13
- package/src/roles/coder-role.js +25 -3
- package/src/roles/planner-role.js +42 -1
- package/src/roles/repairer-role.js +181 -0
- package/src/roles/tester-role.js +70 -4
- package/src/session/mutators.js +304 -0
- package/src/session/runtime-overrides.js +59 -0
- package/src/{session-store.js → session/store.js} +2 -2
- package/src/skills/semantic-detector.js +10 -5
- package/src/skills/skill-detector.js +1 -1
- package/src/sonar/api.js +1 -1
- package/src/sonar/cloud-scanner.js +0 -1
- package/src/sonar/credentials.js +14 -6
- package/src/sonar/discovery.js +123 -0
- package/src/sonar/manager.js +18 -0
- package/src/sync/detect-drift.js +113 -0
- package/src/tracker-bootstrap.js +44 -0
- package/src/utils/agent-detect.js +15 -11
- package/src/utils/board-prompt-bridge.js +109 -0
- package/src/utils/cli-progress.js +161 -0
- package/src/utils/cwd-confirm.js +39 -0
- package/src/utils/derive-project-name-from-cwd.js +119 -0
- package/src/utils/display/event-handlers.js +1 -1
- package/src/utils/display/header.js +1 -1
- package/src/utils/display/session-summary.js +3 -3
- package/src/utils/display/solomon.js +1 -1
- package/src/utils/garbage-collector.js +396 -0
- package/src/utils/git.js +16 -4
- package/src/utils/locale.js +1 -1
- package/src/utils/paths.js +21 -1
- package/src/utils/process.js +1 -1
- package/src/utils/prompt-project-name.js +35 -0
- package/src/utils/role-assigner.js +1 -1
- package/src/utils/run-context.js +75 -0
- package/src/utils/run-log.js +12 -0
- package/src/utils/task-file.js +1 -1
- package/src/utils/telemetry.js +13 -1
- package/src/webperf/devtools-detect.js +1 -1
- package/templates/coder-rules.md +13 -0
- package/templates/roles/coder.md +34 -0
- package/templates/roles/repairer.md +94 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kj audit --agent-readiness — score a repo for AI-agent readability.
|
|
3
|
+
*
|
|
4
|
+
* Static, deterministic, NO LLM call. Inspired by addyosmani/agentic-seo.
|
|
5
|
+
* Catches the failure modes that make repos invisible to Cursor /
|
|
6
|
+
* Cline / Aider / Claude Code:
|
|
7
|
+
* - no llms.txt → agents have to crawl the README
|
|
8
|
+
* - oversized doc pages → agents truncate or drop them
|
|
9
|
+
* - JS-only docs → agents can't render
|
|
10
|
+
* - broken heading hierarchy → agents misinfer structure
|
|
11
|
+
* - no robots allowlist → some agents skip silently
|
|
12
|
+
*
|
|
13
|
+
* Returns a 0-100 score + per-check detail + top remediation hints.
|
|
14
|
+
* Pure data transformation: filesystem reads in, analysis out.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
|
|
20
|
+
const PAGE_BYTE_LIMIT = 32 * 1024; // ≈ 8K tokens, the typical agent budget per fetch
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Catalogue of checks. Each check has:
|
|
24
|
+
* - id — stable identifier
|
|
25
|
+
* - weight — points contributed when ok
|
|
26
|
+
* - run — (rootDir) => { ok, hint, details }
|
|
27
|
+
*/
|
|
28
|
+
const CHECKS = [
|
|
29
|
+
{
|
|
30
|
+
id: "llms-txt-present",
|
|
31
|
+
weight: 25,
|
|
32
|
+
description: "llms.txt at repo root",
|
|
33
|
+
run(rootDir) {
|
|
34
|
+
const p = path.join(rootDir, "llms.txt");
|
|
35
|
+
const ok = fs.existsSync(p) && fs.statSync(p).isFile();
|
|
36
|
+
return ok
|
|
37
|
+
? { ok: true, hint: null, details: { path: p } }
|
|
38
|
+
: { ok: false, hint: "Add llms.txt at repo root with a single-fetch index of docs + commands.", details: {} };
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: "llms-txt-valid",
|
|
43
|
+
weight: 10,
|
|
44
|
+
description: "llms.txt has at least one section heading and one link",
|
|
45
|
+
run(rootDir) {
|
|
46
|
+
const p = path.join(rootDir, "llms.txt");
|
|
47
|
+
if (!fs.existsSync(p)) return { ok: false, hint: "llms.txt missing — see llms-txt-present.", details: {} };
|
|
48
|
+
const text = safeRead(p);
|
|
49
|
+
const hasSection = /^##\s+/m.test(text);
|
|
50
|
+
const hasLink = /\[[^\]]+\]\([^)]+\)/.test(text);
|
|
51
|
+
const ok = hasSection && hasLink;
|
|
52
|
+
return ok
|
|
53
|
+
? { ok: true, hint: null, details: { hasSection, hasLink } }
|
|
54
|
+
: { ok: false, hint: "Add `## Section` headings and `[label](path)` links to llms.txt.", details: { hasSection, hasLink } };
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "robots-allowlist",
|
|
59
|
+
weight: 10,
|
|
60
|
+
description: "robots.txt explicitly Allows AI crawlers",
|
|
61
|
+
run(rootDir) {
|
|
62
|
+
// Check root or public/.
|
|
63
|
+
const candidates = ["robots.txt", "public/robots.txt", "static/robots.txt"];
|
|
64
|
+
const found = candidates.find((c) => fs.existsSync(path.join(rootDir, c)));
|
|
65
|
+
if (!found) return { ok: false, hint: "Add robots.txt with `User-agent: GPTBot|ClaudeBot|PerplexityBot\\nAllow: /` blocks.", details: {} };
|
|
66
|
+
const text = safeRead(path.join(rootDir, found));
|
|
67
|
+
const aiBots = ["GPTBot", "ClaudeBot", "PerplexityBot", "anthropic-ai", "Google-Extended"];
|
|
68
|
+
const allowed = aiBots.filter((bot) => new RegExp(`User-agent:\\s*${bot}[\\s\\S]*?Allow:\\s*/`, "i").test(text));
|
|
69
|
+
const ok = allowed.length > 0;
|
|
70
|
+
return ok
|
|
71
|
+
? { ok: true, hint: null, details: { found, allowed } }
|
|
72
|
+
: { ok: false, hint: `robots.txt found at ${found} but no AI crawler explicitly Allowed. Add explicit User-agent: ClaudeBot blocks.`, details: { found, allowed } };
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: "page-token-budget",
|
|
77
|
+
weight: 15,
|
|
78
|
+
description: "every .md doc fits within ≈ 8K tokens (32 KB raw)",
|
|
79
|
+
run(rootDir) {
|
|
80
|
+
const docsDir = path.join(rootDir, "docs");
|
|
81
|
+
if (!fs.existsSync(docsDir)) {
|
|
82
|
+
// No docs/ at all is its own kind of agent-unfriendly, but
|
|
83
|
+
// not strictly a budget violation. Pass with a hint.
|
|
84
|
+
return { ok: true, hint: "No docs/ directory found. Consider adding one.", details: { docsDirPresent: false } };
|
|
85
|
+
}
|
|
86
|
+
const oversized = listMarkdownFiles(docsDir).filter((f) => fs.statSync(f).size > PAGE_BYTE_LIMIT);
|
|
87
|
+
if (oversized.length === 0) {
|
|
88
|
+
return { ok: true, hint: null, details: { oversizedCount: 0 } };
|
|
89
|
+
}
|
|
90
|
+
const sample = oversized.slice(0, 3).map((f) => `${path.relative(rootDir, f)} (${(fs.statSync(f).size / 1024).toFixed(1)} KB)`);
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
hint: `Split or trim ${oversized.length} oversized doc(s): ${sample.join(", ")}.`,
|
|
94
|
+
details: { oversizedCount: oversized.length, sample },
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: "heading-hierarchy",
|
|
100
|
+
weight: 10,
|
|
101
|
+
description: "each .md has exactly one H1 and no skipped levels",
|
|
102
|
+
run(rootDir) {
|
|
103
|
+
const docsDir = path.join(rootDir, "docs");
|
|
104
|
+
if (!fs.existsSync(docsDir)) return { ok: true, hint: null, details: { docsDirPresent: false } };
|
|
105
|
+
const violations = [];
|
|
106
|
+
for (const f of listMarkdownFiles(docsDir)) {
|
|
107
|
+
const text = safeRead(f);
|
|
108
|
+
const h1Count = (text.match(/^#\s+/gm) || []).length;
|
|
109
|
+
if (h1Count !== 1) {
|
|
110
|
+
violations.push({ file: path.relative(rootDir, f), h1Count });
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
// Check for skipped levels (#### without preceding ###, etc.)
|
|
114
|
+
const headings = [...text.matchAll(/^(#{1,6})\s/gm)].map((m) => m[1].length);
|
|
115
|
+
let prev = 0;
|
|
116
|
+
for (const lvl of headings) {
|
|
117
|
+
if (prev > 0 && lvl > prev + 1) {
|
|
118
|
+
violations.push({ file: path.relative(rootDir, f), reason: `H${prev}→H${lvl} skip` });
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
prev = lvl;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (violations.length === 0) {
|
|
125
|
+
return { ok: true, hint: null, details: { violationsCount: 0 } };
|
|
126
|
+
}
|
|
127
|
+
const sample = violations.slice(0, 3);
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
hint: `Fix heading hierarchy in ${violations.length} doc(s): ${sample.map((v) => v.file).join(", ")}.`,
|
|
131
|
+
details: { violationsCount: violations.length, sample },
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: "agents-readme",
|
|
137
|
+
weight: 10,
|
|
138
|
+
description: "docs/agents/README.md as agent entry point",
|
|
139
|
+
run(rootDir) {
|
|
140
|
+
const p = path.join(rootDir, "docs/agents/README.md");
|
|
141
|
+
const ok = fs.existsSync(p);
|
|
142
|
+
return ok
|
|
143
|
+
? { ok: true, hint: null, details: { path: p } }
|
|
144
|
+
: { ok: false, hint: "Create docs/agents/README.md as the agent-facing index (links from llms.txt).", details: {} };
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: "skill-md-coverage",
|
|
149
|
+
weight: 20,
|
|
150
|
+
description: "at least one SKILL.md per declared CLI capability",
|
|
151
|
+
run(rootDir) {
|
|
152
|
+
const skillDir = path.join(rootDir, "docs/agents");
|
|
153
|
+
if (!fs.existsSync(skillDir)) return { ok: false, hint: "Create docs/agents/ with one SKILL.<command>.md per CLI capability.", details: {} };
|
|
154
|
+
const skillFiles = fs.readdirSync(skillDir).filter((f) => /^SKILL\..+\.md$/.test(f));
|
|
155
|
+
if (skillFiles.length === 0) {
|
|
156
|
+
return { ok: false, hint: "Add SKILL.<command>.md files documenting each CLI capability.", details: { skillFilesCount: 0 } };
|
|
157
|
+
}
|
|
158
|
+
// We don't enumerate the user's own CLI here (out of scope for a
|
|
159
|
+
// generic auditor). At least 1 SKILL.md is the minimum signal.
|
|
160
|
+
return { ok: true, hint: null, details: { skillFilesCount: skillFiles.length } };
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
function safeRead(p) {
|
|
166
|
+
try { return fs.readFileSync(p, "utf8"); } catch { return ""; }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function listMarkdownFiles(dir, out = []) {
|
|
170
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
171
|
+
if (e.name.startsWith(".") || e.name === "node_modules") continue;
|
|
172
|
+
const p = path.join(dir, e.name);
|
|
173
|
+
if (e.isDirectory()) listMarkdownFiles(p, out);
|
|
174
|
+
else if (e.isFile() && e.name.endsWith(".md")) out.push(p);
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Run every agent-readiness check against `rootDir`. Returns a
|
|
181
|
+
* report with score, per-check results, and ranked remediation hints.
|
|
182
|
+
*
|
|
183
|
+
* @param {string} rootDir — absolute path to the repo root to audit
|
|
184
|
+
* @returns {{ score: number, checks: object[], topFixes: object[] }}
|
|
185
|
+
*/
|
|
186
|
+
export function runAgentReadiness(rootDir) {
|
|
187
|
+
const checks = [];
|
|
188
|
+
let earned = 0;
|
|
189
|
+
let possible = 0;
|
|
190
|
+
for (const c of CHECKS) {
|
|
191
|
+
possible += c.weight;
|
|
192
|
+
let result;
|
|
193
|
+
try {
|
|
194
|
+
result = c.run(rootDir);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
result = { ok: false, hint: `Check threw: ${err.message}`, details: { error: err.message } };
|
|
197
|
+
}
|
|
198
|
+
if (result.ok) earned += c.weight;
|
|
199
|
+
checks.push({
|
|
200
|
+
id: c.id,
|
|
201
|
+
description: c.description,
|
|
202
|
+
weight: c.weight,
|
|
203
|
+
score: result.ok ? c.weight : 0,
|
|
204
|
+
ok: result.ok,
|
|
205
|
+
hint: result.hint,
|
|
206
|
+
details: result.details || {},
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
const score = possible === 0 ? 0 : Math.round((earned / possible) * 100);
|
|
210
|
+
const topFixes = checks
|
|
211
|
+
.filter((c) => !c.ok && c.hint)
|
|
212
|
+
.sort((a, b) => b.weight - a.weight)
|
|
213
|
+
.slice(0, 5)
|
|
214
|
+
.map((c, i) => ({
|
|
215
|
+
priority: i + 1,
|
|
216
|
+
checkId: c.id,
|
|
217
|
+
weightLost: c.weight,
|
|
218
|
+
action: c.hint,
|
|
219
|
+
}));
|
|
220
|
+
return { score, possible, earned, checks, topFixes };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Render the report in plain text for the human terminal.
|
|
225
|
+
*/
|
|
226
|
+
export function formatAgentReadinessReport({ score, checks, topFixes }, rootDir) {
|
|
227
|
+
const lines = [`Agent Readiness — ${rootDir}: ${score}/100`, ""];
|
|
228
|
+
for (const c of checks) {
|
|
229
|
+
const tick = c.ok ? "✓" : "✗";
|
|
230
|
+
const pts = `(+${c.score})`.padStart(6);
|
|
231
|
+
lines.push(` ${tick} ${c.description.padEnd(50)} ${pts}${c.hint ? " " + c.hint : ""}`);
|
|
232
|
+
}
|
|
233
|
+
if (topFixes.length > 0) {
|
|
234
|
+
lines.push("", "Top fixes:");
|
|
235
|
+
for (const f of topFixes) {
|
|
236
|
+
lines.push(` ${f.priority}. [${f.checkId}] ${f.action}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return lines.join("\n");
|
|
240
|
+
}
|
package/src/audit/basal-cost.js
CHANGED
|
@@ -92,14 +92,68 @@ function runDepcheck(projectDir) {
|
|
|
92
92
|
});
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
// Regexes hoisted to module scope so they're compiled once instead of per file.
|
|
96
|
+
// Naming: *Re catches static/syntactic patterns; we still rely on the import set
|
|
97
|
+
// being a superset (false negative > false positive in audit context).
|
|
98
|
+
const EXPORT_RE = /export\s+(?:default\s+)?(?:function|class|const|let|var|async\s+function)\s+(\w+)/g;
|
|
99
|
+
const NAMED_EXPORT_RE = /export\s*\{([^}]+)\}/g;
|
|
100
|
+
const STATIC_NAMED_IMPORT_RE = /import\s+\{([^}]+)\}\s+from/g;
|
|
101
|
+
const DEFAULT_IMPORT_RE = /import\s+(\w+)\s+from/g;
|
|
102
|
+
// Namespace import (`import * as ns from <path>`) — knip-equivalent: mark
|
|
103
|
+
// the target file as fully-consumed because we cannot statically know
|
|
104
|
+
// which `ns.X` accesses happen.
|
|
105
|
+
const NAMESPACE_IMPORT_RE = /import\s+\*\s+as\s+\w+\s+from\s+["']([^"']+)["']/g;
|
|
106
|
+
// Any dynamic `await import(<path>)` — destructured, late-bound, or
|
|
107
|
+
// member-accessed. Conservatively mark the target as fully-consumed:
|
|
108
|
+
// trying to enumerate downstream destructuring/member-access patterns is
|
|
109
|
+
// fragile (the same test may use parenthesised assignment with no const,
|
|
110
|
+
// or bind first and `.foo`-access later). A false-negative here (one
|
|
111
|
+
// extra "live" export we can't prove dead) is always cheaper than a
|
|
112
|
+
// false-positive in audit context.
|
|
113
|
+
const DYNAMIC_IMPORT_RE = /\bawait\s+import\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
114
|
+
// Re-export bridges (`export { x } from <path>` and `export * from <path>`)
|
|
115
|
+
// — both consume from the target file (the re-exporter is itself another
|
|
116
|
+
// file consumer downstream).
|
|
117
|
+
const RE_EXPORT_NAMED_RE = /export\s*\{([^}]+)\}\s*from\s*["']([^"']+)["']/g;
|
|
118
|
+
const RE_EXPORT_STAR_RE = /export\s*\*\s*from\s*["']([^"']+)["']/g;
|
|
119
|
+
// Export preceded by a JSDoc block with `@internal` is opt-out: documented as
|
|
120
|
+
// dynamic-import-only or test-fixture surface that the regex passes can't see.
|
|
121
|
+
const INTERNAL_EXPORT_RE = /\/\*\*[\s\S]*?@internal[\s\S]*?\*\/\s*export\s+(?:default\s+)?(?:function|class|const|let|var|async\s+function)\s+(\w+)/g;
|
|
122
|
+
|
|
123
|
+
function resolveImportSpecifier(fromFile, specifier) {
|
|
124
|
+
// Only resolve project-relative paths; bare specifiers (npm packages) and
|
|
125
|
+
// node: builtins are not in our exportMap and don't matter here.
|
|
126
|
+
if (!specifier.startsWith(".") && !specifier.startsWith("/")) return null;
|
|
127
|
+
const base = path.resolve(path.dirname(fromFile), specifier);
|
|
128
|
+
// Caller will probe both with and without explicit extensions.
|
|
129
|
+
return base.endsWith(".js") || base.endsWith(".mjs") || base.endsWith(".cjs")
|
|
130
|
+
|| base.endsWith(".ts") || base.endsWith(".tsx")
|
|
131
|
+
? base
|
|
132
|
+
: `${base}.js`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Strip every string literal (template, double, single) from the content
|
|
136
|
+
// before scanning for export declarations. Test files routinely embed
|
|
137
|
+
// sample source code inside fixtures (template literals for plugin
|
|
138
|
+
// modules, double-quoted diff lines for prompt tests, etc.). At top level
|
|
139
|
+
// `export ...` is a statement, never an expression, so it can never
|
|
140
|
+
// legitimately appear inside a quoted string in real code — meaning the
|
|
141
|
+
// strip is safe for export detection. We do NOT use this stripped view
|
|
142
|
+
// for import detection: the import regexes anchor on `import {...} from`
|
|
143
|
+
// regardless of the path string, but the namespace/dynamic-import passes
|
|
144
|
+
// need the actual path inside the quotes to resolve the target module.
|
|
145
|
+
function stripStringLiterals(content) {
|
|
146
|
+
return content
|
|
147
|
+
.replace(/`[^`]*`/g, "``")
|
|
148
|
+
.replace(/"(?:\\.|[^"\\])*"/g, '""')
|
|
149
|
+
.replace(/'(?:\\.|[^'\\])*'/g, "''");
|
|
150
|
+
}
|
|
151
|
+
|
|
95
152
|
async function findDeadExports(sourceFiles) {
|
|
96
153
|
const exportMap = new Map(); // file -> [exportName]
|
|
154
|
+
const internalExportsByFile = new Map(); // file -> Set<exportName>
|
|
97
155
|
const importedNames = new Set();
|
|
98
|
-
|
|
99
|
-
const exportRe = /export\s+(?:default\s+)?(?:function|class|const|let|var|async\s+function)\s+(\w+)/g;
|
|
100
|
-
const namedExportRe = /export\s*\{([^}]+)\}/g;
|
|
101
|
-
const importRe = /import\s+\{([^}]+)\}\s+from/g;
|
|
102
|
-
const defaultImportRe = /import\s+(\w+)\s+from/g;
|
|
156
|
+
const fullyConsumedFiles = new Set(); // resolved file paths reached via `import *` or bound dynamic import
|
|
103
157
|
|
|
104
158
|
for (const file of sourceFiles) {
|
|
105
159
|
let content;
|
|
@@ -108,12 +162,13 @@ async function findDeadExports(sourceFiles) {
|
|
|
108
162
|
} catch {
|
|
109
163
|
continue;
|
|
110
164
|
}
|
|
165
|
+
const exportContent = stripStringLiterals(content);
|
|
111
166
|
|
|
112
167
|
const exports = [];
|
|
113
|
-
for (const match of
|
|
168
|
+
for (const match of exportContent.matchAll(EXPORT_RE)) {
|
|
114
169
|
exports.push(match[1]);
|
|
115
170
|
}
|
|
116
|
-
for (const match of
|
|
171
|
+
for (const match of exportContent.matchAll(NAMED_EXPORT_RE)) {
|
|
117
172
|
const names = match[1].split(",").map(n => n.trim().split(/\s+as\s+/).pop().trim());
|
|
118
173
|
exports.push(...names);
|
|
119
174
|
}
|
|
@@ -121,18 +176,46 @@ async function findDeadExports(sourceFiles) {
|
|
|
121
176
|
exportMap.set(file, exports);
|
|
122
177
|
}
|
|
123
178
|
|
|
124
|
-
|
|
179
|
+
// Collect `@internal`-tagged exports up front so the dead-pass can skip them.
|
|
180
|
+
const internalSet = new Set();
|
|
181
|
+
for (const match of exportContent.matchAll(INTERNAL_EXPORT_RE)) {
|
|
182
|
+
internalSet.add(match[1]);
|
|
183
|
+
}
|
|
184
|
+
if (internalSet.size > 0) {
|
|
185
|
+
internalExportsByFile.set(file, internalSet);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for (const match of content.matchAll(STATIC_NAMED_IMPORT_RE)) {
|
|
125
189
|
const names = match[1].split(",").map(n => n.trim().split(/\s+as\s+/)[0].trim());
|
|
126
190
|
for (const name of names) importedNames.add(name);
|
|
127
191
|
}
|
|
128
|
-
for (const match of content.matchAll(
|
|
192
|
+
for (const match of content.matchAll(DEFAULT_IMPORT_RE)) {
|
|
129
193
|
importedNames.add(match[1]);
|
|
130
194
|
}
|
|
195
|
+
for (const match of content.matchAll(NAMESPACE_IMPORT_RE)) {
|
|
196
|
+
const resolved = resolveImportSpecifier(file, match[1]);
|
|
197
|
+
if (resolved) fullyConsumedFiles.add(resolved);
|
|
198
|
+
}
|
|
199
|
+
for (const match of content.matchAll(DYNAMIC_IMPORT_RE)) {
|
|
200
|
+
const resolved = resolveImportSpecifier(file, match[1]);
|
|
201
|
+
if (resolved) fullyConsumedFiles.add(resolved);
|
|
202
|
+
}
|
|
203
|
+
for (const match of content.matchAll(RE_EXPORT_NAMED_RE)) {
|
|
204
|
+
const names = match[1].split(",").map(n => n.trim().split(/\s+as\s+/)[0].trim());
|
|
205
|
+
for (const name of names) importedNames.add(name);
|
|
206
|
+
}
|
|
207
|
+
for (const match of content.matchAll(RE_EXPORT_STAR_RE)) {
|
|
208
|
+
const resolved = resolveImportSpecifier(file, match[1]);
|
|
209
|
+
if (resolved) fullyConsumedFiles.add(resolved);
|
|
210
|
+
}
|
|
131
211
|
}
|
|
132
212
|
|
|
133
213
|
const dead = [];
|
|
134
214
|
for (const [file, exports] of exportMap) {
|
|
215
|
+
if (fullyConsumedFiles.has(file)) continue; // module imported via * or bound await import — can't tell which exports are used
|
|
216
|
+
const internalSet = internalExportsByFile.get(file);
|
|
135
217
|
for (const name of exports) {
|
|
218
|
+
if (internalSet?.has(name)) continue; // explicitly opted out via @internal
|
|
136
219
|
if (!importedNames.has(name)) {
|
|
137
220
|
dead.push({ file, name });
|
|
138
221
|
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic-only summary formatter for `kj audit` two-phase mode
|
|
3
|
+
* (KJC-TSK-0364). Renders the data AuditRole.collectDeterministic()
|
|
4
|
+
* gathers — basalCost, growth delta, stack, Sonar findings, WebPerf —
|
|
5
|
+
* as a self-contained markdown block the user can read BEFORE deciding
|
|
6
|
+
* whether to spend tokens on the LLM analysis.
|
|
7
|
+
*
|
|
8
|
+
* Kept in src/audit/ (not src/commands/) so the same formatter can be
|
|
9
|
+
* reused by future MCP tools and other entrypoints.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { groupIssuesBySeverity } from "./sonar-findings.js";
|
|
13
|
+
import { groupVulnerabilitiesBySeverity } from "./osv-findings.js";
|
|
14
|
+
import { groupFindingsBySeverity as groupSemgrepBySeverity } from "./semgrep-findings.js";
|
|
15
|
+
|
|
16
|
+
const MAX_SAMPLE_DEAD_EXPORTS = 10;
|
|
17
|
+
const MAX_SAMPLE_SONAR_PER_SEVERITY = 5;
|
|
18
|
+
const MAX_SAMPLE_OSV_PER_SEVERITY = 5;
|
|
19
|
+
const MAX_SAMPLE_SEMGREP_PER_SEVERITY = 5;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {{basalCost?: object, growthDelta?: object, stack?: object, sonarFindings?: object, webperf?: object, osvFindings?: object}} ctx
|
|
23
|
+
* @returns {string} markdown block
|
|
24
|
+
*/
|
|
25
|
+
export function formatDeterministicSummary(ctx) {
|
|
26
|
+
const lines = ["## Deterministic Findings", ""];
|
|
27
|
+
lines.push("These are the rule-based findings (no LLM). They cost zero tokens and are always factually accurate. The interactive prompt below asks if you want to extend this with an LLM analysis (which will cost tokens).");
|
|
28
|
+
lines.push("");
|
|
29
|
+
|
|
30
|
+
if (ctx.stack) lines.push(...formatStackBlock(ctx.stack));
|
|
31
|
+
if (ctx.basalCost) lines.push(...formatBasalCostBlock(ctx.basalCost, ctx.growthDelta));
|
|
32
|
+
if (ctx.sonarFindings) lines.push(...formatSonarBlock(ctx.sonarFindings));
|
|
33
|
+
if (ctx.osvFindings) lines.push(...formatOsvBlock(ctx.osvFindings));
|
|
34
|
+
if (ctx.semgrepFindings) lines.push(...formatSemgrepBlock(ctx.semgrepFindings));
|
|
35
|
+
if (ctx.webperf) lines.push(...formatWebperfBlock(ctx.webperf));
|
|
36
|
+
|
|
37
|
+
return lines.join("\n");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function formatSemgrepBlock(semgrepFindings) {
|
|
41
|
+
if (!semgrepFindings.available) {
|
|
42
|
+
return ["### Semgrep SAST", `- Status: not available — ${semgrepFindings.reason || "semgrep not installed"}`, ""];
|
|
43
|
+
}
|
|
44
|
+
const lines = ["### Semgrep SAST"];
|
|
45
|
+
lines.push(`- Total findings: ${semgrepFindings.total ?? 0}`);
|
|
46
|
+
if ((semgrepFindings.total ?? 0) > 0) {
|
|
47
|
+
const groups = groupSemgrepBySeverity(semgrepFindings.findings || []);
|
|
48
|
+
for (const [severity, findings] of Object.entries(groups)) {
|
|
49
|
+
if (findings.length === 0) continue;
|
|
50
|
+
lines.push(` - ${severity} (${findings.length}):`);
|
|
51
|
+
for (const f of findings.slice(0, MAX_SAMPLE_SEMGREP_PER_SEVERITY)) {
|
|
52
|
+
const loc = f.file ? `${f.file}${f.line ? `:${f.line}` : ""}` : "";
|
|
53
|
+
lines.push(` - ${loc} [${f.rule}]`);
|
|
54
|
+
}
|
|
55
|
+
if (findings.length > MAX_SAMPLE_SEMGREP_PER_SEVERITY) {
|
|
56
|
+
lines.push(` - ... and ${findings.length - MAX_SAMPLE_SEMGREP_PER_SEVERITY} more in ${severity}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
lines.push("");
|
|
61
|
+
return lines;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function formatOsvBlock(osvFindings) {
|
|
65
|
+
if (!osvFindings.available) {
|
|
66
|
+
return ["### OSV Vulnerabilities", `- Status: not available — ${osvFindings.reason || "osv-scanner not installed"}`, ""];
|
|
67
|
+
}
|
|
68
|
+
const lines = ["### OSV Vulnerabilities (Dependencies)"];
|
|
69
|
+
lines.push(`- Total open: ${osvFindings.total ?? 0}`);
|
|
70
|
+
if ((osvFindings.total ?? 0) > 0) {
|
|
71
|
+
const groups = groupVulnerabilitiesBySeverity(osvFindings.vulnerabilities || []);
|
|
72
|
+
for (const [severity, vulns] of Object.entries(groups)) {
|
|
73
|
+
if (vulns.length === 0) continue;
|
|
74
|
+
lines.push(` - ${severity} (${vulns.length}):`);
|
|
75
|
+
for (const v of vulns.slice(0, MAX_SAMPLE_OSV_PER_SEVERITY)) {
|
|
76
|
+
const where = v.package ? `${v.package}@${v.version || "?"}` : "(unknown)";
|
|
77
|
+
const fixed = v.fixed ? ` → fixed in ${v.fixed}` : "";
|
|
78
|
+
lines.push(` - ${v.id} ${where}${fixed}`);
|
|
79
|
+
}
|
|
80
|
+
if (vulns.length > MAX_SAMPLE_OSV_PER_SEVERITY) {
|
|
81
|
+
lines.push(` - ... and ${vulns.length - MAX_SAMPLE_OSV_PER_SEVERITY} more in ${severity}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
lines.push("");
|
|
86
|
+
return lines;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function formatStackBlock(stack) {
|
|
90
|
+
if (!stack || (!stack.frameworks?.length && !stack.language && !stack.isFrontend && !stack.isBackend)) return [];
|
|
91
|
+
const lines = ["### Project Stack"];
|
|
92
|
+
if (stack.language) lines.push(`- Language: ${stack.language}`);
|
|
93
|
+
if (stack.frameworks?.length) lines.push(`- Frameworks: ${stack.frameworks.join(", ")}`);
|
|
94
|
+
const tier = stack.isFullstack ? "fullstack (frontend + backend)"
|
|
95
|
+
: stack.isFrontend ? "frontend-only"
|
|
96
|
+
: stack.isBackend ? "backend-only"
|
|
97
|
+
: "unknown";
|
|
98
|
+
lines.push(`- Tier: ${tier}`);
|
|
99
|
+
lines.push("");
|
|
100
|
+
return lines;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function formatBasalCostBlock(basalCost, growthDelta) {
|
|
104
|
+
const lines = ["### Basal Cost"];
|
|
105
|
+
lines.push(`- Source: ${basalCost.totalLines} lines across ${basalCost.totalFiles} files`);
|
|
106
|
+
const deps = basalCost.dependencies || {};
|
|
107
|
+
if (deps.total) lines.push(`- Dependencies: ${deps.total} (${deps.dependencies} prod + ${deps.devDependencies} dev)`);
|
|
108
|
+
|
|
109
|
+
const unused = basalCost.unusedDependencies?.unused;
|
|
110
|
+
if (Array.isArray(unused) && unused.length > 0) {
|
|
111
|
+
lines.push(`- Unused dependencies (${unused.length}): ${unused.join(", ")}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const dead = Array.isArray(basalCost.deadExports) ? basalCost.deadExports : [];
|
|
115
|
+
if (dead.length > 0) {
|
|
116
|
+
lines.push(`- Dead exports: ${dead.length}`);
|
|
117
|
+
for (const de of dead.slice(0, MAX_SAMPLE_DEAD_EXPORTS)) {
|
|
118
|
+
lines.push(` - \`${de.name}\` in ${de.file}`);
|
|
119
|
+
}
|
|
120
|
+
if (dead.length > MAX_SAMPLE_DEAD_EXPORTS) {
|
|
121
|
+
lines.push(` - ... and ${dead.length - MAX_SAMPLE_DEAD_EXPORTS} more`);
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
lines.push("- Dead exports: 0");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (growthDelta) {
|
|
128
|
+
lines.push("");
|
|
129
|
+
lines.push("**Growth since last audit**");
|
|
130
|
+
lines.push(`- Lines: ${growthDelta.lines >= 0 ? "+" : ""}${growthDelta.lines}`);
|
|
131
|
+
lines.push(`- Files: ${growthDelta.files >= 0 ? "+" : ""}${growthDelta.files}`);
|
|
132
|
+
lines.push(`- Dependencies: ${growthDelta.deps >= 0 ? "+" : ""}${growthDelta.deps}`);
|
|
133
|
+
if (growthDelta.since) lines.push(`- Since: ${growthDelta.since}`);
|
|
134
|
+
}
|
|
135
|
+
lines.push("");
|
|
136
|
+
return lines;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function formatSonarBlock(sonarFindings) {
|
|
140
|
+
if (!sonarFindings.available) {
|
|
141
|
+
return ["### SonarQube", `- Status: not available — ${sonarFindings.reason || "host unreachable"}`, ""];
|
|
142
|
+
}
|
|
143
|
+
const lines = ["### SonarQube"];
|
|
144
|
+
if (sonarFindings.qualityGate?.status) lines.push(`- Quality gate: ${sonarFindings.qualityGate.status}`);
|
|
145
|
+
lines.push(`- Open issues: ${sonarFindings.total ?? 0}`);
|
|
146
|
+
if ((sonarFindings.total ?? 0) > 0) {
|
|
147
|
+
const groups = groupIssuesBySeverity(sonarFindings.issues || []);
|
|
148
|
+
for (const [severity, issues] of Object.entries(groups)) {
|
|
149
|
+
if (issues.length === 0) continue;
|
|
150
|
+
lines.push(` - ${severity} (${issues.length}):`);
|
|
151
|
+
for (const issue of issues.slice(0, MAX_SAMPLE_SONAR_PER_SEVERITY)) {
|
|
152
|
+
const loc = issue.component ? `${issue.component}${issue.line ? `:${issue.line}` : ""}` : "";
|
|
153
|
+
const rule = issue.rule ? ` [${issue.rule}]` : "";
|
|
154
|
+
lines.push(` - ${loc}${rule} ${issue.message || ""}`.trim());
|
|
155
|
+
}
|
|
156
|
+
if (issues.length > MAX_SAMPLE_SONAR_PER_SEVERITY) {
|
|
157
|
+
lines.push(` - ... and ${issues.length - MAX_SAMPLE_SONAR_PER_SEVERITY} more in ${severity}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
lines.push("");
|
|
162
|
+
return lines;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function formatWebperfBlock(webperf) {
|
|
166
|
+
if (!webperf.available) {
|
|
167
|
+
return ["### WebPerf", `- Status: not collected — ${webperf.reason || "no frontend signal"}`, ""];
|
|
168
|
+
}
|
|
169
|
+
const lines = ["### WebPerf"];
|
|
170
|
+
if (webperf.mode === "cwv-result" && webperf.cwv) {
|
|
171
|
+
lines.push(`- Mode: live CWV measurement`);
|
|
172
|
+
if (webperf.url) lines.push(`- URL: ${webperf.url}`);
|
|
173
|
+
lines.push(`- Verdict: ${webperf.cwv.pass ? "PASS" : "FAIL"}`);
|
|
174
|
+
for (const [metric, score] of Object.entries(webperf.cwv.scores || {})) {
|
|
175
|
+
lines.push(` - ${metric.toUpperCase()}: ${score.value} (${score.rating})`);
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
lines.push(`- Mode: static hints only (no live CWV measurement available)`);
|
|
179
|
+
lines.push(`- LLM phase will scan source for bundle/lazy/img-opt patterns when activated`);
|
|
180
|
+
}
|
|
181
|
+
lines.push("");
|
|
182
|
+
return lines;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Detect whether the deterministic context found any signal worth showing.
|
|
187
|
+
* If everything came back empty/unavailable, the CLI can skip the
|
|
188
|
+
* "Continue with LLM?" prompt — there's nothing to compare against.
|
|
189
|
+
*/
|
|
190
|
+
export function deterministicContextHasFindings(ctx) {
|
|
191
|
+
if (!ctx) return false;
|
|
192
|
+
if (ctx.basalCost?.deadExports?.length > 0) return true;
|
|
193
|
+
if (ctx.basalCost?.unusedDependencies?.unused?.length > 0) return true;
|
|
194
|
+
if (ctx.sonarFindings?.available && (ctx.sonarFindings.total ?? 0) > 0) return true;
|
|
195
|
+
if (ctx.sonarFindings?.qualityGate?.status === "ERROR") return true;
|
|
196
|
+
if (ctx.osvFindings?.available && (ctx.osvFindings.total ?? 0) > 0) return true;
|
|
197
|
+
if (ctx.semgrepFindings?.available && (ctx.semgrepFindings.total ?? 0) > 0) return true;
|
|
198
|
+
if (ctx.growthDelta && (Math.abs(ctx.growthDelta.lines || 0) > 100 || Math.abs(ctx.growthDelta.deps || 0) > 0)) return true;
|
|
199
|
+
return false;
|
|
200
|
+
}
|