claude-flow 3.56.7 → 3.56.9
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/.claude/helpers/hook-handler.cjs +109 -23
- package/README.md +9 -3
- package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
- package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts +1 -0
- package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts.map +1 -1
- package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js +3 -1
- package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js.map +1 -1
- package/node_modules/@claude-flow/codex/dist/index.d.ts +3 -3
- package/node_modules/@claude-flow/codex/dist/index.js +1 -1
- package/node_modules/@claude-flow/codex/package.json +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
- package/node_modules/@claude-flow/security/dist/policy/envelope.js +6 -6
- package/node_modules/@claude-flow/security/dist/policy/envelope.js.map +1 -1
- package/node_modules/@claude-flow/security/package.json +1 -1
- package/package.json +5 -5
- package/v3/@claude-flow/cli/dist/src/commands/analyze.js +40 -29
- package/v3/@claude-flow/cli/dist/src/commands/coverage-reader.d.ts +29 -0
- package/v3/@claude-flow/cli/dist/src/commands/coverage-reader.js +247 -0
- package/v3/@claude-flow/cli/dist/src/commands/hooks.js +9 -180
- package/v3/@claude-flow/cli/dist/src/commands/tui.js +2 -2
- package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +109 -23
- package/v3/@claude-flow/cli/dist/src/parser.js +32 -22
- package/v3/@claude-flow/cli/dist/src/production/monitoring.d.ts +1 -0
- package/v3/@claude-flow/cli/dist/src/production/monitoring.js +19 -6
- package/v3/@claude-flow/cli/dist/src/production/rate-limiter.js +5 -4
- package/v3/@claude-flow/cli/dist/src/production/retry.js +3 -0
- package/v3/@claude-flow/cli/dist/src/services/config-file-manager.js +3 -3
- package/v3/@claude-flow/cli/dist/src/services/policy-ledger-anchor.js +20 -2
- package/v3/@claude-flow/cli/package.json +5 -5
- package/v3/@claude-flow/shared/dist/events/rvf-event-log.d.ts +9 -0
- package/v3/@claude-flow/shared/dist/events/rvf-event-log.js +44 -3
- package/v3/@claude-flow/shared/dist/mcp/transport/http.js +11 -1
- package/v3/@claude-flow/shared/dist/resilience/rate-limiter.js +3 -3
- package/v3/@claude-flow/shared/dist/resilience/retry.js +3 -1
- package/v3/@claude-flow/shared/package.json +12 -9
- package/v3/@claude-flow/shared/dist/events/event-store-pagination.test.d.ts +0 -2
- package/v3/@claude-flow/shared/dist/events/event-store-pagination.test.js +0 -32
- package/v3/@claude-flow/shared/dist/events/event-store.test.d.ts +0 -8
- package/v3/@claude-flow/shared/dist/events/event-store.test.js +0 -313
- package/v3/@claude-flow/shared/dist/hooks/hooks.test.d.ts +0 -9
- package/v3/@claude-flow/shared/dist/hooks/hooks.test.js +0 -322
- package/v3/@claude-flow/shared/dist/hooks/verify-exports.test.d.ts +0 -9
- package/v3/@claude-flow/shared/dist/hooks/verify-exports.test.js +0 -93
- package/v3/@claude-flow/shared/dist/resilience/bulkhead-sync-throw.test.d.ts +0 -2
- package/v3/@claude-flow/shared/dist/resilience/bulkhead-sync-throw.test.js +0 -33
- package/v3/@claude-flow/shared/dist/resilience/retry-timeout-cleanup.test.d.ts +0 -2
- package/v3/@claude-flow/shared/dist/resilience/retry-timeout-cleanup.test.js +0 -38
|
@@ -97,11 +97,19 @@ const diffCommand = {
|
|
|
97
97
|
output.printInfo(`Analyzing diff: ${output.highlight(ref)}`);
|
|
98
98
|
try {
|
|
99
99
|
// Call MCP tool for diff analysis
|
|
100
|
+
// Shape returned by the analyze_diff MCP tool (ruvector/diff-classifier.ts).
|
|
101
|
+
// Typed from the classifier itself so the renderer cannot drift from it again.
|
|
100
102
|
const result = await callMCPTool('analyze_diff', {
|
|
101
103
|
ref,
|
|
102
|
-
|
|
104
|
+
// Always request file risks: the handler computes them anyway, and the
|
|
105
|
+
// high-risk file list below is derived from them.
|
|
106
|
+
includeFileRisks: true,
|
|
103
107
|
includeReviewers: showReviewers || showAll,
|
|
104
108
|
});
|
|
109
|
+
if (result.error) {
|
|
110
|
+
output.printError(`Diff analysis failed: ${result.message ?? 'unknown error'}`);
|
|
111
|
+
return { success: false, exitCode: 1 };
|
|
112
|
+
}
|
|
105
113
|
// JSON output
|
|
106
114
|
if (formatType === 'json') {
|
|
107
115
|
output.printJson(result);
|
|
@@ -110,13 +118,18 @@ const diffCommand = {
|
|
|
110
118
|
output.writeln();
|
|
111
119
|
// Summary box
|
|
112
120
|
const files = result.files || [];
|
|
113
|
-
const
|
|
114
|
-
const
|
|
121
|
+
const fileRisks = result.fileRisks || [];
|
|
122
|
+
const risk = result.risk || { overall: 'low', score: 0, breakdown: { low: 0, medium: 0, high: 0, critical: 0 } };
|
|
123
|
+
const classification = result.classification;
|
|
124
|
+
const category = classification?.primary ?? 'unknown';
|
|
125
|
+
const secondary = classification?.secondary?.length ? classification.secondary.join(', ') : '';
|
|
126
|
+
const totalChanges = files.reduce((sum, f) => sum + f.additions + f.deletions, 0);
|
|
127
|
+
const highRiskFiles = fileRisks.filter((r) => r.risk === 'high' || r.risk === 'critical').map((r) => r.file);
|
|
115
128
|
output.printBox([
|
|
116
129
|
`Ref: ${result.ref || 'HEAD'}`,
|
|
117
130
|
`Files: ${files.length}`,
|
|
118
131
|
`Risk: ${getRiskDisplay(risk.overall)} (${risk.score}/100)`,
|
|
119
|
-
`Type: ${
|
|
132
|
+
`Type: ${category}${secondary ? ` (${secondary})` : ''}`,
|
|
120
133
|
``,
|
|
121
134
|
result.summary || 'No summary available',
|
|
122
135
|
].join('\n'), 'Diff Analysis');
|
|
@@ -128,33 +141,28 @@ const diffCommand = {
|
|
|
128
141
|
output.printTable({
|
|
129
142
|
columns: [
|
|
130
143
|
{ key: 'metric', header: 'Metric', width: 25 },
|
|
131
|
-
{ key: 'value', header: 'Value', width:
|
|
144
|
+
{ key: 'value', header: 'Value', width: 42 },
|
|
132
145
|
],
|
|
133
146
|
data: [
|
|
134
147
|
{ metric: 'Overall Risk', value: getRiskDisplay(risk.overall) },
|
|
135
148
|
{ metric: 'Risk Score', value: `${risk.score}/100` },
|
|
136
|
-
{ metric: 'Files Changed', value:
|
|
137
|
-
{ metric: 'Total Lines Changed', value:
|
|
138
|
-
{ metric: '
|
|
149
|
+
{ metric: 'Files Changed', value: files.length },
|
|
150
|
+
{ metric: 'Total Lines Changed', value: totalChanges },
|
|
151
|
+
{ metric: 'Files by Risk', value: `low ${risk.breakdown.low} / medium ${risk.breakdown.medium} / high ${risk.breakdown.high} / critical ${risk.breakdown.critical}` },
|
|
139
152
|
],
|
|
140
153
|
});
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
output.writeln(output.bold(output.warning('Security Concerns')));
|
|
145
|
-
output.printList(risk.breakdown.securityConcerns.map(c => output.warning(c)));
|
|
146
|
-
}
|
|
147
|
-
// Breaking changes
|
|
148
|
-
if (risk.breakdown.breakingChanges.length > 0) {
|
|
154
|
+
// Risk factors reported by the classifier
|
|
155
|
+
const riskFactors = classification?.riskFactors ?? [];
|
|
156
|
+
if (riskFactors.length > 0) {
|
|
149
157
|
output.writeln();
|
|
150
|
-
output.writeln(output.bold(output.
|
|
151
|
-
output.printList(
|
|
158
|
+
output.writeln(output.bold(output.warning('Risk Factors')));
|
|
159
|
+
output.printList(riskFactors.map(c => output.warning(c)));
|
|
152
160
|
}
|
|
153
161
|
// High risk files
|
|
154
|
-
if (
|
|
162
|
+
if (highRiskFiles.length > 0) {
|
|
155
163
|
output.writeln();
|
|
156
164
|
output.writeln(output.bold('High Risk Files'));
|
|
157
|
-
output.printList(
|
|
165
|
+
output.printList(highRiskFiles.map(f => output.warning(f)));
|
|
158
166
|
}
|
|
159
167
|
}
|
|
160
168
|
// Classification
|
|
@@ -168,13 +176,16 @@ const diffCommand = {
|
|
|
168
176
|
{ key: 'value', header: 'Value', width: 40 },
|
|
169
177
|
],
|
|
170
178
|
data: [
|
|
171
|
-
{ field: 'Category', value:
|
|
172
|
-
{ field: '
|
|
173
|
-
{ field: 'Confidence', value: `${(classification
|
|
179
|
+
{ field: 'Category', value: category },
|
|
180
|
+
{ field: 'Secondary', value: secondary || '-' },
|
|
181
|
+
{ field: 'Confidence', value: `${((classification?.confidence ?? 0) * 100).toFixed(0)}%` },
|
|
182
|
+
{ field: 'Impact', value: classification?.impactLevel ?? '-' },
|
|
174
183
|
],
|
|
175
184
|
});
|
|
176
|
-
|
|
177
|
-
|
|
185
|
+
if (classification?.testingStrategy?.length) {
|
|
186
|
+
output.writeln();
|
|
187
|
+
output.writeln(output.dim(`Testing strategy: ${classification.testingStrategy.join(', ')}`));
|
|
188
|
+
}
|
|
178
189
|
}
|
|
179
190
|
// Reviewers
|
|
180
191
|
if (showReviewers || showAll) {
|
|
@@ -190,13 +201,13 @@ const diffCommand = {
|
|
|
190
201
|
}
|
|
191
202
|
}
|
|
192
203
|
// Verbose file-level details
|
|
193
|
-
if (verbose &&
|
|
204
|
+
if (verbose && fileRisks.length > 0) {
|
|
194
205
|
output.writeln();
|
|
195
206
|
output.writeln(output.bold('File-Level Analysis'));
|
|
196
207
|
output.writeln(output.dim('-'.repeat(50)));
|
|
197
208
|
output.printTable({
|
|
198
209
|
columns: [
|
|
199
|
-
{ key: '
|
|
210
|
+
{ key: 'file', header: 'File', width: 40 },
|
|
200
211
|
{ key: 'risk', header: 'Risk', width: 12, format: (v) => getRiskDisplay(String(v)) },
|
|
201
212
|
{ key: 'score', header: 'Score', width: 8, align: 'right' },
|
|
202
213
|
{ key: 'reasons', header: 'Reasons', width: 30, format: (v) => {
|
|
@@ -204,7 +215,7 @@ const diffCommand = {
|
|
|
204
215
|
return reasons.slice(0, 2).join('; ');
|
|
205
216
|
} },
|
|
206
217
|
],
|
|
207
|
-
data:
|
|
218
|
+
data: fileRisks.map((r) => ({ ...r })),
|
|
208
219
|
});
|
|
209
220
|
}
|
|
210
221
|
// Files changed table
|
|
@@ -219,7 +230,7 @@ const diffCommand = {
|
|
|
219
230
|
{ key: 'additions', header: '+', width: 8, align: 'right', format: (v) => output.success(`+${v}`) },
|
|
220
231
|
{ key: 'deletions', header: '-', width: 8, align: 'right', format: (v) => output.error(`-${v}`) },
|
|
221
232
|
],
|
|
222
|
-
data: files.slice(0, 20),
|
|
233
|
+
data: files.slice(0, 20).map((f) => ({ ...f })),
|
|
223
234
|
});
|
|
224
235
|
if (files.length > 20) {
|
|
225
236
|
output.writeln(output.dim(` ... and ${files.length - 20} more files`));
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
interface CoverageFileEntry {
|
|
2
|
+
filePath: string;
|
|
3
|
+
lines: number;
|
|
4
|
+
branches: number;
|
|
5
|
+
functions: number;
|
|
6
|
+
statements: number;
|
|
7
|
+
}
|
|
8
|
+
interface CoverageData {
|
|
9
|
+
found: boolean;
|
|
10
|
+
source: string;
|
|
11
|
+
entries: CoverageFileEntry[];
|
|
12
|
+
summary: {
|
|
13
|
+
totalFiles: number;
|
|
14
|
+
overallLineCoverage: number;
|
|
15
|
+
overallBranchCoverage: number;
|
|
16
|
+
overallFunctionCoverage: number;
|
|
17
|
+
overallStatementCoverage: number;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Read coverage data from disk. Checks these locations in order:
|
|
22
|
+
* 1. coverage/coverage-summary.json (Jest/Istanbul)
|
|
23
|
+
* 2. coverage/lcov.info (lcov format)
|
|
24
|
+
* 3. coverage/coverage-final.json (Istanbul counters)
|
|
25
|
+
* 4. .nyc_output/out.json (nyc counters or legacy summary)
|
|
26
|
+
*/
|
|
27
|
+
export declare function readCoverageFromDisk(): CoverageData;
|
|
28
|
+
export {};
|
|
29
|
+
//# sourceMappingURL=coverage-reader.d.ts.map
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
/** Coverage reports are build output the project controls: refuse anything large enough to stall or exhaust the process. */
|
|
4
|
+
const MAX_REPORT_BYTES = 64 * 1024 * 1024;
|
|
5
|
+
/** The file's text, or undefined when it is missing, not a regular file, unreadable, or over the size cap. */
|
|
6
|
+
function readReport(path) {
|
|
7
|
+
try {
|
|
8
|
+
const stat = statSync(path);
|
|
9
|
+
if (!stat.isFile() || stat.size > MAX_REPORT_BYTES)
|
|
10
|
+
return undefined;
|
|
11
|
+
return readFileSync(path, 'utf-8');
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Read coverage data from disk. Checks these locations in order:
|
|
19
|
+
* 1. coverage/coverage-summary.json (Jest/Istanbul)
|
|
20
|
+
* 2. coverage/lcov.info (lcov format)
|
|
21
|
+
* 3. coverage/coverage-final.json (Istanbul counters)
|
|
22
|
+
* 4. .nyc_output/out.json (nyc counters or legacy summary)
|
|
23
|
+
*/
|
|
24
|
+
export function readCoverageFromDisk() {
|
|
25
|
+
const cwd = process.cwd();
|
|
26
|
+
const noData = {
|
|
27
|
+
found: false,
|
|
28
|
+
source: 'none',
|
|
29
|
+
entries: [],
|
|
30
|
+
summary: { totalFiles: 0, overallLineCoverage: 0, overallBranchCoverage: 0, overallFunctionCoverage: 0, overallStatementCoverage: 0 },
|
|
31
|
+
};
|
|
32
|
+
// 1. Try coverage-summary.json (Jest/Istanbul)
|
|
33
|
+
for (const relPath of ['coverage/coverage-summary.json', 'coverage-summary.json']) {
|
|
34
|
+
const text = readReport(join(cwd, relPath));
|
|
35
|
+
if (text !== undefined) {
|
|
36
|
+
try {
|
|
37
|
+
const raw = JSON.parse(text);
|
|
38
|
+
const report = parseCoverageSummaryJson(raw, relPath);
|
|
39
|
+
if (report.found)
|
|
40
|
+
return report;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// malformed, try next
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// 2. Try lcov.info
|
|
48
|
+
for (const relPath of ['coverage/lcov.info', 'lcov.info']) {
|
|
49
|
+
const raw = readReport(join(cwd, relPath));
|
|
50
|
+
if (raw !== undefined) {
|
|
51
|
+
try {
|
|
52
|
+
const report = parseLcovInfo(raw, relPath);
|
|
53
|
+
if (report.found)
|
|
54
|
+
return report;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// malformed, try next
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Istanbul's raw counter map (Jest, Vitest, nyc).
|
|
62
|
+
for (const relPath of ['coverage/coverage-final.json', 'coverage-final.json', '.nyc_output/out.json']) {
|
|
63
|
+
const text = readReport(join(cwd, relPath));
|
|
64
|
+
if (text === undefined)
|
|
65
|
+
continue;
|
|
66
|
+
try {
|
|
67
|
+
const raw = JSON.parse(text);
|
|
68
|
+
const report = parseIstanbulCoverage(raw, relPath);
|
|
69
|
+
if (report.found)
|
|
70
|
+
return report;
|
|
71
|
+
const legacy = parseCoverageSummaryJson(raw, relPath);
|
|
72
|
+
if (legacy.found)
|
|
73
|
+
return legacy;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Missing or malformed, try the next supported report.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return noData;
|
|
80
|
+
}
|
|
81
|
+
function parseCoverageSummaryJson(data, source) {
|
|
82
|
+
const entries = [];
|
|
83
|
+
const metricsByFile = [];
|
|
84
|
+
for (const [filePath, metrics] of Object.entries(data)) {
|
|
85
|
+
if (filePath === 'total')
|
|
86
|
+
continue;
|
|
87
|
+
const m = metrics;
|
|
88
|
+
if (!m || typeof m !== 'object')
|
|
89
|
+
continue;
|
|
90
|
+
if (!['lines', 'branches', 'functions', 'statements'].some(key => m[key]))
|
|
91
|
+
continue;
|
|
92
|
+
metricsByFile.push(m);
|
|
93
|
+
const linePct = percentage(m.lines) ?? 0;
|
|
94
|
+
const branchPct = (percentage(m.branches) ?? 0);
|
|
95
|
+
const funcPct = (percentage(m.functions) ?? 0);
|
|
96
|
+
const stmtPct = (percentage(m.statements) ?? 0);
|
|
97
|
+
entries.push({ filePath, lines: linePct, branches: branchPct, functions: funcPct, statements: stmtPct });
|
|
98
|
+
}
|
|
99
|
+
// Only aggregate complete counts. A missing denominator is unknown, not empty.
|
|
100
|
+
const total = data['total'];
|
|
101
|
+
const aggregate = (key) => {
|
|
102
|
+
const supplied = percentage(total?.[key]);
|
|
103
|
+
if (supplied !== undefined)
|
|
104
|
+
return supplied;
|
|
105
|
+
const metrics = metricsByFile.map(file => file[key]);
|
|
106
|
+
if (metrics.length === 0 || !metrics.every(hasCounts))
|
|
107
|
+
return 0;
|
|
108
|
+
return ratio(metrics.reduce((sum, m) => sum + m.covered, 0), metrics.reduce((sum, m) => sum + m.total, 0));
|
|
109
|
+
};
|
|
110
|
+
// Sort by lowest line coverage
|
|
111
|
+
entries.sort((a, b) => a.lines - b.lines);
|
|
112
|
+
return {
|
|
113
|
+
found: entries.length > 0,
|
|
114
|
+
source,
|
|
115
|
+
entries,
|
|
116
|
+
summary: {
|
|
117
|
+
totalFiles: entries.length,
|
|
118
|
+
overallLineCoverage: aggregate('lines'),
|
|
119
|
+
overallBranchCoverage: aggregate('branches'),
|
|
120
|
+
overallFunctionCoverage: aggregate('functions'),
|
|
121
|
+
overallStatementCoverage: aggregate('statements'),
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function parseLcovInfo(raw, source) {
|
|
126
|
+
const entries = [];
|
|
127
|
+
let currentFile = '';
|
|
128
|
+
let linesHit = 0, linesFound = 0;
|
|
129
|
+
let branchesHit = 0, branchesFound = 0;
|
|
130
|
+
let functionsHit = 0, functionsFound = 0;
|
|
131
|
+
let totalLH = 0, totalLF = 0, totalBH = 0, totalBF = 0, totalFH = 0, totalFF = 0;
|
|
132
|
+
const flushRecord = () => {
|
|
133
|
+
if (currentFile) {
|
|
134
|
+
// A corrupt record cannot report more hits than lines, or a negative count.
|
|
135
|
+
linesHit = Math.min(linesHit, linesFound);
|
|
136
|
+
branchesHit = Math.min(branchesHit, branchesFound);
|
|
137
|
+
functionsHit = Math.min(functionsHit, functionsFound);
|
|
138
|
+
totalLH += linesHit;
|
|
139
|
+
totalLF += linesFound;
|
|
140
|
+
totalBH += branchesHit;
|
|
141
|
+
totalBF += branchesFound;
|
|
142
|
+
totalFH += functionsHit;
|
|
143
|
+
totalFF += functionsFound;
|
|
144
|
+
entries.push({
|
|
145
|
+
filePath: currentFile,
|
|
146
|
+
lines: linesFound > 0 ? (linesHit / linesFound) * 100 : 100,
|
|
147
|
+
branches: branchesFound > 0 ? (branchesHit / branchesFound) * 100 : 100,
|
|
148
|
+
functions: functionsFound > 0 ? (functionsHit / functionsFound) * 100 : 100,
|
|
149
|
+
statements: linesFound > 0 ? (linesHit / linesFound) * 100 : 100,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
for (const line of raw.split('\n')) {
|
|
154
|
+
const trimmed = line.trim();
|
|
155
|
+
if (trimmed.startsWith('SF:')) {
|
|
156
|
+
flushRecord();
|
|
157
|
+
currentFile = trimmed.slice(3);
|
|
158
|
+
linesHit = 0;
|
|
159
|
+
linesFound = 0;
|
|
160
|
+
branchesHit = 0;
|
|
161
|
+
branchesFound = 0;
|
|
162
|
+
functionsHit = 0;
|
|
163
|
+
functionsFound = 0;
|
|
164
|
+
}
|
|
165
|
+
else if (trimmed.startsWith('LH:')) {
|
|
166
|
+
linesHit = count(trimmed.slice(3));
|
|
167
|
+
}
|
|
168
|
+
else if (trimmed.startsWith('LF:')) {
|
|
169
|
+
linesFound = count(trimmed.slice(3));
|
|
170
|
+
}
|
|
171
|
+
else if (trimmed.startsWith('BRH:')) {
|
|
172
|
+
branchesHit = count(trimmed.slice(4));
|
|
173
|
+
}
|
|
174
|
+
else if (trimmed.startsWith('BRF:')) {
|
|
175
|
+
branchesFound = count(trimmed.slice(4));
|
|
176
|
+
}
|
|
177
|
+
else if (trimmed.startsWith('FNH:')) {
|
|
178
|
+
functionsHit = count(trimmed.slice(4));
|
|
179
|
+
}
|
|
180
|
+
else if (trimmed.startsWith('FNF:')) {
|
|
181
|
+
functionsFound = count(trimmed.slice(4));
|
|
182
|
+
}
|
|
183
|
+
else if (trimmed === 'end_of_record') {
|
|
184
|
+
flushRecord();
|
|
185
|
+
currentFile = '';
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
flushRecord();
|
|
189
|
+
entries.sort((a, b) => a.lines - b.lines);
|
|
190
|
+
return {
|
|
191
|
+
found: entries.length > 0,
|
|
192
|
+
source,
|
|
193
|
+
entries,
|
|
194
|
+
summary: {
|
|
195
|
+
totalFiles: entries.length,
|
|
196
|
+
overallLineCoverage: ratio(totalLH, totalLF),
|
|
197
|
+
overallBranchCoverage: ratio(totalBH, totalBF),
|
|
198
|
+
overallFunctionCoverage: ratio(totalFH, totalFF),
|
|
199
|
+
overallStatementCoverage: ratio(totalLH, totalLF),
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/** A non-negative integer count; anything else in a report line is treated as 0. */
|
|
204
|
+
function count(text) {
|
|
205
|
+
const n = parseInt(text, 10);
|
|
206
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
207
|
+
}
|
|
208
|
+
function ratio(covered, total) {
|
|
209
|
+
return total > 0 ? covered / total * 100 : 100;
|
|
210
|
+
}
|
|
211
|
+
function hasCounts(metric) {
|
|
212
|
+
return typeof metric?.total === 'number' && Number.isFinite(metric.total) && metric.total >= 0
|
|
213
|
+
&& typeof metric.covered === 'number' && Number.isFinite(metric.covered)
|
|
214
|
+
&& metric.covered >= 0 && metric.covered <= metric.total;
|
|
215
|
+
}
|
|
216
|
+
function percentage(metric) {
|
|
217
|
+
if (typeof metric?.pct === 'number' && Number.isFinite(metric.pct)
|
|
218
|
+
&& metric.pct >= 0 && metric.pct <= 100)
|
|
219
|
+
return metric.pct;
|
|
220
|
+
return hasCounts(metric) ? ratio(metric.covered, metric.total) : undefined;
|
|
221
|
+
}
|
|
222
|
+
/** Istanbul line coverage uses the maximum statement hit count on each start line. */
|
|
223
|
+
function parseIstanbulCoverage(data, source) {
|
|
224
|
+
const summary = {};
|
|
225
|
+
for (const [filePath, value] of Object.entries(data)) {
|
|
226
|
+
const cov = value;
|
|
227
|
+
if (!cov || !cov.s || !cov.f || !cov.b || !cov.statementMap)
|
|
228
|
+
continue;
|
|
229
|
+
const lines = new Map();
|
|
230
|
+
for (const [id, hits] of Object.entries(cov.s)) {
|
|
231
|
+
const line = cov.statementMap[id]?.start?.line;
|
|
232
|
+
if (!Number.isInteger(line) || line < 1)
|
|
233
|
+
throw new Error('Invalid Istanbul statement location');
|
|
234
|
+
lines.set(line, Math.max(lines.get(line) ?? 0, hits));
|
|
235
|
+
}
|
|
236
|
+
const metric = (hits) => {
|
|
237
|
+
if (hits.some(n => !Number.isFinite(n) || n < 0))
|
|
238
|
+
throw new Error('Invalid Istanbul counter');
|
|
239
|
+
const covered = hits.filter(n => n > 0).length;
|
|
240
|
+
return { total: hits.length, covered, pct: ratio(covered, hits.length) };
|
|
241
|
+
};
|
|
242
|
+
summary[filePath] = { lines: metric([...lines.values()]), statements: metric(Object.values(cov.s)),
|
|
243
|
+
functions: metric(Object.values(cov.f)), branches: metric(Object.values(cov.b).flat()) };
|
|
244
|
+
}
|
|
245
|
+
return parseCoverageSummaryJson(summary, source);
|
|
246
|
+
}
|
|
247
|
+
//# sourceMappingURL=coverage-reader.js.map
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* V3 CLI Hooks Command
|
|
3
|
+
* Self-learning hooks system for intelligent workflow automation
|
|
4
|
+
*/
|
|
1
5
|
import { validateFeedbackPatterns } from '../memory/feedback-patterns.js';
|
|
6
|
+
import { readCoverageFromDisk } from './coverage-reader.js';
|
|
2
7
|
import { output } from '../output.js';
|
|
3
8
|
import { confirm } from '../prompt.js';
|
|
4
9
|
import { callMCPTool, MCPClientError } from '../mcp-client.js';
|
|
5
10
|
import { storeCommand } from './transfer-store.js';
|
|
6
|
-
import {
|
|
7
|
-
import { join } from 'node:path';
|
|
11
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
8
12
|
import { getSecurityStatus as sharedGetSecurityStatus, getSwarmStatus as sharedGetSwarmStatus, getGitUncommittedCount as sharedGetGitUncommittedCount, } from '../funnel/local-signals.js';
|
|
9
13
|
/**
|
|
10
14
|
* #1686 — `?? 0` only defaults null/undefined; NaN slips through and
|
|
@@ -16,181 +20,6 @@ function safeNum(value, fallback = 0) {
|
|
|
16
20
|
const n = typeof value === 'number' ? value : Number(value);
|
|
17
21
|
return Number.isFinite(n) ? n : fallback;
|
|
18
22
|
}
|
|
19
|
-
/**
|
|
20
|
-
* Read coverage data from disk. Checks these locations in order:
|
|
21
|
-
* 1. coverage/coverage-summary.json (Jest/Istanbul)
|
|
22
|
-
* 2. coverage/lcov.info (lcov format)
|
|
23
|
-
* 3. .nyc_output/out.json (nyc)
|
|
24
|
-
*/
|
|
25
|
-
function readCoverageFromDisk() {
|
|
26
|
-
const cwd = process.cwd();
|
|
27
|
-
const noData = {
|
|
28
|
-
found: false,
|
|
29
|
-
source: 'none',
|
|
30
|
-
entries: [],
|
|
31
|
-
summary: { totalFiles: 0, overallLineCoverage: 0, overallBranchCoverage: 0, overallFunctionCoverage: 0, overallStatementCoverage: 0 },
|
|
32
|
-
};
|
|
33
|
-
// 1. Try coverage-summary.json (Jest/Istanbul)
|
|
34
|
-
for (const relPath of ['coverage/coverage-summary.json', 'coverage-summary.json']) {
|
|
35
|
-
const summaryPath = join(cwd, relPath);
|
|
36
|
-
if (existsSync(summaryPath)) {
|
|
37
|
-
try {
|
|
38
|
-
const raw = JSON.parse(readFileSync(summaryPath, 'utf-8'));
|
|
39
|
-
return parseCoverageSummaryJson(raw, relPath);
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
// malformed, try next
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
// 2. Try lcov.info
|
|
47
|
-
for (const relPath of ['coverage/lcov.info', 'lcov.info']) {
|
|
48
|
-
const lcovPath = join(cwd, relPath);
|
|
49
|
-
if (existsSync(lcovPath)) {
|
|
50
|
-
try {
|
|
51
|
-
const raw = readFileSync(lcovPath, 'utf-8');
|
|
52
|
-
return parseLcovInfo(raw, relPath);
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
// malformed, try next
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
// 3. Try .nyc_output/out.json
|
|
60
|
-
const nycPath = join(cwd, '.nyc_output', 'out.json');
|
|
61
|
-
if (existsSync(nycPath)) {
|
|
62
|
-
try {
|
|
63
|
-
const raw = JSON.parse(readFileSync(nycPath, 'utf-8'));
|
|
64
|
-
return parseCoverageSummaryJson(raw, '.nyc_output/out.json');
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
// malformed
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return noData;
|
|
71
|
-
}
|
|
72
|
-
function parseCoverageSummaryJson(data, source) {
|
|
73
|
-
const entries = [];
|
|
74
|
-
let totalLines = 0, coveredLines = 0;
|
|
75
|
-
let totalBranches = 0, coveredBranches = 0;
|
|
76
|
-
let totalFunctions = 0, coveredFunctions = 0;
|
|
77
|
-
let totalStatements = 0, coveredStatements = 0;
|
|
78
|
-
for (const [filePath, metrics] of Object.entries(data)) {
|
|
79
|
-
if (filePath === 'total')
|
|
80
|
-
continue;
|
|
81
|
-
const m = metrics;
|
|
82
|
-
if (!m || typeof m !== 'object')
|
|
83
|
-
continue;
|
|
84
|
-
const linePct = m.lines?.pct ?? m.lines?.covered != null ? ((m.lines?.covered ?? 0) / Math.max(m.lines?.total ?? 1, 1)) * 100 : 0;
|
|
85
|
-
const branchPct = m.branches?.pct ?? (m.branches?.total ? ((m.branches?.covered ?? 0) / m.branches.total) * 100 : 100);
|
|
86
|
-
const funcPct = m.functions?.pct ?? (m.functions?.total ? ((m.functions?.covered ?? 0) / m.functions.total) * 100 : 100);
|
|
87
|
-
const stmtPct = m.statements?.pct ?? (m.statements?.total ? ((m.statements?.covered ?? 0) / m.statements.total) * 100 : 100);
|
|
88
|
-
entries.push({ filePath, lines: linePct, branches: branchPct, functions: funcPct, statements: stmtPct });
|
|
89
|
-
totalLines += m.lines?.total ?? 0;
|
|
90
|
-
coveredLines += m.lines?.covered ?? 0;
|
|
91
|
-
totalBranches += m.branches?.total ?? 0;
|
|
92
|
-
coveredBranches += m.branches?.covered ?? 0;
|
|
93
|
-
totalFunctions += m.functions?.total ?? 0;
|
|
94
|
-
coveredFunctions += m.functions?.covered ?? 0;
|
|
95
|
-
totalStatements += m.statements?.total ?? 0;
|
|
96
|
-
coveredStatements += m.statements?.covered ?? 0;
|
|
97
|
-
}
|
|
98
|
-
// Also read the total key if present
|
|
99
|
-
const total = data['total'];
|
|
100
|
-
const overallLine = total?.lines?.pct ?? (totalLines > 0 ? (coveredLines / totalLines) * 100 : 0);
|
|
101
|
-
const overallBranch = total?.branches?.pct ?? (totalBranches > 0 ? (coveredBranches / totalBranches) * 100 : 0);
|
|
102
|
-
const overallFunction = total?.functions?.pct ?? (totalFunctions > 0 ? (coveredFunctions / totalFunctions) * 100 : 0);
|
|
103
|
-
const overallStatement = total?.statements?.pct ?? (totalStatements > 0 ? (coveredStatements / totalStatements) * 100 : 0);
|
|
104
|
-
// Sort by lowest line coverage
|
|
105
|
-
entries.sort((a, b) => a.lines - b.lines);
|
|
106
|
-
return {
|
|
107
|
-
found: true,
|
|
108
|
-
source,
|
|
109
|
-
entries,
|
|
110
|
-
summary: {
|
|
111
|
-
totalFiles: entries.length,
|
|
112
|
-
overallLineCoverage: overallLine,
|
|
113
|
-
overallBranchCoverage: overallBranch,
|
|
114
|
-
overallFunctionCoverage: overallFunction,
|
|
115
|
-
overallStatementCoverage: overallStatement,
|
|
116
|
-
},
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
function parseLcovInfo(raw, source) {
|
|
120
|
-
const entries = [];
|
|
121
|
-
let currentFile = '';
|
|
122
|
-
let linesHit = 0, linesFound = 0;
|
|
123
|
-
let branchesHit = 0, branchesFound = 0;
|
|
124
|
-
let functionsHit = 0, functionsFound = 0;
|
|
125
|
-
const flushRecord = () => {
|
|
126
|
-
if (currentFile) {
|
|
127
|
-
entries.push({
|
|
128
|
-
filePath: currentFile,
|
|
129
|
-
lines: linesFound > 0 ? (linesHit / linesFound) * 100 : 0,
|
|
130
|
-
branches: branchesFound > 0 ? (branchesHit / branchesFound) * 100 : 100,
|
|
131
|
-
functions: functionsFound > 0 ? (functionsHit / functionsFound) * 100 : 100,
|
|
132
|
-
statements: linesFound > 0 ? (linesHit / linesFound) * 100 : 0,
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
};
|
|
136
|
-
for (const line of raw.split('\n')) {
|
|
137
|
-
const trimmed = line.trim();
|
|
138
|
-
if (trimmed.startsWith('SF:')) {
|
|
139
|
-
currentFile = trimmed.slice(3);
|
|
140
|
-
linesHit = 0;
|
|
141
|
-
linesFound = 0;
|
|
142
|
-
branchesHit = 0;
|
|
143
|
-
branchesFound = 0;
|
|
144
|
-
functionsHit = 0;
|
|
145
|
-
functionsFound = 0;
|
|
146
|
-
}
|
|
147
|
-
else if (trimmed.startsWith('LH:')) {
|
|
148
|
-
linesHit = parseInt(trimmed.slice(3), 10) || 0;
|
|
149
|
-
}
|
|
150
|
-
else if (trimmed.startsWith('LF:')) {
|
|
151
|
-
linesFound = parseInt(trimmed.slice(3), 10) || 0;
|
|
152
|
-
}
|
|
153
|
-
else if (trimmed.startsWith('BRH:')) {
|
|
154
|
-
branchesHit = parseInt(trimmed.slice(4), 10) || 0;
|
|
155
|
-
}
|
|
156
|
-
else if (trimmed.startsWith('BRF:')) {
|
|
157
|
-
branchesFound = parseInt(trimmed.slice(4), 10) || 0;
|
|
158
|
-
}
|
|
159
|
-
else if (trimmed.startsWith('FNH:')) {
|
|
160
|
-
functionsHit = parseInt(trimmed.slice(4), 10) || 0;
|
|
161
|
-
}
|
|
162
|
-
else if (trimmed.startsWith('FNF:')) {
|
|
163
|
-
functionsFound = parseInt(trimmed.slice(4), 10) || 0;
|
|
164
|
-
}
|
|
165
|
-
else if (trimmed === 'end_of_record') {
|
|
166
|
-
flushRecord();
|
|
167
|
-
currentFile = '';
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
flushRecord();
|
|
171
|
-
entries.sort((a, b) => a.lines - b.lines);
|
|
172
|
-
let totalLH = 0, totalLF = 0, totalBH = 0, totalBF = 0;
|
|
173
|
-
for (const e of entries) {
|
|
174
|
-
// Approximate from percentages (we lost exact counts after flush, but summaries are okay)
|
|
175
|
-
totalLH += e.lines;
|
|
176
|
-
totalLF += 100;
|
|
177
|
-
totalBH += e.branches;
|
|
178
|
-
totalBF += 100;
|
|
179
|
-
}
|
|
180
|
-
const n = entries.length || 1;
|
|
181
|
-
return {
|
|
182
|
-
found: true,
|
|
183
|
-
source,
|
|
184
|
-
entries,
|
|
185
|
-
summary: {
|
|
186
|
-
totalFiles: entries.length,
|
|
187
|
-
overallLineCoverage: totalLH / n,
|
|
188
|
-
overallBranchCoverage: totalBH / n,
|
|
189
|
-
overallFunctionCoverage: 0,
|
|
190
|
-
overallStatementCoverage: totalLH / n,
|
|
191
|
-
},
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
23
|
/**
|
|
195
24
|
* Classify a coverage gap by priority type based on coverage percentage and threshold
|
|
196
25
|
*/
|
|
@@ -2996,7 +2825,7 @@ const coverageRouteCommand = {
|
|
|
2996
2825
|
'c8: npx c8 npm test',
|
|
2997
2826
|
]);
|
|
2998
2827
|
output.writeln();
|
|
2999
|
-
output.writeln(output.dim('Expected files: coverage/coverage-summary.json, coverage/lcov.info, or .nyc_output/out.json'));
|
|
2828
|
+
output.writeln(output.dim('Expected files: coverage/coverage-summary.json, coverage/lcov.info, coverage/coverage-final.json, or .nyc_output/out.json'));
|
|
3000
2829
|
return { success: false, exitCode: 1 };
|
|
3001
2830
|
}
|
|
3002
2831
|
}
|
|
@@ -3187,7 +3016,7 @@ const coverageSuggestCommand = {
|
|
|
3187
3016
|
'c8: npx c8 npm test',
|
|
3188
3017
|
]);
|
|
3189
3018
|
output.writeln();
|
|
3190
|
-
output.writeln(output.dim('Expected files: coverage/coverage-summary.json, coverage/lcov.info, or .nyc_output/out.json'));
|
|
3019
|
+
output.writeln(output.dim('Expected files: coverage/coverage-summary.json, coverage/lcov.info, coverage/coverage-final.json, or .nyc_output/out.json'));
|
|
3191
3020
|
return { success: false, exitCode: 1 };
|
|
3192
3021
|
}
|
|
3193
3022
|
}
|
|
@@ -3407,7 +3236,7 @@ const coverageGapsCommand = {
|
|
|
3407
3236
|
'c8: npx c8 npm test',
|
|
3408
3237
|
]);
|
|
3409
3238
|
output.writeln();
|
|
3410
|
-
output.writeln(output.dim('Expected files: coverage/coverage-summary.json, coverage/lcov.info, or .nyc_output/out.json'));
|
|
3239
|
+
output.writeln(output.dim('Expected files: coverage/coverage-summary.json, coverage/lcov.info, coverage/coverage-final.json, or .nyc_output/out.json'));
|
|
3411
3240
|
return { success: false, exitCode: 1 };
|
|
3412
3241
|
}
|
|
3413
3242
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* a one-line hint and exit 2, unless --plain asks for one frame as plain text and exit 0 (ADR-489 decision E).
|
|
10
10
|
*
|
|
11
11
|
* npx ruflo tui # interactive, in a terminal
|
|
12
|
-
* npx ruflo tui --view swarm # start on the Swarm page (keys 1-
|
|
12
|
+
* npx ruflo tui --view swarm # start on the Swarm page (keys 1-6 or o c s e m a switch pages)
|
|
13
13
|
* npx ruflo tui --plain | head # one plain frame, exit 0 (scripts, CI)
|
|
14
14
|
* npx ruflo tui | cat # not a terminal and no --plain: a hint on stderr, exit 2
|
|
15
15
|
*/
|
|
@@ -55,7 +55,7 @@ export const tuiCommand = {
|
|
|
55
55
|
],
|
|
56
56
|
examples: [
|
|
57
57
|
{ command: 'npx ruflo tui', description: 'Open the console in this terminal' },
|
|
58
|
-
{ command: 'npx ruflo tui --view swarm', description: 'Start on the Swarm page (keys 1-
|
|
58
|
+
{ command: 'npx ruflo tui --view swarm', description: 'Start on the Swarm page (keys 1-6 switch pages, tab cycles, q quits)' },
|
|
59
59
|
{ command: 'npx ruflo tui --view ai --model openai/gpt-4o-mini --max-spend-usd 0.1', description: 'The AI page on a cheap OpenRouter model, stopping at $0.10 reported (key in $OPENROUTER_API_KEY, never shown)' },
|
|
60
60
|
{ command: 'npx ruflo tui --plain | head -40', description: 'One plain frame, exit 0: for scripts and CI (no terminal needed)' },
|
|
61
61
|
],
|