Skip to content

Commit 4c586fb

Browse files
feat: add fixmap verify and release v0.7.3 (#99)
plan answers where to start. verify answers whether the change that followed matches the plan, by comparing a saved report against a real git diff. Five checks, all comparisons between two things the user already has, so nothing is executed and no repository code runs: - edits in generated or retired locations, which the next build discards - files the change needed that the plan never ranked - a leading file left untouched - source moving with no test moving, pointing at the routed test - risk areas the change reached that the plan never flagged Only the first exits non-zero. That one is wrong regardless of what the task was; the rest are advisory, because a plan can be wrong and a change can still be right, and only someone reading both can say which. The output reports the gap and leaves that judgement alone. Also fixes test routes carrying a repository-wide related-files list. Every route received the same set, so a report claimed npm --prefix packages/core run test would exercise another package's tests. Related files are scoped to the route's package; a root script keeps everything. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 1abcb23 commit 4c586fb

15 files changed

Lines changed: 461 additions & 24 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
All notable changes to FixMap are documented here.
44

5+
## 0.7.3 - 2026-07-26
6+
7+
### Added
8+
9+
- `fixmap verify --report <file> --diff <spec>` closes the loop after an edit. It compares a saved plan against the diff that followed and reports five things: edits in generated or retired locations that the next build discards, files the change needed that the plan never ranked, an untouched leading file, source moving with no test moving, and risk areas the plan never flagged. Both inputs are things the user already has, so nothing is executed and no repository code runs. Only a discarded edit exits non-zero — that one is wrong regardless of the task, while everything else is advisory, because a plan can be wrong and a change can still be right. Available as `verifyPlan` from the core package and with `--format json`.
10+
11+
### Fixed
12+
13+
- Test routes list only the tests each command can actually run. `findRelatedTests` ran once and its result was assigned to every route, so a report claimed `npm --prefix packages/core run test` would exercise `packages/action/test/runner.test.ts`, which that command never reaches. On this repository all three routes carried an identical eight files spanning three packages. Related files are now scoped to the route's package directory; a repository-root script keeps everything.
14+
515
## 0.7.2 - 2026-07-26
616

717
### Added

‎README.md‎

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,36 @@ or path from this file in the task to raise it.
9797

9898
It distinguishes the cases that actually differ: the file was ranked, it scored below the cutoff, it was deliberately excluded (a test, a lockfile, generated output whose source was ranked instead), or the scan never saw it. When a scan hits its file limit, it says so rather than implying the path does not exist. Add `--format json` for the machine-readable form.
9999

100+
### Verify the change afterwards
101+
102+
`plan` answers where to start. `verify` answers whether the change that followed matches the plan — by comparing the saved report against a real git diff:
103+
104+
```bash
105+
npx -y @aryam/fixmap@latest plan --issue "password reset emails fail" \
106+
--format json --output fixmap-report.json
107+
108+
# ...make the change...
109+
110+
npx -y @aryam/fixmap@latest verify --report fixmap-report.json --diff main...HEAD
111+
```
112+
113+
```text
114+
FixMap verified 3 changed files against the plan and raised 1 error and 2 warnings.
115+
116+
- **error** A file was edited in a generated or retired location. A build regenerates
117+
these, so the change will be lost. Edit the source they are produced from.
118+
- `dist/auth/reset-password.js`
119+
- **warning** One file changed that the plan did not rank. Either the task grew beyond
120+
the original description, or the ranking missed them — worth checking which.
121+
- `src/billing/charge.ts`
122+
- **warning** Code changed but no test did. The plan routed this test as most related.
123+
- `test/reset-password.test.ts`
124+
```
125+
126+
It checks five things: edits in generated or retired locations, files the change needed that the plan never ranked, an untouched leading file, source moving with no test moving, and risk areas the plan never flagged. Nothing is executed — both inputs are things you already have.
127+
128+
Only a discarded edit exits non-zero, because that one is wrong regardless of the task. Everything else is advisory: a plan can be wrong and a change can still be right, and FixMap reports the gap rather than judging it. `verify` is CLI-only for now; MCP and Action support follow.
129+
100130
### MCP server
101131

102132
FixMap exposes one stdio tool, `fixmap_plan`, so an agent can request the same report directly.
@@ -167,7 +197,7 @@ jobs:
167197
with:
168198
fetch-depth: 0
169199
- id: fixmap
170-
uses: aryamthecodebreaker/FixMap@v0.7.2
200+
uses: aryamthecodebreaker/FixMap@v0.7.3
171201
with:
172202
github-token: ${{ secrets.GITHUB_TOKEN }}
173203
```
@@ -247,11 +277,11 @@ Read the full [benchmark methodology and scanner measurements](docs/BENCHMARKS.m
247277
npm run evaluate:heldout
248278
```
249279

250-
## What changed in v0.7.2
280+
## What changed in v0.7.3
251281

252-
`--explain <path>` answers the question a ranked list cannot: why the file you expected is missing. It separates being ranked, scoring below the cutoff, being deliberately excluded, and never being scanned.
282+
`fixmap verify` closes the loop: it compares a saved plan against the diff that followed, flagging edits a build will discard, files the change needed that the plan never ranked, source moving without tests, and risk the plan never mentioned.
253283

254-
Every published rate now carries a confidence interval, because at twelve cases one result flipping moves Top-3 by eight points. And both evaluations report how often a wrong file ranks *first* while the right one sits lower — the regression suite's 100% Top-3 was concealing a 40% misleading rate, which is what an agent actually pays for.
284+
Test routes now list only the tests each command can actually run. Every route previously carried the same repository-wide list, so a report claimed `npm --prefix packages/core run test` would exercise another package's tests.
255285

256286
[Inspect the changelog](CHANGELOG.md) · [See the held-out results](benchmarks/heldout/README.md) · [See every regression ranking](benchmarks/external/README.md) · [Audit the efficiency assumptions](docs/BENCHMARKS.md)
257287

‎apps/web/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"typecheck": "tsc --noEmit"
1616
},
1717
"dependencies": {
18-
"@aryam/fixmap-core": "0.7.2",
18+
"@aryam/fixmap-core": "0.7.3",
1919
"next": "16.2.11",
2020
"react": "19.2.7",
2121
"react-dom": "19.2.7"

‎docs/LAUNCH_KIT.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ Marketplace: https://github.com/marketplace/actions/fixmap
207207

208208
npm: https://www.npmjs.com/package/@aryam/fixmap
209209

210-
Release: https://github.com/aryamthecodebreaker/FixMap/releases/tag/v0.7.2
210+
Release: https://github.com/aryamthecodebreaker/FixMap/releases/tag/v0.7.3
211211

212212
I would especially value feedback on the ranking explanations and which repository signals would make FixMap more useful before an agent starts editing.
213213

‎package-lock.json‎

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "fixmap-workspace",
3-
"version": "0.7.2",
3+
"version": "0.7.3",
44
"private": true,
55
"description": "Local-first repo context for coding agents: paste a GitHub issue URL to get ranked files, test routes, and risks.",
66
"license": "MIT",

‎packages/action/package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@fixmap/action",
3-
"version": "0.7.2",
3+
"version": "0.7.3",
44
"description": "GitHub Action wrapper for FixMap pull request reports.",
55
"private": true,
66
"license": "MIT",
@@ -11,6 +11,6 @@
1111
"typecheck": "tsc -p tsconfig.json --noEmit"
1212
},
1313
"dependencies": {
14-
"@aryam/fixmap-core": "0.7.2"
14+
"@aryam/fixmap-core": "0.7.3"
1515
}
1616
}

