* fix(#1012): invoke fallow with its real CLI and wire the report normalizer The /gsd-code-review structural pre-pass invoked fallow with flags no published fallow version accepts (--json, --profile, --stdin-files), so it failed on every run and degraded silently per REQ-FALLOW-02 — the feature never delivered on any fallow version. Three compounding defects: 1. Invalid flags. Real fallow audit uses --format json (not --json), -q/--quiet, --changed-since/--base for changed-files scoping (no file-list input), and --max-crap for thresholds. There is no --profile or --stdin-files. 2. Exit-code handling. fallow audit exits 1 when it FINDS issues (verdict=fail), 0 when clean. The pre-pass treated any non-zero exit as a crash and discarded the output — i.e. it threw away exactly the findings it exists to surface. Success is now decided by whether a valid fallow JSON report was produced, not by the exit code. 3. Schema mismatch. normalizeFallowReport parsed a fictional top-level schema (unusedExports/duplicates/circularDependencies) fallow never shipped, and was dead code (the workflow embedded raw JSON; its tests asserted the fictional schema, one even calling a non-existent runFallowAudit and passing vacuously). Fixes: align the invocation to fallow's documented agent-facing pattern; map the profile preset (minimal/standard/strict) to --max-crap (50/30/15); scope phase runs via --changed-since with a repo-scope fallback; rewrite the normalizer to fallow's real schema (dead_code.unused_exports/unused_files/circular_dependencies + duplication.clone_groups) and wire it into the workflow so the reviewer receives normalized findings; replace the fictional-schema fixtures and tests with real-schema ones and delete the vacuous runFallowAudit test. Closes #1012 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(#1012): backfill changeset PR number to 1044 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,9 @@
|
||||
* ADR-457 build-at-publish: the hand-written bin/lib/fallow-runner.cjs
|
||||
* collapsed to a TypeScript source of truth. Behaviour is preserved
|
||||
* byte-for-behaviour from the prior hand-written .cjs; only types are added.
|
||||
*
|
||||
* Parses the real fallow `audit --format json` schema (schema_version 3
|
||||
* envelope, nested dead_code/duplication sections). See fallow 2.70.0+.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
@@ -67,35 +70,79 @@ export function requireFallowBinary({ cwd, envPath = process.env['PATH'] ?? '' }
|
||||
);
|
||||
}
|
||||
|
||||
// --- Real fallow audit --format json schema (schema_version 3) interfaces ---
|
||||
|
||||
interface FallowUnusedExport {
|
||||
symbol?: string;
|
||||
file?: string;
|
||||
path?: string;
|
||||
export_name?: string;
|
||||
is_type_only?: boolean;
|
||||
line?: number | null;
|
||||
col?: number | null;
|
||||
span_start?: number | null;
|
||||
is_re_export?: boolean;
|
||||
actions?: unknown[];
|
||||
introduced?: boolean;
|
||||
}
|
||||
|
||||
interface FallowDuplicateItem {
|
||||
interface FallowUnusedFile {
|
||||
path?: string;
|
||||
actions?: unknown[];
|
||||
introduced?: boolean;
|
||||
}
|
||||
|
||||
interface FallowCircularDependency {
|
||||
files?: string[];
|
||||
length?: number;
|
||||
line?: number | null;
|
||||
col?: number | null;
|
||||
actions?: unknown[];
|
||||
introduced?: boolean;
|
||||
}
|
||||
|
||||
interface FallowCloneInstance {
|
||||
file?: string;
|
||||
start?: number | null;
|
||||
start_line?: number | null;
|
||||
end_line?: number | null;
|
||||
start_col?: number | null;
|
||||
end_col?: number | null;
|
||||
fragment?: string;
|
||||
}
|
||||
|
||||
interface FallowDuplicate {
|
||||
similarity?: number;
|
||||
left?: FallowDuplicateItem;
|
||||
right?: FallowDuplicateItem;
|
||||
interface FallowCloneGroup {
|
||||
instances?: FallowCloneInstance[];
|
||||
}
|
||||
|
||||
interface FallowCircular {
|
||||
cycle?: string[];
|
||||
interface FallowDeadCode {
|
||||
unused_exports?: FallowUnusedExport[];
|
||||
unused_files?: FallowUnusedFile[];
|
||||
circular_dependencies?: FallowCircularDependency[];
|
||||
summary?: unknown;
|
||||
schema_version?: number;
|
||||
}
|
||||
|
||||
interface FallowDuplication {
|
||||
clone_groups?: FallowCloneGroup[];
|
||||
stats?: unknown;
|
||||
}
|
||||
|
||||
interface FallowReport {
|
||||
unusedExports?: unknown[];
|
||||
duplicates?: unknown[];
|
||||
circularDependencies?: unknown[];
|
||||
schema_version?: number;
|
||||
version?: string;
|
||||
command?: string;
|
||||
verdict?: string;
|
||||
changed_files_count?: number;
|
||||
base_ref?: string;
|
||||
head_sha?: string;
|
||||
elapsed_ms?: number;
|
||||
summary?: unknown;
|
||||
attribution?: unknown;
|
||||
dead_code?: FallowDeadCode;
|
||||
duplication?: FallowDuplication;
|
||||
complexity?: unknown;
|
||||
}
|
||||
|
||||
export interface FallowFinding {
|
||||
type: 'unused_export' | 'duplicate_block' | 'circular_dependency';
|
||||
type: 'unused_export' | 'unused_file' | 'duplicate_block' | 'circular_dependency';
|
||||
message: string;
|
||||
file: string;
|
||||
line: number | null;
|
||||
@@ -105,6 +152,7 @@ export interface FallowFinding {
|
||||
export interface NormalizedFallowReport {
|
||||
summary: {
|
||||
unused_exports: number;
|
||||
unused_files: number;
|
||||
duplicates: number;
|
||||
circular_dependencies: number;
|
||||
total: number;
|
||||
@@ -113,53 +161,84 @@ export interface NormalizedFallowReport {
|
||||
}
|
||||
|
||||
export function normalizeFallowReport(report: FallowReport | null | undefined): NormalizedFallowReport {
|
||||
const unused: FallowUnusedExport[] = Array.isArray(report?.unusedExports)
|
||||
? (report.unusedExports as FallowUnusedExport[])
|
||||
: [];
|
||||
const duplicates: FallowDuplicate[] = Array.isArray(report?.duplicates)
|
||||
? (report.duplicates as FallowDuplicate[])
|
||||
: [];
|
||||
const circular: FallowCircular[] = Array.isArray(report?.circularDependencies)
|
||||
? (report.circularDependencies as FallowCircular[])
|
||||
: [];
|
||||
const deadCodeRaw = report?.dead_code;
|
||||
const duplicationRaw = report?.duplication;
|
||||
const unusedExports: FallowUnusedExport[] = (Array.isArray(deadCodeRaw?.unused_exports)
|
||||
? (deadCodeRaw?.unused_exports ?? [])
|
||||
: []).filter((x): x is FallowUnusedExport => x !== null && typeof x === 'object');
|
||||
const unusedFiles: FallowUnusedFile[] = (Array.isArray(deadCodeRaw?.unused_files)
|
||||
? (deadCodeRaw?.unused_files ?? [])
|
||||
: []).filter((x): x is FallowUnusedFile => x !== null && typeof x === 'object');
|
||||
const circularDeps: FallowCircularDependency[] = (Array.isArray(deadCodeRaw?.circular_dependencies)
|
||||
? (deadCodeRaw?.circular_dependencies ?? [])
|
||||
: []).filter((x): x is FallowCircularDependency => x !== null && typeof x === 'object');
|
||||
const cloneGroups: FallowCloneGroup[] = (Array.isArray(duplicationRaw?.clone_groups)
|
||||
? (duplicationRaw?.clone_groups ?? [])
|
||||
: []).filter((x): x is FallowCloneGroup => x !== null && typeof x === 'object');
|
||||
|
||||
const findings: FallowFinding[] = [];
|
||||
|
||||
for (const item of unused) {
|
||||
for (const item of unusedExports) {
|
||||
if (!item || typeof item !== 'object') continue;
|
||||
findings.push({
|
||||
type: 'unused_export',
|
||||
message: `Unused export ${item.symbol ?? '<unknown>'}`,
|
||||
file: item.file ?? '',
|
||||
message: `Unused export ${item.export_name ?? '<unknown>'}`,
|
||||
file: item.path ?? '',
|
||||
line: item.line ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
for (const item of duplicates) {
|
||||
for (const item of unusedFiles) {
|
||||
if (!item || typeof item !== 'object') continue;
|
||||
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 ?? '',
|
||||
type: 'unused_file',
|
||||
message: `Unused file ${item.path ?? '<unknown>'}`,
|
||||
file: item.path ?? '',
|
||||
line: null,
|
||||
});
|
||||
}
|
||||
|
||||
for (const item of circular) {
|
||||
for (const item of circularDeps) {
|
||||
if (!item || typeof item !== 'object') continue;
|
||||
const files = Array.isArray(item.files) ? item.files : [];
|
||||
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,
|
||||
message: `Circular dependency: ${files.join(' -> ')}`,
|
||||
file: files.length > 0 ? files[0] : '',
|
||||
line: item.line ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
for (const group of cloneGroups) {
|
||||
if (!group || typeof group !== 'object') continue;
|
||||
const instances = Array.isArray(group.instances) ? group.instances : [];
|
||||
findings.push({
|
||||
type: 'duplicate_block',
|
||||
message: `Duplicate block (${instances.length} instances)`,
|
||||
file: instances[0]?.file ?? '',
|
||||
line: instances[0]?.start_line ?? null,
|
||||
related_file: instances[1]?.file ?? '',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
summary: {
|
||||
unused_exports: unused.length,
|
||||
duplicates: duplicates.length,
|
||||
circular_dependencies: circular.length,
|
||||
unused_exports: unusedExports.length,
|
||||
unused_files: unusedFiles.length,
|
||||
duplicates: cloneGroups.length,
|
||||
circular_dependencies: circularDeps.length,
|
||||
total: findings.length,
|
||||
},
|
||||
findings,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeFallowReportFile(filePath: string): NormalizedFallowReport {
|
||||
try {
|
||||
const raw = fs.readFileSync(filePath, 'utf8');
|
||||
const parsed = JSON.parse(raw) as FallowReport;
|
||||
return normalizeFallowReport(parsed);
|
||||
} catch {
|
||||
return normalizeFallowReport(null);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user