Skip to content

Commit bca5d17

Browse files
Merge pull request #23 from mvanhorn/feat/5-verbose-flag
feat: add --verbose flag to mex check
2 parents f74a3a1 + c81f5c5 commit bca5d17

5 files changed

Lines changed: 151 additions & 15 deletions

File tree

src/cli.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import chalk from "chalk";
22
import { Command } from "commander";
33
import { findConfig } from "./config.js";
4-
import { reportConsole, reportQuiet, reportJSON } from "./reporter.js";
4+
import { reportConsole, reportQuiet, reportJSON, reportVerbose } from "./reporter.js";
55

66
const program = new Command();
77

@@ -32,17 +32,19 @@ program
3232
.option("--json", "Output full drift report as JSON")
3333
.option("--quiet", "Single-line summary only")
3434
.option("--fix", "Run sync to fix any issues found")
35+
.option("--verbose", "Show detailed diagnostic output")
3536
.action(async (opts) => {
3637
try {
3738
const config = findConfig();
3839
const { runDriftCheck } = await import("./drift/index.js");
39-
const report = await runDriftCheck(config);
40+
const report = await runDriftCheck(config, { verbose: opts.verbose });
4041

4142
if (opts.json) {
42-
reportJSON(report);
43+
reportJSON(report, { verbose: opts.verbose });
4344
} else if (opts.quiet) {
4445
reportQuiet(report);
4546
} else {
47+
if (opts.verbose) reportVerbose(report);
4648
reportConsole(report);
4749
}
4850

src/drift/index.ts

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,17 @@ import { checkCrossFile } from "./checkers/cross-file.js";
1515
import { checkScriptCoverage } from "./checkers/script-coverage.js";
1616

1717
/** Run full drift detection across all scaffold files */
18-
export async function runDriftCheck(config: MexConfig): Promise<DriftReport> {
18+
export async function runDriftCheck(
19+
config: MexConfig,
20+
opts: { verbose?: boolean } = {}
21+
): Promise<DriftReport> {
1922
const { projectRoot, scaffoldRoot } = config;
2023

2124
// Find all markdown files in scaffold
2225
const scaffoldFiles = findScaffoldFiles(projectRoot, scaffoldRoot);
2326
const allClaims: Claim[] = [];
2427
const allIssues: DriftIssue[] = [];
28+
const checkerIssueCounts: Array<[string, number]> = [];
2529

2630
// Extract claims from all files
2731
for (const filePath of scaffoldFiles) {
@@ -36,34 +40,55 @@ export async function runDriftCheck(config: MexConfig): Promise<DriftReport> {
3640

3741
// Frontmatter edge check
3842
const frontmatter = parseFrontmatter(filePath);
39-
allIssues.push(
40-
...checkEdges(frontmatter, filePath, source, projectRoot, scaffoldRoot)
41-
);
43+
const edgeIssues = checkEdges(frontmatter, filePath, source, projectRoot, scaffoldRoot);
44+
allIssues.push(...edgeIssues);
4245

4346
// Staleness check
4447
const stalenessIssues = await checkStaleness(source, source, projectRoot);
4548
allIssues.push(...stalenessIssues);
49+
50+
checkerIssueCounts.push([`edges:${source}`, edgeIssues.length]);
51+
checkerIssueCounts.push([`staleness:${source}`, stalenessIssues.length]);
4652
}
4753

4854
// Run checkers that work on claims
49-
allIssues.push(...checkPaths(allClaims, projectRoot, scaffoldRoot));
50-
allIssues.push(...checkCommands(allClaims, projectRoot));
51-
allIssues.push(...checkDependencies(allClaims, projectRoot));
52-
allIssues.push(...checkCrossFile(allClaims));
55+
const pathIssues = checkPaths(allClaims, projectRoot, scaffoldRoot);
56+
allIssues.push(...pathIssues);
57+
checkerIssueCounts.push(["paths", pathIssues.length]);
58+
59+
const commandIssues = checkCommands(allClaims, projectRoot);
60+
allIssues.push(...commandIssues);
61+
checkerIssueCounts.push(["commands", commandIssues.length]);
62+
63+
const dependencyIssues = checkDependencies(allClaims, projectRoot);
64+
allIssues.push(...dependencyIssues);
65+
checkerIssueCounts.push(["dependencies", dependencyIssues.length]);
66+
67+
const crossFileIssues = checkCrossFile(allClaims);
68+
allIssues.push(...crossFileIssues);
69+
checkerIssueCounts.push(["cross-file", crossFileIssues.length]);
5370

5471
// Run structural checkers
55-
allIssues.push(...checkIndexSync(projectRoot, scaffoldRoot));
72+
const indexSyncIssues = checkIndexSync(projectRoot, scaffoldRoot);
73+
allIssues.push(...indexSyncIssues);
74+
checkerIssueCounts.push(["index-sync", indexSyncIssues.length]);
5675

5776
// Run coverage checkers (reality → scaffold direction)
58-
allIssues.push(...checkScriptCoverage(scaffoldFiles, projectRoot));
77+
const scriptCoverageIssues = checkScriptCoverage(scaffoldFiles, projectRoot);
78+
allIssues.push(...scriptCoverageIssues);
79+
checkerIssueCounts.push(["script-coverage", scriptCoverageIssues.length]);
5980

6081
const score = computeScore(allIssues);
82+
const verboseLog = opts.verbose
83+
? buildVerboseLog(scaffoldFiles.length, allClaims, checkerIssueCounts)
84+
: undefined;
6185

6286
return {
6387
score,
6488
issues: allIssues,
6589
filesChecked: scaffoldFiles.length,
6690
timestamp: new Date().toISOString(),
91+
verboseLog,
6792
};
6893
}
6994

@@ -108,3 +133,21 @@ function findScaffoldFiles(
108133
// Deduplicate
109134
return [...new Set(files)];
110135
}
136+
137+
export function buildVerboseLog(
138+
filesScanned: number,
139+
claims: Claim[],
140+
checkerIssueCounts: Array<[string, number]>
141+
): string[] {
142+
const pathClaims = claims.filter((claim) => claim.kind === "path").length;
143+
const commandClaims = claims.filter((claim) => claim.kind === "command").length;
144+
const dependencyClaims = claims.filter((claim) => claim.kind === "dependency").length;
145+
146+
return [
147+
`Scaffold files scanned: ${filesScanned}`,
148+
`Claims extracted: ${claims.length} (path: ${pathClaims}, command: ${commandClaims}, dependency: ${dependencyClaims})`,
149+
...checkerIssueCounts.map(
150+
([checker, count]) => `Checker ${checker}: ${count} issue${count === 1 ? "" : "s"}`
151+
),
152+
];
153+
}

src/reporter.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,18 @@ export function reportQuiet(report: DriftReport): void {
5656
console.log(`mex: drift score ${color(`${report.score}/100`)}${detail}`);
5757
}
5858

59-
export function reportJSON(report: DriftReport): void {
60-
console.log(JSON.stringify(report, null, 2));
59+
export function reportJSON(report: DriftReport, opts?: { verbose?: boolean }): void {
60+
const output = opts?.verbose ? report : { ...report, verboseLog: undefined };
61+
console.log(JSON.stringify(output, null, 2));
62+
}
63+
64+
export function reportVerbose(report: DriftReport): void {
65+
if (!report.verboseLog?.length) return;
66+
console.log(chalk.dim("── Verbose ──"));
67+
for (const line of report.verboseLog) {
68+
console.log(chalk.dim(` ${line}`));
69+
}
70+
console.log();
6171
}
6272

6373
function printSummary(report: DriftReport): void {

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export interface DriftReport {
5757
issues: DriftIssue[];
5858
filesChecked: number;
5959
timestamp: string;
60+
verboseLog?: string[];
6061
}
6162

6263
// ── Frontmatter ──

test/verbose.test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { describe, it, expect, vi } from "vitest";
2+
import { buildVerboseLog } from "../src/drift/index.js";
3+
import { reportJSON } from "../src/reporter.js";
4+
import type { Claim, DriftReport } from "../src/types.js";
5+
6+
function makeClaim(kind: Claim["kind"]): Claim {
7+
return {
8+
kind,
9+
value: "test-value",
10+
file: "test.md",
11+
line: 1,
12+
raw: "test raw",
13+
};
14+
}
15+
16+
function makeReport(opts?: { verboseLog?: string[] }): DriftReport {
17+
return {
18+
score: 85,
19+
issues: [],
20+
filesChecked: 3,
21+
timestamp: "2026-04-10T00:00:00.000Z",
22+
verboseLog: opts?.verboseLog,
23+
};
24+
}
25+
26+
describe("buildVerboseLog", () => {
27+
it("returns file count and claim breakdown", () => {
28+
const claims: Claim[] = [
29+
makeClaim("path"),
30+
makeClaim("path"),
31+
makeClaim("command"),
32+
makeClaim("dependency"),
33+
];
34+
const checkerCounts: Array<[string, number]> = [
35+
["path", 1],
36+
["edges", 0],
37+
];
38+
39+
const log = buildVerboseLog(5, claims, checkerCounts);
40+
41+
expect(log[0]).toBe("Scaffold files scanned: 5");
42+
expect(log[1]).toContain("Claims extracted: 4");
43+
expect(log[1]).toContain("path: 2");
44+
expect(log[1]).toContain("command: 1");
45+
expect(log[1]).toContain("dependency: 1");
46+
expect(log[2]).toBe("Checker path: 1 issue");
47+
expect(log[3]).toBe("Checker edges: 0 issues");
48+
});
49+
50+
it("handles empty claims and checkers", () => {
51+
const log = buildVerboseLog(0, [], []);
52+
expect(log).toHaveLength(2);
53+
expect(log[0]).toBe("Scaffold files scanned: 0");
54+
expect(log[1]).toContain("Claims extracted: 0");
55+
});
56+
});
57+
58+
describe("reportJSON verbose gating", () => {
59+
it("excludes verboseLog from JSON when verbose is off", () => {
60+
const report = makeReport({ verboseLog: ["line1", "line2"] });
61+
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
62+
63+
reportJSON(report);
64+
65+
const output = JSON.parse(spy.mock.calls[0][0]);
66+
expect(output.verboseLog).toBeUndefined();
67+
spy.mockRestore();
68+
});
69+
70+
it("includes verboseLog in JSON when verbose is on", () => {
71+
const report = makeReport({ verboseLog: ["line1", "line2"] });
72+
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
73+
74+
reportJSON(report, { verbose: true });
75+
76+
const output = JSON.parse(spy.mock.calls[0][0]);
77+
expect(output.verboseLog).toEqual(["line1", "line2"]);
78+
spy.mockRestore();
79+
});
80+
});

0 commit comments

Comments
 (0)