@redaksjon/brennpunkt 0.0.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.
@@ -0,0 +1,115 @@
1
+ function parseLcov(content) {
2
+ const files = [];
3
+ let current = null;
4
+ for (const line of content.split("\n")) {
5
+ const trimmed = line.trim();
6
+ if (trimmed.startsWith("SF:")) {
7
+ current = {
8
+ file: trimmed.slice(3),
9
+ linesFound: 0,
10
+ linesHit: 0,
11
+ functionsFound: 0,
12
+ functionsHit: 0,
13
+ branchesFound: 0,
14
+ branchesHit: 0
15
+ };
16
+ } else if (current) {
17
+ if (trimmed.startsWith("LF:")) {
18
+ current.linesFound = parseInt(trimmed.slice(3), 10);
19
+ } else if (trimmed.startsWith("LH:")) {
20
+ current.linesHit = parseInt(trimmed.slice(3), 10);
21
+ } else if (trimmed.startsWith("FNF:")) {
22
+ current.functionsFound = parseInt(trimmed.slice(4), 10);
23
+ } else if (trimmed.startsWith("FNH:")) {
24
+ current.functionsHit = parseInt(trimmed.slice(4), 10);
25
+ } else if (trimmed.startsWith("BRF:")) {
26
+ current.branchesFound = parseInt(trimmed.slice(4), 10);
27
+ } else if (trimmed.startsWith("BRH:")) {
28
+ current.branchesHit = parseInt(trimmed.slice(4), 10);
29
+ } else if (trimmed === "end_of_record") {
30
+ files.push(current);
31
+ current = null;
32
+ }
33
+ }
34
+ }
35
+ return files;
36
+ }
37
+
38
+ function calculateOverallCoverage(files) {
39
+ const totals = files.reduce((acc, f) => ({
40
+ linesFound: acc.linesFound + f.linesFound,
41
+ linesHit: acc.linesHit + f.linesHit,
42
+ functionsFound: acc.functionsFound + f.functionsFound,
43
+ functionsHit: acc.functionsHit + f.functionsHit,
44
+ branchesFound: acc.branchesFound + f.branchesFound,
45
+ branchesHit: acc.branchesHit + f.branchesHit
46
+ }), {
47
+ linesFound: 0,
48
+ linesHit: 0,
49
+ functionsFound: 0,
50
+ functionsHit: 0,
51
+ branchesFound: 0,
52
+ branchesHit: 0
53
+ });
54
+ return {
55
+ lines: {
56
+ found: totals.linesFound,
57
+ hit: totals.linesHit,
58
+ coverage: totals.linesFound > 0 ? Math.round(totals.linesHit / totals.linesFound * 1e4) / 100 : 100
59
+ },
60
+ functions: {
61
+ found: totals.functionsFound,
62
+ hit: totals.functionsHit,
63
+ coverage: totals.functionsFound > 0 ? Math.round(totals.functionsHit / totals.functionsFound * 1e4) / 100 : 100
64
+ },
65
+ branches: {
66
+ found: totals.branchesFound,
67
+ hit: totals.branchesHit,
68
+ coverage: totals.branchesFound > 0 ? Math.round(totals.branchesHit / totals.branchesFound * 1e4) / 100 : 100
69
+ },
70
+ fileCount: files.length
71
+ };
72
+ }
73
+ function analyzeFile(file, weights) {
74
+ const lineCoverage = file.linesFound > 0 ? file.linesHit / file.linesFound * 100 : 100;
75
+ const functionCoverage = file.functionsFound > 0 ? file.functionsHit / file.functionsFound * 100 : 100;
76
+ const branchCoverage = file.branchesFound > 0 ? file.branchesHit / file.branchesFound * 100 : 100;
77
+ const lineGap = 100 - lineCoverage;
78
+ const functionGap = 100 - functionCoverage;
79
+ const branchGap = 100 - branchCoverage;
80
+ const sizeFactor = Math.log10(Math.max(file.linesFound, 1) + 1);
81
+ const priorityScore = (branchGap * weights.branches + functionGap * weights.functions + lineGap * weights.lines) * sizeFactor;
82
+ return {
83
+ file: file.file,
84
+ lines: {
85
+ found: file.linesFound,
86
+ hit: file.linesHit,
87
+ coverage: Math.round(lineCoverage * 100) / 100
88
+ },
89
+ functions: {
90
+ found: file.functionsFound,
91
+ hit: file.functionsHit,
92
+ coverage: Math.round(functionCoverage * 100) / 100
93
+ },
94
+ branches: {
95
+ found: file.branchesFound,
96
+ hit: file.branchesHit,
97
+ coverage: Math.round(branchCoverage * 100) / 100
98
+ },
99
+ priorityScore: Math.round(priorityScore * 100) / 100,
100
+ uncoveredLines: file.linesFound - file.linesHit,
101
+ uncoveredBranches: file.branchesFound - file.branchesHit
102
+ };
103
+ }
104
+ function analyzeCoverage(files, weights, minLines, top) {
105
+ const overall = calculateOverallCoverage(files);
106
+ const analyzed = files.filter((f) => f.linesFound >= minLines).map((f) => analyzeFile(f, weights)).sort((a, b) => b.priorityScore - a.priorityScore);
107
+ const results = top ? analyzed.slice(0, top) : analyzed;
108
+ return {
109
+ overall,
110
+ files: results
111
+ };
112
+ }
113
+
114
+ export { analyzeCoverage as a, analyzeFile as b, calculateOverallCoverage as c, parseLcov as p };
115
+ //# sourceMappingURL=analyzer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyzer.js","sources":["../src/parser.ts","../src/analyzer.ts"],"sourcesContent":["/**\n * LCOV file parser\n */\n\nimport type { LcovFileData } from './types';\n\n/**\n * Parse an LCOV format file content into structured file coverage data.\n * \n * LCOV format documentation:\n * - SF:<source file path> - Start of file record\n * - LF:<lines found> - Total number of instrumented lines\n * - LH:<lines hit> - Number of lines with execution count > 0\n * - FNF:<functions found> - Total number of functions\n * - FNH:<functions hit> - Number of functions executed\n * - BRF:<branches found> - Total number of branches\n * - BRH:<branches hit> - Number of branches taken\n * - end_of_record - End of file record\n * \n * @param content - Raw LCOV file content\n * @returns Array of parsed file coverage data\n */\nexport function parseLcov(content: string): LcovFileData[] {\n const files: LcovFileData[] = [];\n let current: LcovFileData | null = null;\n\n for (const line of content.split('\\n')) {\n const trimmed = line.trim();\n \n if (trimmed.startsWith('SF:')) {\n current = {\n file: trimmed.slice(3),\n linesFound: 0,\n linesHit: 0,\n functionsFound: 0,\n functionsHit: 0,\n branchesFound: 0,\n branchesHit: 0,\n };\n } else if (current) {\n if (trimmed.startsWith('LF:')) {\n current.linesFound = parseInt(trimmed.slice(3), 10);\n } else if (trimmed.startsWith('LH:')) {\n current.linesHit = parseInt(trimmed.slice(3), 10);\n } else if (trimmed.startsWith('FNF:')) {\n current.functionsFound = parseInt(trimmed.slice(4), 10);\n } else if (trimmed.startsWith('FNH:')) {\n current.functionsHit = parseInt(trimmed.slice(4), 10);\n } else if (trimmed.startsWith('BRF:')) {\n current.branchesFound = parseInt(trimmed.slice(4), 10);\n } else if (trimmed.startsWith('BRH:')) {\n current.branchesHit = parseInt(trimmed.slice(4), 10);\n } else if (trimmed === 'end_of_record') {\n files.push(current);\n current = null;\n }\n }\n }\n\n return files;\n}\n","/**\n * Coverage analysis and priority scoring\n */\n\nimport type { \n LcovFileData, \n PriorityWeights, \n OverallCoverage, \n AnalyzedFile,\n AnalysisResult \n} from './types';\n\n/**\n * Calculate overall coverage statistics from all files.\n * This aggregates totals before any filtering is applied.\n * \n * @param files - Array of parsed LCOV file data\n * @returns Overall coverage summary\n */\nexport function calculateOverallCoverage(files: LcovFileData[]): OverallCoverage {\n const totals = files.reduce((acc, f) => ({\n linesFound: acc.linesFound + f.linesFound,\n linesHit: acc.linesHit + f.linesHit,\n functionsFound: acc.functionsFound + f.functionsFound,\n functionsHit: acc.functionsHit + f.functionsHit,\n branchesFound: acc.branchesFound + f.branchesFound,\n branchesHit: acc.branchesHit + f.branchesHit,\n }), {\n linesFound: 0,\n linesHit: 0,\n functionsFound: 0,\n functionsHit: 0,\n branchesFound: 0,\n branchesHit: 0,\n });\n\n return {\n lines: {\n found: totals.linesFound,\n hit: totals.linesHit,\n coverage: totals.linesFound > 0 \n ? Math.round((totals.linesHit / totals.linesFound) * 10000) / 100 \n : 100,\n },\n functions: {\n found: totals.functionsFound,\n hit: totals.functionsHit,\n coverage: totals.functionsFound > 0 \n ? Math.round((totals.functionsHit / totals.functionsFound) * 10000) / 100 \n : 100,\n },\n branches: {\n found: totals.branchesFound,\n hit: totals.branchesHit,\n coverage: totals.branchesFound > 0 \n ? Math.round((totals.branchesHit / totals.branchesFound) * 10000) / 100 \n : 100,\n },\n fileCount: files.length,\n };\n}\n\n/**\n * Calculate coverage percentages and priority score for a single file.\n * \n * Priority scoring logic:\n * - Higher score = higher priority for testing\n * - Based on coverage gaps (100 - coverage%) for each metric\n * - Weighted by the provided weights (default: branches 0.5, functions 0.3, lines 0.2)\n * - Scaled by file size (log10) so larger files with low coverage rank higher\n * \n * @param file - Parsed LCOV data for a single file\n * @param weights - Weights for each coverage type\n * @returns Analyzed file with coverage percentages and priority score\n */\nexport function analyzeFile(file: LcovFileData, weights: PriorityWeights): AnalyzedFile {\n const lineCoverage = file.linesFound > 0 \n ? (file.linesHit / file.linesFound) * 100 \n : 100;\n \n const functionCoverage = file.functionsFound > 0 \n ? (file.functionsHit / file.functionsFound) * 100 \n : 100;\n \n const branchCoverage = file.branchesFound > 0 \n ? (file.branchesHit / file.branchesFound) * 100 \n : 100;\n\n // Priority score: lower coverage = higher priority (inverted)\n // Weighted combination of coverage gaps\n const lineGap = 100 - lineCoverage;\n const functionGap = 100 - functionCoverage;\n const branchGap = 100 - branchCoverage;\n\n // Factor in file size - bigger files with low coverage = more important\n const sizeFactor = Math.log10(Math.max(file.linesFound, 1) + 1);\n\n const priorityScore = (\n (branchGap * weights.branches) +\n (functionGap * weights.functions) +\n (lineGap * weights.lines)\n ) * sizeFactor;\n\n return {\n file: file.file,\n lines: {\n found: file.linesFound,\n hit: file.linesHit,\n coverage: Math.round(lineCoverage * 100) / 100,\n },\n functions: {\n found: file.functionsFound,\n hit: file.functionsHit,\n coverage: Math.round(functionCoverage * 100) / 100,\n },\n branches: {\n found: file.branchesFound,\n hit: file.branchesHit,\n coverage: Math.round(branchCoverage * 100) / 100,\n },\n priorityScore: Math.round(priorityScore * 100) / 100,\n uncoveredLines: file.linesFound - file.linesHit,\n uncoveredBranches: file.branchesFound - file.branchesHit,\n };\n}\n\n/**\n * Perform complete coverage analysis on LCOV data.\n * \n * @param files - Array of parsed LCOV file data\n * @param weights - Priority weights for scoring\n * @param minLines - Minimum lines threshold for including a file\n * @param top - Optional limit on number of results\n * @returns Complete analysis result with overall and per-file data\n */\nexport function analyzeCoverage(\n files: LcovFileData[],\n weights: PriorityWeights,\n minLines: number,\n top: number | null\n): AnalysisResult {\n // Calculate overall coverage from ALL files (before filtering)\n const overall = calculateOverallCoverage(files);\n \n // Filter and analyze\n const analyzed = files\n .filter(f => f.linesFound >= minLines)\n .map(f => analyzeFile(f, weights))\n .sort((a, b) => b.priorityScore - a.priorityScore);\n\n // Apply top limit if specified\n const results = top ? analyzed.slice(0, top) : analyzed;\n\n return {\n overall,\n files: results,\n };\n}\n"],"names":[],"mappings":"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"}
package/dist/main.js ADDED
@@ -0,0 +1,376 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
3
+ import { resolve, dirname } from 'node:path';
4
+ import { Command } from 'commander';
5
+ import { z } from 'zod';
6
+ import { p as parseLcov, a as analyzeCoverage } from './analyzer.js';
7
+
8
+ function supportsColor() {
9
+ if (process.env.NO_COLOR !== void 0) {
10
+ return false;
11
+ }
12
+ if (process.env.FORCE_COLOR !== void 0) {
13
+ return true;
14
+ }
15
+ if (!process.stdout.isTTY) {
16
+ return false;
17
+ }
18
+ if (process.env.TERM === "dumb") {
19
+ return false;
20
+ }
21
+ return true;
22
+ }
23
+ function color(code, text, useColors) {
24
+ if (!useColors) return text;
25
+ return `${code}${text}\x1B[0m`;
26
+ }
27
+ function colorPct(pct, useColors) {
28
+ const text = `${pct.toFixed(2)}%`;
29
+ if (pct >= 90) return color("\x1B[32m", text, useColors);
30
+ if (pct >= 80) return color("\x1B[33m", text, useColors);
31
+ return color("\x1B[31m", text, useColors);
32
+ }
33
+ function getColorCode(coverage, useColors) {
34
+ if (!useColors) return "";
35
+ if (coverage < 50) return "\x1B[31m";
36
+ if (coverage < 80) return "\x1B[33m";
37
+ return "\x1B[32m";
38
+ }
39
+ function formatTable(result, options, forceColors) {
40
+ const { overall, files: analyzed } = result;
41
+ const lines = [];
42
+ const divider = "─".repeat(120);
43
+ const useColors = supportsColor();
44
+ const reset = useColors ? "\x1B[0m" : "";
45
+ lines.push("");
46
+ lines.push("📊 Coverage Priority Report");
47
+ lines.push("");
48
+ lines.push("┌─────────────────────────────────────────────────────────────────┐");
49
+ lines.push("│ OVERALL COVERAGE │");
50
+ lines.push("├─────────────────┬─────────────────┬─────────────────────────────┤");
51
+ lines.push(`│ Lines: ${colorPct(overall.lines.coverage, useColors).padEnd(24)}│ Functions: ${colorPct(overall.functions.coverage, useColors).padEnd(20)}│ Branches: ${colorPct(overall.branches.coverage, useColors).padEnd(22)}│`);
52
+ lines.push(`│ (${overall.lines.hit}/${overall.lines.found})`.padEnd(18) + `│ (${overall.functions.hit}/${overall.functions.found})`.padEnd(18) + `│ (${overall.branches.hit}/${overall.branches.found})`.padEnd(30) + "│");
53
+ lines.push("└─────────────────┴─────────────────┴─────────────────────────────┘");
54
+ lines.push(`
55
+ Files: ${overall.fileCount} | Weights: B=${options.weights.branches}, F=${options.weights.functions}, L=${options.weights.lines} | Min lines: ${options.minLines}
56
+ `);
57
+ lines.push(divider);
58
+ lines.push(
59
+ "Priority".padEnd(10) + "File".padEnd(45) + "Lines".padEnd(12) + "Funcs".padEnd(12) + "Branch".padEnd(12) + "Uncov Lines".padEnd(12) + "Score"
60
+ );
61
+ lines.push(divider);
62
+ analyzed.forEach((item, index) => {
63
+ const priority = index + 1;
64
+ const fileName = item.file.length > 43 ? "..." + item.file.slice(-40) : item.file;
65
+ const colorLine = getColorCode(item.lines.coverage, useColors);
66
+ const colorFunc = getColorCode(item.functions.coverage, useColors);
67
+ const colorBranch = getColorCode(item.branches.coverage, useColors);
68
+ lines.push(
69
+ `#${priority}`.padEnd(10) + fileName.padEnd(45) + `${colorLine}${item.lines.coverage.toFixed(1)}%${reset}`.padEnd(21) + `${colorFunc}${item.functions.coverage.toFixed(1)}%${reset}`.padEnd(21) + `${colorBranch}${item.branches.coverage.toFixed(1)}%${reset}`.padEnd(21) + `${item.uncoveredLines}`.padEnd(12) + item.priorityScore.toFixed(1)
70
+ );
71
+ });
72
+ lines.push(divider);
73
+ lines.push(`
74
+ Total files analyzed: ${analyzed.length}`);
75
+ const totalUncoveredLines = analyzed.reduce((sum, f) => sum + f.uncoveredLines, 0);
76
+ const totalUncoveredBranches = analyzed.reduce((sum, f) => sum + f.uncoveredBranches, 0);
77
+ lines.push(`Total uncovered lines: ${totalUncoveredLines}`);
78
+ lines.push(`Total uncovered branches: ${totalUncoveredBranches}`);
79
+ lines.push("");
80
+ lines.push("🎯 Recommended Focus (Top 3):");
81
+ lines.push("");
82
+ analyzed.slice(0, 3).forEach((item, i) => {
83
+ const reasons = [];
84
+ if (item.branches.coverage < 70) reasons.push(`${item.branches.found - item.branches.hit} untested branches`);
85
+ if (item.functions.coverage < 80) reasons.push(`${item.functions.found - item.functions.hit} untested functions`);
86
+ if (item.lines.coverage < 70) reasons.push(`${item.uncoveredLines} uncovered lines`);
87
+ lines.push(` ${i + 1}. ${item.file}`);
88
+ lines.push(` ${reasons.join(", ") || "General coverage improvement"}`);
89
+ lines.push("");
90
+ });
91
+ return lines.join("\n");
92
+ }
93
+ function formatJson(result) {
94
+ return JSON.stringify(result, null, 2);
95
+ }
96
+
97
+ const VERSION = "0.0.1";
98
+ const PROGRAM_NAME = "brennpunkt";
99
+ const CONFIG_FILE_NAME = "brennpunkt.yaml";
100
+ const COVERAGE_SEARCH_PATHS = [
101
+ "coverage/lcov.info",
102
+ // Jest, Vitest, c8 (most common)
103
+ ".coverage/lcov.info",
104
+ // Some configurations
105
+ "coverage/lcov/lcov.info",
106
+ // Karma
107
+ "lcov.info",
108
+ // Project root
109
+ ".nyc_output/lcov.info",
110
+ // NYC legacy
111
+ "test-results/lcov.info"
112
+ // Some CI configurations
113
+ ];
114
+ const ConfigSchema = z.object({
115
+ coveragePath: z.string().optional(),
116
+ weights: z.string().optional(),
117
+ minLines: z.number().optional(),
118
+ json: z.boolean().optional(),
119
+ top: z.number().optional()
120
+ });
121
+ const DEFAULTS = {
122
+ coveragePath: "coverage/lcov.info",
123
+ weights: "0.5,0.3,0.2",
124
+ minLines: 10,
125
+ json: false,
126
+ top: void 0
127
+ };
128
+ function parseWeights(weightsStr) {
129
+ const parts = weightsStr.split(",").map(Number);
130
+ if (parts.length !== 3 || parts.some(isNaN)) {
131
+ throw new Error("Weights must be three comma-separated numbers (branches,functions,lines)");
132
+ }
133
+ return {
134
+ branches: parts[0],
135
+ functions: parts[1],
136
+ lines: parts[2]
137
+ };
138
+ }
139
+ function resolveCoveragePath(inputPath) {
140
+ if (inputPath.startsWith("/")) {
141
+ return inputPath;
142
+ }
143
+ return resolve(process.cwd(), inputPath);
144
+ }
145
+ function discoverCoverageFile() {
146
+ const cwd = process.cwd();
147
+ const searched = [];
148
+ for (const searchPath of COVERAGE_SEARCH_PATHS) {
149
+ const fullPath = resolve(cwd, searchPath);
150
+ searched.push(fullPath);
151
+ if (existsSync(fullPath)) {
152
+ return { found: fullPath, searched };
153
+ }
154
+ }
155
+ return null;
156
+ }
157
+ function parseSimpleYaml(content) {
158
+ const result = {};
159
+ for (const line of content.split("\n")) {
160
+ const trimmed = line.trim();
161
+ if (!trimmed || trimmed.startsWith("#")) {
162
+ continue;
163
+ }
164
+ const colonIndex = trimmed.indexOf(":");
165
+ if (colonIndex === -1) {
166
+ continue;
167
+ }
168
+ const key = trimmed.slice(0, colonIndex).trim();
169
+ let value = trimmed.slice(colonIndex + 1).trim();
170
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
171
+ value = value.slice(1, -1);
172
+ }
173
+ if (value === "true") {
174
+ result[key] = true;
175
+ } else if (value === "false") {
176
+ result[key] = false;
177
+ } else if (!isNaN(Number(value)) && value !== "") {
178
+ result[key] = Number(value);
179
+ } else {
180
+ result[key] = value;
181
+ }
182
+ }
183
+ return result;
184
+ }
185
+ function readConfigFile(configPath) {
186
+ const paths = configPath ? [configPath] : [
187
+ resolve(process.cwd(), CONFIG_FILE_NAME),
188
+ resolve(process.cwd(), ".brennpunkt", "config.yaml")
189
+ ];
190
+ for (const filePath of paths) {
191
+ if (existsSync(filePath)) {
192
+ try {
193
+ const content = readFileSync(filePath, "utf-8");
194
+ const parsed = parseSimpleYaml(content);
195
+ return ConfigSchema.partial().parse(parsed);
196
+ } catch {
197
+ }
198
+ }
199
+ }
200
+ return {};
201
+ }
202
+ function generateConfigFile(outputPath) {
203
+ const filePath = outputPath || resolve(process.cwd(), CONFIG_FILE_NAME);
204
+ const configContent = `# Brennpunkt Configuration
205
+ # https://github.com/redaksjon/brennpunkt
206
+
207
+ # Path to lcov.info coverage file
208
+ # coveragePath: coverage/lcov.info
209
+
210
+ # Priority weights for branches, functions, lines (must sum to 1.0)
211
+ # Higher branch weight means untested branches are prioritized more heavily
212
+ # weights: "0.5,0.3,0.2"
213
+
214
+ # Minimum number of lines for a file to be included in analysis
215
+ # Helps filter out tiny utility files
216
+ # minLines: 10
217
+
218
+ # Output format (true for JSON, false for table)
219
+ # json: false
220
+
221
+ # Limit results to top N files (comment out or remove for all files)
222
+ # top: 20
223
+ `;
224
+ const dir = dirname(filePath);
225
+ if (!existsSync(dir)) {
226
+ mkdirSync(dir, { recursive: true });
227
+ }
228
+ writeFileSync(filePath, configContent, "utf-8");
229
+ process.stdout.write(`Configuration file created: ${filePath}
230
+ `);
231
+ }
232
+ function checkConfig(cliArgs) {
233
+ const fileConfig = readConfigFile(cliArgs.config);
234
+ process.stdout.write("\n");
235
+ process.stdout.write("================================================================================\n");
236
+ process.stdout.write("BRENNPUNKT CONFIGURATION\n");
237
+ process.stdout.write("================================================================================\n\n");
238
+ const configPath = cliArgs.config || resolve(process.cwd(), CONFIG_FILE_NAME);
239
+ const configExists = existsSync(configPath);
240
+ process.stdout.write(`Config file: ${configPath}
241
+ `);
242
+ process.stdout.write(`Status: ${configExists ? "Found" : "Not found (using defaults)"}
243
+
244
+ `);
245
+ process.stdout.write("RESOLVED CONFIGURATION:\n");
246
+ process.stdout.write("--------------------------------------------------------------------------------\n");
247
+ const mergedConfig = { ...DEFAULTS, ...fileConfig };
248
+ const formatValue = (key, value, isFromFile) => {
249
+ const source = isFromFile ? "[config file]" : "[default] ";
250
+ process.stdout.write(` ${source} ${key.padEnd(15)}: ${JSON.stringify(value)}
251
+ `);
252
+ };
253
+ for (const key of Object.keys(DEFAULTS)) {
254
+ const fileValue = fileConfig[key];
255
+ const isFromFile = fileValue !== void 0;
256
+ formatValue(key, mergedConfig[key], isFromFile);
257
+ }
258
+ process.stdout.write("\n================================================================================\n");
259
+ }
260
+ function runAnalysis(config, explicitPath) {
261
+ let resolvedPath;
262
+ if (explicitPath && config.coveragePath) {
263
+ resolvedPath = resolveCoveragePath(config.coveragePath);
264
+ if (!existsSync(resolvedPath)) {
265
+ process.stderr.write(`Error: Could not find coverage file at ${resolvedPath}
266
+ `);
267
+ process.stderr.write("Run tests with coverage first: npm test -- --coverage\n");
268
+ process.exit(1);
269
+ }
270
+ } else {
271
+ const discovered = discoverCoverageFile();
272
+ if (!discovered) {
273
+ process.stderr.write("Error: Could not find coverage file\n");
274
+ process.stderr.write("\nSearched locations:\n");
275
+ for (const searchPath of COVERAGE_SEARCH_PATHS) {
276
+ process.stderr.write(` - ${searchPath}
277
+ `);
278
+ }
279
+ process.stderr.write("\nRun tests with coverage first: npm test -- --coverage\n");
280
+ process.stderr.write("Or specify path explicitly: brennpunkt <path-to-lcov.info>\n");
281
+ process.exit(1);
282
+ }
283
+ resolvedPath = discovered.found;
284
+ const relativePath = resolvedPath.replace(process.cwd() + "/", "");
285
+ process.stderr.write(`Using coverage file: ${relativePath}
286
+
287
+ `);
288
+ }
289
+ let weights;
290
+ try {
291
+ weights = parseWeights(config.weights || DEFAULTS.weights);
292
+ } catch (err) {
293
+ process.stderr.write(`Error: ${err.message}
294
+ `);
295
+ process.exit(1);
296
+ }
297
+ const options = {
298
+ weights,
299
+ minLines: config.minLines ?? DEFAULTS.minLines,
300
+ json: config.json ?? DEFAULTS.json,
301
+ top: config.top ?? null
302
+ };
303
+ let lcovContent;
304
+ try {
305
+ lcovContent = readFileSync(resolvedPath, "utf-8");
306
+ } catch (err) {
307
+ process.stderr.write(`Error: Could not read ${resolvedPath}
308
+ `);
309
+ process.stderr.write(`${err.message}
310
+ `);
311
+ process.exit(1);
312
+ }
313
+ const files = parseLcov(lcovContent);
314
+ if (files.length === 0) {
315
+ process.stderr.write("Warning: No coverage data found in the file\n");
316
+ process.exit(0);
317
+ }
318
+ const result = analyzeCoverage(files, options.weights, options.minLines, options.top);
319
+ if (options.json) {
320
+ process.stdout.write(formatJson(result) + "\n");
321
+ } else {
322
+ process.stdout.write(formatTable(result, options) + "\n");
323
+ }
324
+ }
325
+ function main() {
326
+ const program = new Command();
327
+ program.name(PROGRAM_NAME).description("Coverage priority analyzer - identify where to focus testing efforts").version(VERSION).argument("[coverage-path]", "Path to lcov.info file", DEFAULTS.coveragePath).option("-w, --weights <weights>", "Custom weights for branches,functions,lines", DEFAULTS.weights).option("-m, --min-lines <number>", "Exclude files with fewer than N lines", String(DEFAULTS.minLines)).option("-j, --json", "Output as JSON", DEFAULTS.json).option("-t, --top <number>", "Show only top N priority files").option("-c, --config <path>", "Path to configuration file", CONFIG_FILE_NAME).option("--init-config", "Generate a default brennpunkt.yaml configuration file").option("--check-config", "Display resolved configuration and exit");
328
+ program.parse();
329
+ const cliArgs = program.opts();
330
+ const coveragePath = program.args[0] || cliArgs.coveragePath;
331
+ if (process.argv.includes("--init-config")) {
332
+ generateConfigFile(cliArgs.config);
333
+ return;
334
+ }
335
+ if (process.argv.includes("--check-config")) {
336
+ checkConfig(cliArgs);
337
+ return;
338
+ }
339
+ const fileConfig = readConfigFile(cliArgs.config);
340
+ const explicitPath = Boolean(coveragePath || fileConfig.coveragePath);
341
+ let parsedMinLines;
342
+ let parsedTop;
343
+ if (cliArgs.minLines) {
344
+ parsedMinLines = parseInt(cliArgs.minLines, 10);
345
+ if (isNaN(parsedMinLines) || parsedMinLines < 0) {
346
+ process.stderr.write(`Error: --min-lines must be a non-negative number
347
+ `);
348
+ process.exit(1);
349
+ }
350
+ }
351
+ if (cliArgs.top) {
352
+ parsedTop = parseInt(cliArgs.top, 10);
353
+ if (isNaN(parsedTop) || parsedTop <= 0) {
354
+ process.stderr.write(`Error: --top must be a positive number
355
+ `);
356
+ process.exit(1);
357
+ }
358
+ }
359
+ const config = {
360
+ ...DEFAULTS,
361
+ ...fileConfig,
362
+ ...coveragePath && { coveragePath },
363
+ ...cliArgs.weights && { weights: cliArgs.weights },
364
+ ...parsedMinLines !== void 0 && { minLines: parsedMinLines },
365
+ ...cliArgs.json !== void 0 && { json: cliArgs.json },
366
+ ...parsedTop !== void 0 && { top: parsedTop }
367
+ };
368
+ runAnalysis(config, explicitPath);
369
+ }
370
+ const isTestEnvironment = process.env.NODE_ENV === "test" || process.env.VITEST === "true" || process.argv.some((arg) => arg.includes("vitest"));
371
+ if (!isTestEnvironment) {
372
+ main();
373
+ }
374
+
375
+ export { ConfigSchema, discoverCoverageFile };
376
+ //# sourceMappingURL=main.js.map