diff --git a/CHANGELOG.md b/CHANGELOG.md index 911df4c..60a780f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 5e107fb..cac68fe 100644 --- a/README.md +++ b/README.md @@ -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 `](#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. + ![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) diff --git a/packages/action/dist/index.mjs b/packages/action/dist/index.mjs index 2e216b7..1769078 100644 --- a/packages/action/dist/index.mjs +++ b/packages/action/dist/index.mjs @@ -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 = []; diff --git a/packages/cli/src/cli-runner.ts b/packages/cli/src/cli-runner.ts index 92476e4..0f54a93 100644 --- a/packages/cli/src/cli-runner.ts +++ b/packages/cli/src/cli-runner.ts @@ -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 \"\" --explain \n"; + } + + return undefined; +} + export function parseArgs(args: string[]): CliOptions { const command = args[0] ?? ""; let issueText = ""; diff --git a/packages/cli/test/cli-runner.test.ts b/packages/cli/test/cli-runner.test.ts index 2adf431..fb61ef4 100644 --- a/packages/cli/test/cli-runner.test.ts +++ b/packages/cli/test/cli-runner.test.ts @@ -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 () => { diff --git a/packages/core/src/report.ts b/packages/core/src/report.ts index 7f8666d..9d501c3 100644 --- a/packages/core/src/report.ts +++ b/packages/core/src/report.ts @@ -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[] = []; diff --git a/packages/core/test/report.test.ts b/packages/core/test/report.test.ts index 1ac214c..8ec3294 100644 --- a/packages/core/test/report.test.ts +++ b/packages/core/test/report.test.ts @@ -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" }) + ); + }); });