Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

All notable changes to FixMap are documented here.

## Unreleased

### Fixed

- Risk notes are no longer derived from demo code. Express ships `examples/auth/`, which the ranker already deprioritizes as example code — but the risk map still read it, turning a low-confidence demo file into "**high** authentication: authentication-related files are affected" on a task about request body parsing. A risk derived from evidence the ranking itself discounted is precisely the confident-but-wrong output the diagnostics exist to prevent. Changed files still count wherever they live, because a diff is fact rather than a guess.

### Added

- The CLI now points at the next useful command. Saving a JSON plan suggests the `verify` invocation for it; a weak, empty, or tightly clustered ranking suggests `--explain`. Hints go to stderr, so reports piped to a file, parsed as JSON, or posted by the Action are unchanged, and only one appears per run.

## 0.7.3 - 2026-07-26

### Added
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ npx -y @aryam/fixmap@latest plan --issue https://github.com/aryamthecodebreaker/

No clone, signup, configuration, or source upload is required.

| Command | Answers |
| --- | --- |
| [`fixmap plan`](#cli) | Which files, tests, and risks should I look at first? |
| [`fixmap plan --explain <path>`](#ask-why) | Why is the file I expected *not* in that list? |
| [`fixmap verify`](#verify-the-change-afterwards) | Did the change I made match the plan? |
| [`fixmap mcp`](#mcp-server) | The same report, requested directly by an agent |

The CLI points at the next useful command as you go, so `--explain` and `verify` surface when they apply rather than only living here.

<!-- Reproducible recording: regenerate with `npm run build:cli && node scripts/render-demo.mjs` -->
![Animated FixMap terminal recording: one command produces ranked context files with confidence and reasons, a related test route, a high authentication risk note, and honest diagnostics.](docs/assets/fixmap-cli-demo.svg)

Expand Down
6 changes: 5 additions & 1 deletion packages/action/dist/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1090,8 +1090,12 @@ var RISK_RULES = [
{ area: "public-api", severity: "medium", tokens: ["api", "route", "public"], reason: "public interfaces or request handling may change" },
{ area: "dependencies", severity: "medium", tokens: ["dependency", "lock", "package"], reason: "dependency changes can affect build and supply-chain behavior" }
];
var AUXILIARY_RISK_DIRS = /* @__PURE__ */ new Set(["demo", "demos", "example", "examples", "sample", "samples", "fixture", "fixtures"]);
function carriesRiskEvidence(path) {
return !path.split("/").slice(0, -1).some((segment) => AUXILIARY_RISK_DIRS.has(segment.toLowerCase()));
}
function buildRiskNotes(contextPaths, changedFiles = []) {
const contextTokens = new Set(contextPaths.flatMap((path) => [...tokenizePath(path)]));
const contextTokens = new Set(contextPaths.filter(carriesRiskEvidence).flatMap((path) => [...tokenizePath(path)]));
const changedTokens = new Set(changedFiles.flatMap((path) => [...tokenizePath(path)]));
const diffPresent = changedFiles.length > 0;
const risks = [];
Expand Down
31 changes: 31 additions & 0 deletions packages/cli/src/cli-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,40 @@ export async function runCli(args: string[], dependencies: CliDependencies = {})
stdout(rendered);
}

const hint = nextCommandHint(options, report);
if (hint) {
stderr(hint);
}

return 0;
}

/**
* Names the command that helps next, on stderr so the report itself stays clean for
* pipes, files, JSON consumers, and the Action comment.
*
* Only one hint, only when it applies. A feature nobody discovers may as well not
* exist, but a banner on every run is noise — so this speaks when the situation makes
* the suggestion obviously useful and stays quiet otherwise.
*/
function nextCommandHint(options: CliOptions, report: FixMapReport): string | undefined {
if (options.output && options.format === "json") {
const diff = options.diffSpec ?? "main...HEAD";
return `\nAfter you make the change, check it against this plan:\n fixmap verify --report ${options.output} --diff ${diff}\n`;
}

const leading = report.contextFiles[0];
const weak =
report.contextFiles.length === 0 ||
leading?.confidence === "low" ||
report.analysis?.ranking.clustered === true;
if (weak) {
return "\nExpected a file that is not listed? Ask why it was left out:\n fixmap plan --issue \"<same task>\" --explain <path>\n";
}

return undefined;
}

export function parseArgs(args: string[]): CliOptions {
const command = args[0] ?? "";
let issueText = "";
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/test/cli-runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,10 @@ describe("CLI argument handling", () => {
expect(exitCode).toBe(0);
expect(buildReport).toHaveBeenCalledWith(expect.objectContaining({ issueText: "reset fails" }));
expect(writeReport).toHaveBeenCalledWith("report.json", expect.stringContaining('"contextFiles"'));
// The report itself never reaches stdout when written to a file. Saving a JSON plan
// does name the command that consumes it, on stderr so the artifact stays clean.
expect(io.stdout).toEqual([]);
expect(io.stderr).toEqual([]);
expect(io.stderr.join("")).toContain("fixmap verify --report report.json");
});

it("requires a task signal before invoking the report builder", async () => {
Expand Down
16 changes: 15 additions & 1 deletion packages/core/src/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,22 @@ const RISK_RULES: { area: string; severity: RiskNote["severity"]; tokens: string
{ area: "dependencies", severity: "medium", tokens: ["dependency", "lock", "package"], reason: "dependency changes can affect build and supply-chain behavior" }
];

// Demo code names sensitive areas without touching them. Express ships examples/auth/,
// which the ranker already deprioritizes as demo code — but reading it for risk turned a
// low-confidence example into "high: authentication-related files are affected" on a task
// about request parsing. A risk note derived from evidence the ranking itself discounted
// is exactly the confident-but-wrong output the diagnostics exist to prevent. A changed
// file is different: a diff is fact, so it still counts wherever it lives.
const AUXILIARY_RISK_DIRS = new Set(["demo", "demos", "example", "examples", "sample", "samples", "fixture", "fixtures"]);

function carriesRiskEvidence(path: string): boolean {
return !path.split("/").slice(0, -1).some((segment) => AUXILIARY_RISK_DIRS.has(segment.toLowerCase()));
}

export function buildRiskNotes(contextPaths: string[], changedFiles: string[] = []): RiskNote[] {
const contextTokens = new Set(contextPaths.flatMap((path) => [...tokenizePath(path)]));
const contextTokens = new Set(
contextPaths.filter(carriesRiskEvidence).flatMap((path) => [...tokenizePath(path)])
);
const changedTokens = new Set(changedFiles.flatMap((path) => [...tokenizePath(path)]));
const diffPresent = changedFiles.length > 0;
const risks: RiskNote[] = [];
Expand Down
11 changes: 11 additions & 0 deletions packages/core/test/report.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,4 +193,15 @@ describe("report rendering", () => {
expect(routes[0]?.command).toBe("npm run test");
expect(routes[0]?.relatedFiles).toContain("packages/core/test/rank.test.ts");
});
it("ignores demo code when deriving risk, but trusts a changed file anywhere", () => {
// Express ships examples/auth/. Reading it for risk turned a deprioritized demo
// file into a high-severity authentication note on a request-parsing task.
expect(buildRiskNotes(["examples/auth/index.js"], [])).toEqual([]);
expect(buildRiskNotes(["src/auth/login.ts"], [])).toContainEqual(
expect.objectContaining({ area: "authentication", severity: "high" })
);
expect(buildRiskNotes([], ["examples/auth/index.js"])).toContainEqual(
expect.objectContaining({ area: "authentication" })
);
});
});