* feat(code-review): add optional fallow structural pre-pass * fix(ci): sync lockfile for fallow optional binaries * fix(test): make fallow integration tests cross-platform * fix(review): require executable fallow binary paths * docs(review): clarify structural findings usage and size guard * fix(fallow): preserve line:0, prefer node_modules/.bin, sync SDK twin (H1, M2, N1 from #3424 review) * fix(workflow): harden fallow pre-pass — exit check, timeout, atomic write, size-guard order (B1, H2-H4, M1, M3 from #3424 review) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(deps): pin fallow floor to ^2.70.0 matching lockfile (H7 from #3424 review) * fix(config): enum-validate fallow.scope/profile + group code_quality.* contiguously (H5, N3 from #3424 review) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(fallow): label mcp gate reserved, version-pin install, expand context schema (B3, H8, M4, M8, L1 from #3424 review) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(fallow): replace source-grep with behavioral tests, expand fixtures, fail-loud tmpdir (B4, H6, L2, L3, M5, M6, N2 from #3424 review) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(workflow): escape closing structural_findings tag in JSON payload (CR #3424 inline finding) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
110 lines
3.0 KiB
JavaScript
110 lines
3.0 KiB
JavaScript
'use strict';
|
|
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
function candidateNames() {
|
|
return process.platform === 'win32'
|
|
? ['fallow.exe', 'fallow.cmd', 'fallow.bat', 'fallow']
|
|
: ['fallow'];
|
|
}
|
|
|
|
function isExecutableFile(filePath) {
|
|
try {
|
|
const stat = fs.statSync(filePath);
|
|
if (!stat.isFile()) return false;
|
|
if (process.platform === 'win32') return true;
|
|
fs.accessSync(filePath, fs.constants.X_OK);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function findInPath(envPath) {
|
|
if (!envPath) return null;
|
|
const names = candidateNames();
|
|
const segments = envPath.split(path.delimiter).filter(Boolean);
|
|
for (const segment of segments) {
|
|
for (const name of names) {
|
|
const candidate = path.join(segment, name);
|
|
if (isExecutableFile(candidate)) return candidate;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function findInNodeModules(cwd) {
|
|
const names = candidateNames();
|
|
const binDir = path.join(cwd, 'node_modules', '.bin');
|
|
for (const name of names) {
|
|
const candidate = path.join(binDir, name);
|
|
if (isExecutableFile(candidate)) return candidate;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function resolveFallowBinary({ cwd, envPath = process.env.PATH || '' }) {
|
|
return findInNodeModules(cwd) || findInPath(envPath) || null;
|
|
}
|
|
|
|
function requireFallowBinary({ cwd, envPath = process.env.PATH || '' }) {
|
|
const binary = resolveFallowBinary({ cwd, envPath });
|
|
if (binary) return binary;
|
|
throw new Error(
|
|
'Fallow is enabled but no binary was found. Please install fallow via `npm install -D fallow` or `cargo install fallow`.',
|
|
);
|
|
}
|
|
|
|
function normalizeFallowReport(report) {
|
|
const unused = Array.isArray(report?.unusedExports) ? report.unusedExports : [];
|
|
const duplicates = Array.isArray(report?.duplicates) ? report.duplicates : [];
|
|
const circular = Array.isArray(report?.circularDependencies) ? report.circularDependencies : [];
|
|
|
|
const findings = [];
|
|
|
|
for (const item of unused) {
|
|
findings.push({
|
|
type: 'unused_export',
|
|
message: `Unused export ${item.symbol || '<unknown>'}`,
|
|
file: item.file || '',
|
|
line: item.line ?? null,
|
|
});
|
|
}
|
|
|
|
for (const item of duplicates) {
|
|
findings.push({
|
|
type: 'duplicate_block',
|
|
message: `Duplicate block (${Math.round((item.similarity || 0) * 100)}% similarity)`,
|
|
file: item.left?.file || '',
|
|
line: item.left?.start ?? null,
|
|
related_file: item.right?.file || '',
|
|
});
|
|
}
|
|
|
|
for (const item of circular) {
|
|
findings.push({
|
|
type: 'circular_dependency',
|
|
message: `Circular dependency: ${(item.cycle || []).join(' -> ')}`,
|
|
file: Array.isArray(item.cycle) && item.cycle.length > 0 ? item.cycle[0] : '',
|
|
line: null,
|
|
});
|
|
}
|
|
|
|
return {
|
|
summary: {
|
|
unused_exports: unused.length,
|
|
duplicates: duplicates.length,
|
|
circular_dependencies: circular.length,
|
|
total: findings.length,
|
|
},
|
|
findings,
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
normalizeFallowReport,
|
|
requireFallowBinary,
|
|
resolveFallowBinary,
|
|
};
|