Files
msd-core/get-shit-done/bin/lib/fallow-runner.cjs
Tom Boucher 75d5ca5875 feat(code-review): integrate fallow structural pre-pass for /gsd-code-review (#3424)
* 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>
2026-05-13 21:19:47 -04:00

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,
};