‎packages/cli/package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@aryam/fixmap",
3-
"version": "0.7.2",
3+
"version": "0.7.3",
44
"mcpName": "io.github.aryamthecodebreaker/fixmap",
55
"description": "Local-first CLI and MCP server mapping GitHub issue URLs, tasks, and diffs to ranked files, tests, and risks.",
66
"license": "MIT",
@@ -46,7 +46,7 @@
4646
"typecheck": "tsc -p tsconfig.json --noEmit"
4747
},
4848
"dependencies": {
49-
"@aryam/fixmap-core": "0.7.2",
49+
"@aryam/fixmap-core": "0.7.3",
5050
"@modelcontextprotocol/sdk": "1.29.0"
5151
},
5252
"engines": {

‎packages/cli/src/cli-runner.ts‎

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { fileURLToPath } from "node:url";
55
import {
66
explainFile,
77
renderJsonReport,
8+
renderVerifyMarkdown,
9+
verifyPlan,
810
renderMarkdownReport,
911
scanRepo,
1012
type FileExplanation,
@@ -22,6 +24,7 @@ export type CliOptions = {
2224
format: "markdown" | "json";
2325
output?: string | undefined;
2426
explainPath?: string | undefined;
27+
reportPath?: string | undefined;
2528
unknownArgs: string[];
2629
invalidValues: string[];
2730
};
@@ -43,10 +46,12 @@ Usage:
4346
fixmap plan --issue "Fix login" --repo https://github.com/owner/repository
4447
fixmap plan --diff main...HEAD
4548
fixmap plan --base main --head HEAD --format json
49+
fixmap verify --report fixmap-report.json --diff main...HEAD
4650
fixmap mcp
4751
4852
Commands:
4953
plan Generate a FixMap report for a task or diff
54+
verify Compare a saved report against the diff that followed it
5055
mcp Run FixMap as an MCP server over stdio for AI coding agents
5156
5257
Options:
@@ -58,6 +63,7 @@ Options:
5863
--format <fmt> Output format: markdown (default) or json
5964
--output <file> Write the report to a file instead of stdout
6065
--explain <path> Explain why one file was ranked where it was, or left out
66+
--report <file> Verify command only: the JSON report the change was planned from
6167
--help, -h Show this help
6268
--version, -v Show the FixMap version
6369
`;
@@ -88,7 +94,7 @@ export async function runCli(args: string[], dependencies: CliDependencies = {})
8894
}
8995

9096
const options = parseArgs(args);
91-
if (options.command !== "plan") {
97+
if (options.command !== "plan" && options.command !== "verify") {
9298
stderr(`Unknown command: ${options.command || "(none)"}\n\n${USAGE}`);
9399
return 1;
94100
}
@@ -104,6 +110,10 @@ export async function runCli(args: string[], dependencies: CliDependencies = {})
104110
stderr(`${sections.join("\n")}\n\n${USAGE}`);
105111
return 1;
106112
}
113+
if (options.command === "verify") {
114+
return runVerify(options, { stdout, stderr });
115+
}
116+
107117
if (!options.issueText && !options.diffSpec && !options.baseRef) {
108118
stderr("Provide --issue, --diff, or --base/--head so FixMap has a task signal.\n");
109119
return 1;
@@ -185,6 +195,7 @@ export function parseArgs(args: string[]): CliOptions {
185195
let format: "markdown" | "json" = "markdown";
186196
let output: string | undefined;
187197
let explainPath: string | undefined;
198+
let reportPath: string | undefined;
188199
const unknownArgs: string[] = [];
189200
const invalidValues: string[] = [];
190201

@@ -236,6 +247,10 @@ export function parseArgs(args: string[]): CliOptions {
236247
} else {
237248
invalidValues.push(`--format received ${JSON.stringify(value ?? "(missing)")}; expected "markdown" or "json"`);
238249
}
250+
} else if (arg === "--report") {
251+
consumeValue();
252+
if (value?.trim()) reportPath = value.trim();
253+
else invalidValues.push("--report requires a path to a FixMap JSON report");
239254
} else if (arg === "--explain") {
240255
consumeValue();
241256
if (value?.trim()) explainPath = value.trim();
@@ -259,6 +274,7 @@ export function parseArgs(args: string[]): CliOptions {
259274
format,
260275
output,
261276
explainPath,
277+
reportPath,
262278
unknownArgs,
263279
invalidValues
264280
};
@@ -271,6 +287,70 @@ function readVersion(): string {
271287
return packageJson.version;
272288
}
273289

290+
/**
291+
* Compares a saved plan against the diff that followed it. Both inputs are things the
292+
* user already has, so nothing is executed and no repository code runs.
293+
*/
294+
async function runVerify(
295+
options: CliOptions,
296+
io: { stdout: (text: string) => void; stderr: (text: string) => void }
297+
): Promise<number> {
298+
if (!options.reportPath) {
299+
io.stderr("Provide --report with the JSON report this change was planned from.\n");
300+
return 1;
301+
}
302+
if (!options.diffSpec && !options.baseRef) {
303+
io.stderr("Provide --diff or --base/--head so FixMap can see what changed.\n");
304+
return 1;
305+
}
306+
if (/^https?:\/\//i.test(options.repo ?? "")) {
307+
io.stderr("verify needs a local checkout; remote mode cannot resolve a diff.\n");
308+
return 1;
309+
}
310+
311+
let report: FixMapReport;
312+
try {
313+
report = JSON.parse(readFileSync(options.reportPath, "utf8")) as FixMapReport;
314+
} catch (error) {
315+
io.stderr(
316+
`Could not read "${options.reportPath}": ${error instanceof Error ? error.message : String(error)}\n` +
317+
"Generate one with: fixmap plan --issue \"...\" --format json --output fixmap-report.json\n"
318+
);
319+
return 1;
320+
}
321+
if (!Array.isArray(report.contextFiles)) {
322+
io.stderr(`"${options.reportPath}" is not a FixMap JSON report: no contextFiles array.\n`);
323+
return 1;
324+
}
325+
326+
try {
327+
const repo = await scanRepo({
328+
repoRoot: options.repo ?? process.cwd(),
329+
diffSpec: options.diffSpec,
330+
baseRef: options.baseRef,
331+
headRef: options.headRef
332+
});
333+
const unresolvedDiff = repo.diagnostics.find((diagnostic) => diagnostic.code === "diff-unavailable");
334+
if (unresolvedDiff) {
335+
io.stderr(`${unresolvedDiff.message}\nVerification needs a resolvable diff to compare against.\n`);
336+
return 1;
337+
}
338+
339+
const result = verifyPlan(report, repo);
340+
io.stdout(
341+
options.format === "json"
342+
? `${JSON.stringify(result, null, 2)}\n`
343+
: renderVerifyMarkdown(result)
344+
);
345+
// A generated-location edit is discarded by the next build, so it fails the command
346+
// rather than being reported and ignored. Everything else is advisory.
347+
return result.findings.some((finding) => finding.severity === "error") ? 1 : 0;
348+
} catch (error) {
349+
io.stderr(`${error instanceof Error ? error.message : String(error)}\n`);
350+
return 1;
351+
}
352+
}
353+
274354
/** Renders one file's explanation. The summary carries the answer; reasons show the working. */
275355
function renderExplanation(explanation: FileExplanation): string {
276356
const lines = [`# Why ${explanation.path}`, "", explanation.summary];

‎packages/core/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@aryam/fixmap-core",
3-
"version": "0.7.2",
3+
"version": "0.7.3",
44
"description": "Deterministic local-first repository scanner, context ranker, and report renderer for coding agents.",
55
"license": "MIT",
66
"repository": {

0 commit comments

Comments
 (0)