diff --git a/apps/web/app/_components/site-footer.tsx b/apps/web/app/_components/site-footer.tsx
index c71cc1b..1feafcf 100644
--- a/apps/web/app/_components/site-footer.tsx
+++ b/apps/web/app/_components/site-footer.tsx
@@ -12,7 +12,7 @@ export function SiteFooter() {
A practical map before the first edit. Free, local-first, and open source.
-
ExploreProductLive demoEvidence
+
ExploreProductLive demoEvidenceChangelog
diff --git a/apps/web/app/_components/site-header.tsx b/apps/web/app/_components/site-header.tsx
index 28ac016..1706bde 100644
--- a/apps/web/app/_components/site-header.tsx
+++ b/apps/web/app/_components/site-header.tsx
@@ -7,6 +7,7 @@ const links = [
{ href: "/product", label: "Product" },
{ href: "/demo", label: "Live demo" },
{ href: "/evidence", label: "Evidence" },
+ { href: "/changelog", label: "Changelog" },
{ href: "/docs", label: "Docs" }
];
diff --git a/apps/web/app/changelog/page.tsx b/apps/web/app/changelog/page.tsx
new file mode 100644
index 0000000..abade12
--- /dev/null
+++ b/apps/web/app/changelog/page.tsx
@@ -0,0 +1,261 @@
+import type { Metadata } from "next";
+import {
+ ArrowRight,
+ ChartLineUp,
+ CheckCircle,
+ Plus,
+ ShieldCheck,
+ Sparkle,
+ Wrench
+} from "@phosphor-icons/react/ssr";
+import { repoUrl, siteStats } from "../_lib/site-data";
+
+export const metadata: Metadata = {
+ title: "Changelog",
+ description: "See what is new, improved, and fixed in every recent FixMap release.",
+ alternates: { canonical: "/changelog" }
+};
+
+type ChangeGroup = {
+ label: "Added" | "Fixed" | "Improved" | "Security" | "Evidence";
+ items: string[];
+};
+
+type Release = {
+ version: string;
+ date: string;
+ label?: string;
+ summary: string;
+ groups: ChangeGroup[];
+};
+
+const releases: Release[] = [
+ {
+ version: "Unreleased",
+ date: "Pending release",
+ label: "In progress",
+ summary: "Improvements that are tested and ready for the next package release.",
+ groups: [
+ {
+ label: "Fixed",
+ items: [
+ "Pretty-printed vendored dependency bundles with a source-map marker no longer escape generated-output detection or rank as a high-confidence edit target."
+ ]
+ },
+ {
+ label: "Evidence",
+ items: [
+ "Added a dedicated adversarial fixture for readable compiled dependencies while keeping Chalk's real vendored implementation ranked first.",
+ "All 420 workspace tests pass; held-out and external accuracy remain unchanged, and the adversarial suite is now 9/9 with zero false-confidence cases."
+ ]
+ }
+ ]
+ },
+ {
+ version: "0.8.6",
+ date: "August 2, 2026",
+ label: "Latest release",
+ summary: "Cleaner implementation rankings without hiding legitimate UI or generated-artifact work.",
+ groups: [
+ {
+ label: "Fixed",
+ items: [
+ "Stylesheets are deprioritized for non-UI implementation tasks, while genuine CSS and layout tasks still rank them normally.",
+ "Explicitly named generated artifacts remain visible, but a maintained source twin caps their confidence and explains the source relationship."
+ ]
+ },
+ {
+ label: "Evidence",
+ items: [
+ "Added counterexample coverage for real CSS tasks and tasks that genuinely target a stale generated artifact.",
+ "Held-out accuracy remained 7/12 Top-1, 8/12 Top-3, and 9/12 Top-5."
+ ]
+ }
+ ]
+ },
+ {
+ version: "0.8.5",
+ date: "August 2, 2026",
+ summary: "A proper installation path and a much shorter everyday command.",
+ groups: [
+ {
+ label: "Improved",
+ items: [
+ "The README and website now lead with a global installation followed by the short fixmap command.",
+ "Install guidance now explains when npm may prefer an older project-local binary and why the running version printed by Doctor is authoritative."
+ ]
+ }
+ ]
+ },
+ {
+ version: "0.8.4",
+ date: "August 2, 2026",
+ summary: "Doctor can identify an exact-version request that started the wrong installation.",
+ groups: [
+ {
+ label: "Fixed",
+ items: [
+ "fixmap doctor reports both the requested and running versions when a local or ancestor installation shadows an exact npm request.",
+ "The clean verification procedure uses an isolated npm prefix and invokes its shim directly on Windows."
+ ]
+ }
+ ]
+ },
+ {
+ version: "0.8.3",
+ date: "August 2, 2026",
+ summary: "Stricter MCP comparison inputs, with honest failures instead of false success.",
+ groups: [
+ {
+ label: "Fixed",
+ items: [
+ "fixmap_compare rejects truncated report-shaped objects while continuing to accept complete reports that legitimately contain zero matches.",
+ "Optional rank, score, and confidence fields are type-checked when present."
+ ]
+ }
+ ]
+ },
+ {
+ version: "0.8.2",
+ date: "August 2, 2026",
+ summary: "A broad reliability release across Windows, test routing, MCP, the Action, and diagnostics.",
+ groups: [
+ {
+ label: "Added",
+ items: [
+ "New diagnostics identify unread content, missing tracked paths, duplicate real paths, generated-path dominance, and missing related tests.",
+ "MCP explain gained working-tree controls, the product page gained Compare, and the site gained robots.txt."
+ ]
+ },
+ {
+ label: "Fixed",
+ items: [
+ "Windows path normalization, BOM and UTF-16 manifests, common GitHub URL forms, failed diff handling, and additional source languages now behave consistently.",
+ "Workspace test routing, Action exclusions and comments, and stemming were hardened with regression coverage."
+ ]
+ }
+ ]
+ },
+ {
+ version: "0.8.1",
+ date: "August 1, 2026",
+ summary: "The first large dogfooding sweep closed gaps across every FixMap surface.",
+ groups: [
+ {
+ label: "Added",
+ items: [
+ "Compare and Doctor MCP tools, working-tree controls, browser-safe exports, and Action inputs for limit, exclude, and untracked files.",
+ "Release gates now verify the npm latest tag, canonical homepage metadata, internal versions, and a clean installation before publishing."
+ ]
+ },
+ {
+ label: "Fixed",
+ items: [
+ "CLI validation, comparison output, exclusions, confidence, risk evidence, JSON ranks, test routing, and Action comment selection."
+ ]
+ }
+ ]
+ },
+ {
+ version: "0.8.0",
+ date: "August 1, 2026",
+ summary: "Plan, focus, compare, and verify became one coherent workflow.",
+ groups: [
+ {
+ label: "Added",
+ items: [
+ "Go and Rust test routing, fixmap doctor, plan comparison, exclusions, result limits, working-tree mode, progress phases, pull-request URLs, and Action verify mode.",
+ "MCP explain lets agents investigate a missing file without shell access."
+ ]
+ },
+ {
+ label: "Fixed",
+ items: [
+ "Confidence now reflects real ranking separation instead of labeling a whole result page high.",
+ "Language detection, diagnostic bounds, file-mention performance, duplicate flags, verify output, and report consistency were corrected."
+ ]
+ },
+ {
+ label: "Security",
+ items: [
+ "Unbounded user text no longer flows into JSON reports, CI logs, or pull-request comments."
+ ]
+ }
+ ]
+ }
+];
+
+const icons = {
+ Added: Plus,
+ Fixed: Wrench,
+ Improved: Sparkle,
+ Security: ShieldCheck,
+ Evidence: ChartLineUp
+} as const;
+
+const releaseId = (version: string) => `v-${version.toLowerCase().replaceAll(".", "-")}`;
+
+export default function ChangelogPage() {
+ return (
+
+
+ Product changelog
+ Every improvement.
Plainly recorded.
+ New features, important fixes, and the evidence behind each recent FixMap release.
+
+
+
+
+
+
+
+ {releases.map((release) => (
+
+
+
+
{release.date}
+
{release.version === "Unreleased" ? "Coming next" : `FixMap v${release.version}`}
+
+ {release.label ? {release.label} : null}
+
+ {release.summary}
+
+ {release.groups.map((group) => {
+ const Icon = icons[group.label];
+ return (
+
+ {group.label}
+
+ {group.items.map((item) => - {item}
)}
+
+
+ );
+ })}
+
+
+ ))}
+
+
+
+
+
+
Nothing hidden
Every release stays inspectable.
The repository contains the full history, exact test evidence, and source for every change.
+
Browse GitHub releases
+
+
+
+ );
+}
diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css
index 852ee3c..cc9ef71 100644
--- a/apps/web/app/globals.css
+++ b/apps/web/app/globals.css
@@ -313,6 +313,40 @@ h3 { margin-bottom: 10px; font-size: 21px; line-height: 1.25; letter-spacing: -.
.docs-bottom > div:first-child p { color: #c2ccd4; }
.not-found { min-height: 630px; display: flex; flex-direction: column; justify-content: center; align-items: flex-start; }
+/* Changelog */
+.changelog-hero { padding-bottom: 84px; }
+.changelog-hero > p:not(.eyebrow) { max-width: 680px; }
+.changelog-hero-meta { margin-top: 34px; display: flex; flex-wrap: wrap; align-items: center; gap: 18px 32px; }
+.changelog-hero-meta > span { display: inline-flex; align-items: center; gap: 8px; color: var(--green-dark); font-size: 14px; font-weight: 650; }
+.changelog-hero-meta > span svg { color: var(--green); }
+.changelog-layout { padding-top: 90px; display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: clamp(60px, 8vw, 120px); align-items: start; }
+.release-index { position: sticky; top: 116px; padding: 22px; border: 1px solid var(--line); border-radius: 12px; background: rgba(255,253,248,.72); }
+.release-index > strong { color: var(--ink-faint); font: 650 11px var(--font-mono), monospace; text-transform: uppercase; letter-spacing: .1em; }
+.release-index nav { margin-top: 15px; display: grid; }
+.release-index a { padding: 11px 0; display: grid; gap: 2px; border-top: 1px solid var(--line); color: var(--ink); text-decoration: none; }
+.release-index a:hover span { color: var(--green); }
+.release-index span { font-size: 14px; font-weight: 650; transition: color .18s ease; }
+.release-index small { color: var(--ink-faint); font-size: 11px; }
+.release-list { position: relative; }
+.release-list::before { content: ""; position: absolute; left: 7px; top: 9px; bottom: 72px; width: 1px; background: var(--line-strong); }
+.release-entry { position: relative; padding: 0 0 86px 52px; scroll-margin-top: 120px; }
+.release-entry::before { content: ""; position: absolute; left: 0; top: 8px; width: 15px; height: 15px; border: 3px solid var(--paper); border-radius: 50%; background: var(--green); box-shadow: 0 0 0 1px var(--green); }
+.release-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 28px; }
+.release-heading p { margin: 0 0 8px; color: var(--ink-faint); font: 600 11px var(--font-mono), monospace; text-transform: uppercase; letter-spacing: .08em; }
+.release-heading h2 { font-size: clamp(34px, 4vw, 48px); }
+.release-heading > span { flex: 0 0 auto; padding: 7px 10px; border: 1px solid rgba(10,90,67,.18); border-radius: 999px; background: var(--mint-soft); color: var(--green-dark); font: 650 10px var(--font-mono), monospace; text-transform: uppercase; letter-spacing: .06em; }
+.release-summary { max-width: 720px; margin-top: 18px; color: var(--ink-soft); font-size: 18px; }
+.release-groups { margin-top: 36px; border-bottom: 1px solid var(--line); }
+.release-group { padding: 25px 0; display: grid; grid-template-columns: 145px minmax(0, 1fr); gap: 32px; border-top: 1px solid var(--line); }
+.release-group h3 { display: flex; align-items: center; gap: 9px; color: var(--green-dark); font-size: 14px; }
+.release-group h3 svg { color: var(--green); }
+.release-group ul { margin: 0; padding: 0; display: grid; gap: 13px; list-style: none; }
+.release-group li { position: relative; padding-left: 19px; color: var(--ink-soft); line-height: 1.65; }
+.release-group li::before { content: ""; position: absolute; left: 0; top: .72em; width: 6px; height: 6px; border-radius: 50%; background: var(--mint); }
+.changelog-bottom { display: flex; align-items: center; justify-content: space-between; gap: 60px; }
+.changelog-bottom > div { max-width: 690px; }
+.changelog-bottom > div > p:last-child { color: #b7c5d1; }
+
/* Footer */
.site-footer { padding: 72px max(32px, calc((100vw - var(--page)) / 2)); background: var(--navy); color: var(--paper-light); }
.footer-main { display: grid; grid-template-columns: 1fr 1.2fr; gap: 100px; }
@@ -370,6 +404,10 @@ h3 { margin-bottom: 10px; font-size: 21px; line-height: 1.25; letter-spacing: -.
.setup-section { grid-template-columns: minmax(0, 1fr); gap: 34px; }
.docs-layout { grid-template-columns: 1fr; }
.docs-sidebar { display: none; }
+ .changelog-layout { grid-template-columns: 1fr; gap: 55px; }
+ .release-index { position: static; }
+ .release-index nav { grid-template-columns: repeat(4, 1fr); gap: 0 18px; }
+ .changelog-bottom { align-items: flex-start; flex-direction: column; }
.footer-main { grid-template-columns: 1fr; gap: 54px; }
}
@@ -417,6 +455,11 @@ h3 { margin-bottom: 10px; font-size: 21px; line-height: 1.25; letter-spacing: -.
.copy-command button { width: 100%; }
.docs-cards { grid-template-columns: 1fr; }
.definition-list > div { grid-template-columns: 1fr; gap: 6px; }
+ .changelog-hero-meta { align-items: flex-start; flex-direction: column; }
+ .release-index nav { grid-template-columns: 1fr 1fr; }
+ .release-entry { padding-left: 32px; }
+ .release-heading { flex-direction: column; gap: 14px; }
+ .release-group { grid-template-columns: 1fr; gap: 16px; }
.setup-next { align-items: flex-start; flex-direction: column; }
.footer-links { grid-template-columns: 1fr 1fr; }
.footer-bottom { flex-direction: column; gap: 8px; }
diff --git a/apps/web/app/sitemap.ts b/apps/web/app/sitemap.ts
index 61f3a79..4a93a4b 100644
--- a/apps/web/app/sitemap.ts
+++ b/apps/web/app/sitemap.ts
@@ -3,7 +3,7 @@ import type { MetadataRoute } from "next";
const base = "https://usefixmap.vercel.app";
export default function sitemap(): MetadataRoute.Sitemap {
- return ["", "/product", "/demo", "/evidence", "/get-started", "/docs"].map((path) => ({
+ return ["", "/product", "/demo", "/evidence", "/changelog", "/get-started", "/docs"].map((path) => ({
url: `${base}${path}`,
changeFrequency: path === "" ? "weekly" : "monthly",
priority: path === "" ? 1 : path === "/demo" || path === "/get-started" ? 0.9 : 0.8
diff --git a/benchmarks/adversarial/README.md b/benchmarks/adversarial/README.md
index 17028f9..1508566 100644
--- a/benchmarks/adversarial/README.md
+++ b/benchmarks/adversarial/README.md
@@ -10,11 +10,11 @@ A wrong answer costs an agent one wasted file read. A wrong answer delivered wit
| Result | Value |
| --- | ---: |
-| Cases | 8 |
-| Passed | 8 |
+| Cases | 9 |
+| Passed | 9 |
| **False-confidence rate** | **0.0** |
-Measured 2026-07-26. Per-case output is checked in at [`results.json`](results.json).
+Measured 2026-08-02. Per-case output is checked in at [`results.json`](results.json).
## The cases
@@ -28,8 +28,9 @@ Measured 2026-07-26. Per-case output is checked in at [`results.json`](results.j
| `absent-feature-surface` | chalk | A CLI flag for a package that exposes no CLI |
| `runtime-only-symptom` | pino | A timing-dependent concurrency symptom with no static trace |
| `documentation-term-flood` | webpack | Only broad development vocabulary that instruction and documentation files carry in volume |
+| `pretty-printed-vendored-bundle` | Synthetic compiled dependency | A readable development bundle with one source-map marker and no maintained source twin |
-Cases reuse the checkouts already pinned by the accuracy suites, so this suite clones nothing new.
+External cases reuse the checkouts already pinned by the accuracy suites, so this suite clones nothing new. The generated-output case uses a deterministic temporary fixture and removes it after every run.
## Why this is not only unit tests
diff --git a/benchmarks/adversarial/dataset.json b/benchmarks/adversarial/dataset.json
index 9c0ced5..7822b31 100644
--- a/benchmarks/adversarial/dataset.json
+++ b/benchmarks/adversarial/dataset.json
@@ -129,6 +129,14 @@
"descriptive",
"vague"
]
+ },
+ {
+ "id": "pretty-printed-vendored-bundle",
+ "kind": "generated-output",
+ "fixture": "pretty-printed-vendored-bundle",
+ "task": "experimental transition state",
+ "why": "A readable development bundle under compiled/ contains the task vocabulary and one source-map marker but has no maintained source twin. It must not lead at high confidence.",
+ "maxConfidence": "low"
}
]
}
diff --git a/benchmarks/adversarial/results.json b/benchmarks/adversarial/results.json
index 7f2fe57..dddb894 100644
--- a/benchmarks/adversarial/results.json
+++ b/benchmarks/adversarial/results.json
@@ -1,6 +1,6 @@
{
- "cases": 8,
- "passed": 8,
+ "cases": 9,
+ "passed": 9,
"falseConfidenceRate": 0,
"results": [
{
@@ -132,6 +132,21 @@
"groundingOk": true,
"diagnosticOk": true,
"passed": true
+ },
+ {
+ "id": "pretty-printed-vendored-bundle",
+ "kind": "generated-output",
+ "maxConfidence": "low",
+ "topConfidence": null,
+ "grounding": "descriptive",
+ "diagnostics": [
+ "no-context-match"
+ ],
+ "contextFileCount": 0,
+ "overconfident": false,
+ "groundingOk": true,
+ "diagnosticOk": true,
+ "passed": true
}
]
}
diff --git a/packages/action/dist/index.mjs b/packages/action/dist/index.mjs
index 5f160be..18ca16a 100644
--- a/packages/action/dist/index.mjs
+++ b/packages/action/dist/index.mjs
@@ -1142,7 +1142,7 @@ function rankContextFiles(repo, input, limit = DEFAULT_CONTEXT_FILE_LIMIT, minSc
score -= BACKUP_COPY_PENALTY;
reasons.push("backup or archived copy deprioritized");
}
- if (isBundledOutput(file.textSample) && !isChanged && !mentionedPaths.has(file.path)) {
+ if (isBundledOutput(file.textSample, file.path) && !isChanged && !mentionedPaths.has(file.path)) {
score -= BUNDLED_OUTPUT_PENALTY;
reasons.push("machine-generated bundle deprioritized");
}
@@ -1347,7 +1347,7 @@ function findRegexTokenOverlap(text, taskTokens) {
}
return [...overlap].slice(0, 2);
}
-function isBundledOutput(textSample) {
+function isBundledOutput(textSample, path) {
if (textSample.length < MIN_BUNDLE_SAMPLE_BYTES) {
return false;
}
@@ -1355,7 +1355,11 @@ function isBundledOutput(textSample) {
if (textSample.length / lineCount >= BUNDLED_LINE_LENGTH) {
return true;
}
- return BUNDLE_MARKERS.filter((marker) => marker.test(textSample)).length >= 2;
+ const markerCount = BUNDLE_MARKERS.filter((marker) => marker.test(textSample)).length;
+ return markerCount >= 2 || markerCount >= 1 && isConventionalBundlePath(path);
+}
+function isConventionalBundlePath(path) {
+ return isGeneratedPath(path) || path.split("/").slice(0, -1).some((segment) => segment.toLowerCase() === "compiled");
}
function isTypeDeclarationPath(path) {
return /\.d\.(?:ts|mts|cts)$/i.test(path);
diff --git a/packages/core/src/rank.ts b/packages/core/src/rank.ts
index a319768..e52aa20 100644
--- a/packages/core/src/rank.ts
+++ b/packages/core/src/rank.ts
@@ -307,7 +307,7 @@ export function rankContextFiles(
reasons.push("backup or archived copy deprioritized");
}
- if (isBundledOutput(file.textSample) && !isChanged && !mentionedPaths.has(file.path)) {
+ if (isBundledOutput(file.textSample, file.path) && !isChanged && !mentionedPaths.has(file.path)) {
score -= BUNDLED_OUTPUT_PENALTY;
reasons.push("machine-generated bundle deprioritized");
}
@@ -599,7 +599,7 @@ function findRegexTokenOverlap(text: string, taskTokens: Set): string[]
return [...overlap].slice(0, 2);
}
-function isBundledOutput(textSample: string): boolean {
+function isBundledOutput(textSample: string, path: string): boolean {
if (textSample.length < MIN_BUNDLE_SAMPLE_BYTES) {
return false;
}
@@ -609,10 +609,17 @@ function isBundledOutput(textSample: string): boolean {
}
// Modern development bundles are often pretty-printed to a few dozen characters per
- // line, so line length alone misses them. Two independent bundler fingerprints keep
- // this conservative: readable vendored source with one helper-like identifier is not
- // penalized, while webpack/esbuild runtime output is.
- return BUNDLE_MARKERS.filter((marker) => marker.test(textSample)).length >= 2;
+ // line, so line length alone misses them. Two independent fingerprints identify one
+ // anywhere. A single fingerprint is enough only when the path supplies independent
+ // generated-output evidence. That catches Next.js-style `dist/compiled/` bundles while
+ // leaving ordinary readable vendored source, such as chalk's `source/vendor/`, alone.
+ const markerCount = BUNDLE_MARKERS.filter((marker) => marker.test(textSample)).length;
+ return markerCount >= 2 || (markerCount >= 1 && isConventionalBundlePath(path));
+}
+
+function isConventionalBundlePath(path: string): boolean {
+ return isGeneratedPath(path) || path.split("/").slice(0, -1)
+ .some((segment) => segment.toLowerCase() === "compiled");
}
function isTypeDeclarationPath(path: string): boolean {
diff --git a/packages/core/test/rank.test.ts b/packages/core/test/rank.test.ts
index e81a39e..6d82457 100644
--- a/packages/core/test/rank.test.ts
+++ b/packages/core/test/rank.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
-import { rankContextFiles } from "../src/rank.js";
+import { rankContextFiles, REPORT_SCORE_CUTOFF } from "../src/rank.js";
import type { RepoMap } from "../src/types.js";
describe("rankContextFiles", () => {
@@ -1210,6 +1210,61 @@ describe("rankContextFiles", () => {
.toContain("machine-generated bundle deprioritized");
});
+ it("deprioritizes a pretty-printed vendored bundle with one marker and no source twin", () => {
+ const bundle = [
+ ...Array.from(
+ { length: 150 },
+ (_, index) =>
+ `function transitionState${index}() { return "experimental transition state"; }`
+ ),
+ "//# sourceMappingURL=react-dom.development.js.map"
+ ].join("\n");
+ const repo: RepoMap = {
+ root: "/repo",
+ packageScripts: [],
+ changedFiles: [],
+ diffText: "",
+ packageManager: "npm",
+ diagnostics: [],
+ files: [
+ {
+ path: "src/router/render.ts",
+ extension: ".ts",
+ sizeBytes: 120,
+ isSource: true,
+ isTest: false,
+ kind: "code",
+ textSample: "export function renderRoute() { return renderPage(); }"
+ },
+ {
+ path: "compiled/react-dom/cjs/react-dom.development.js",
+ extension: ".js",
+ sizeBytes: bundle.length,
+ isSource: true,
+ isTest: false,
+ kind: "code",
+ textSample: bundle
+ }
+ ]
+ };
+
+ const ranked = rankContextFiles(
+ repo,
+ { issueText: "experimental transition state" },
+ 8,
+ Number.NEGATIVE_INFINITY
+ );
+ const vendoredBundle = ranked.find(
+ (file) => file.path === "compiled/react-dom/cjs/react-dom.development.js"
+ );
+
+ expect(bundle.length).toBeGreaterThan(2_000);
+ expect(bundle.length / bundle.split("\n").length).toBeLessThan(100);
+ expect(vendoredBundle?.score).toBeLessThan(REPORT_SCORE_CUTOFF);
+ expect(vendoredBundle?.confidence).toBe("low");
+ expect(vendoredBundle?.reasons).toContain("machine-generated bundle deprioritized");
+ });
+
it("leaves readable vendored source alone, however long the file", () => {
const vendored = Array.from(
{ length: 400 },
diff --git a/scripts/evaluate-adversarial.mjs b/scripts/evaluate-adversarial.mjs
index 11e35de..951d8ad 100644
--- a/scripts/evaluate-adversarial.mjs
+++ b/scripts/evaluate-adversarial.mjs
@@ -16,9 +16,10 @@
// fixtures contain the fabricated identifiers, so running these against this
// repository would resolve them and silently pass.
-import { readFile, writeFile } from "node:fs/promises";
+import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { fileURLToPath, pathToFileURL } from "node:url";
import { dirname, join, resolve } from "node:path";
+import { tmpdir } from "node:os";
import { materializePinnedRepository } from "./lib/external-cache.mjs";
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
@@ -40,8 +41,16 @@ const CHECKOUT_ENVIRONMENT_CODES = new Set(["tracked-paths-absent", "duplicate-r
const results = [];
for (const testCase of dataset.cases) {
- const dir = await materializePinnedRepository(testCase);
- const report = await buildFixMapReport({ repoRoot: dir, issueText: testCase.task });
+ const fixtureDir = testCase.fixture ? await materializeFixture(testCase.fixture) : null;
+ const dir = fixtureDir ?? await materializePinnedRepository(testCase);
+ let report;
+ try {
+ report = await buildFixMapReport({ repoRoot: dir, issueText: testCase.task });
+ } finally {
+ if (fixtureDir) {
+ await rm(fixtureDir, { recursive: true, force: true });
+ }
+ }
const topConfidence = report.contextFiles[0]?.confidence ?? null;
const grounding = report.analysis?.grounding?.specificity ?? null;
@@ -130,3 +139,35 @@ if (process.argv.includes("--gate") && failures.length > 0) {
if (failed) {
process.exit(1);
}
+
+async function materializeFixture(name) {
+ if (name !== "pretty-printed-vendored-bundle") {
+ throw new Error(`Unknown adversarial fixture: ${name}`);
+ }
+
+ const root = await mkdtemp(join(tmpdir(), "fixmap-adversarial-bundle-"));
+ try {
+ const compiledDir = join(root, "compiled", "react-dom", "cjs");
+ const sourceDir = join(root, "src", "router");
+ await mkdir(compiledDir, { recursive: true });
+ await mkdir(sourceDir, { recursive: true });
+
+ const bundle = [
+ ...Array.from(
+ { length: 150 },
+ (_, index) => `function transitionState${index}() { return "experimental transition state"; }`
+ ),
+ "//# sourceMappingURL=react-dom.development.js.map"
+ ].join("\n");
+ await writeFile(join(compiledDir, "react-dom.development.js"), bundle, "utf8");
+ await writeFile(
+ join(sourceDir, "render.ts"),
+ "export function renderRoute() { return renderPage(); }\n",
+ "utf8"
+ );
+ return root;
+ } catch (error) {
+ await rm(root, { recursive: true, force: true });
+ throw error;
+ }
+}