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
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ which is generated from the recorded results rather than transcribed by hand.
- `fixmap benchmark --repo . --last 50` backtests BM25-over-code, FixMap context, and FixMap with Impact Graph on identical historical parent-snapshot corpora. It reports all, path-mentioned, and unmentioned cohorts, Wilson intervals, raw cases, skip counts, and secondary-file recall without executing repository code or scoring generated twins as primary answers.
- `fixmap plan --format agent` emits a compact, stable handoff organized as EDIT CANDIDATE, INSPECT, TEST, RISK, AVOID, and UNCERTAINTY.
- A frozen four-arm agent-study protocol and validator are checked in for future controlled measurements. No agent-effectiveness or time-saved claim is made without completed, auditable runs.
- A 32-second motion-first agent comparison is available on the README and website in animated-preview and 1080p H.264/AAC formats, with original no-vocals music and no unsupported savings claim.
- A 32-second motion-first agent comparison is available on the README and website in animated-preview and 1080p H.264/AAC formats, with original no-vocals music and no claim of measured agent efficiency.

### Improved

Expand Down Expand Up @@ -58,7 +58,7 @@ which is generated from the recorded results rather than transcribed by hand.
### Evidence

- The release ledger maps all 126 open GitHub issues in #500-#627 (excluding already-closed #542 and #599) plus every item in the attached 30-finding review to a resolution and verification source.
- External, held-out, adversarial, BM25/lexical/path baselines, savings, rendered examples, package smoke tests, and the 1,000-file scan gate were regenerated and checked. The held-out evidence continues to state plainly where BM25 leads FixMap.
- External, held-out, adversarial, BM25/lexical/path baselines, context-size proxy, scan-performance, rendered examples, package smoke tests, and the 1,000-file scan gate were regenerated and checked. The held-out evidence continues to state plainly where BM25 leads FixMap.

### Installation

Expand Down
291 changes: 68 additions & 223 deletions apps/web/app/_components/interactive-map-stage.tsx
Original file line number Diff line number Diff line change
@@ -1,242 +1,87 @@
"use client";

import { useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
import {
ArrowRight,
CaretDown,
CheckCircle,
FileText,
GitBranch,
ShieldCheck,
Warning
} from "@phosphor-icons/react";

type StageKey = "files" | "checks" | "risks";

type Example = {
label: string;
issue: string;
keywords: string[];
files: [string, string, string];
checks: [string, string, string];
risks: [string, string, string];
};

const examples: Example[] = [
{
label: "Expiring reset links",
issue: "Password reset links expire too early",
keywords: ["auth", "email", "expire", "link", "password", "reset", "token"],
files: ["src/features/auth/reset/request.ts", "src/features/auth/reset/token-service.ts", "src/lib/email/templates/reset.ts"],
checks: ["Token expiration logic", "Reset token integration tests", "Email link TTL configuration"],
risks: ["Clock and timezone boundary", "Cached authentication config", "Email client link prefetch"]
},
{
label: "Wrong invoice time zone",
issue: "Invoices show the wrong time after daylight saving changes",
keywords: ["date", "daylight", "dst", "invoice", "time", "timezone"],
files: ["src/timezone/resolve.ts", "src/invoices/summary.ts", "src/timezone/index.ts"],
checks: ["Timezone conversion tests", "DST boundary cases", "Invoice rendering path"],
risks: ["Cached timezone offsets", "External API assumptions", "Off-by-one date handling"]
},
{
label: "Duplicate webhook event",
issue: "Payment webhooks sometimes create duplicate orders",
keywords: ["duplicate", "idempotency", "order", "payment", "retry", "webhook"],
files: ["src/payments/webhook.ts", "src/orders/create-order.ts", "src/payments/idempotency.ts"],
checks: ["Webhook replay test", "Order idempotency suite", "Concurrent insert handling"],
risks: ["Retry timing window", "Missing unique constraint", "Out-of-order delivery"]
} from "@phosphor-icons/react/ssr";
import { buildReportFromRepo } from "@aryam/fixmap-core/browser";
import { sampleRepo } from "../sample-repo";

const task = "TOKEN_TTL_MINUTES is ignored and reset links expire immediately.";
const command = `fixmap plan --issue "${task}" --format agent`;
const report = buildReportFromRepo(sampleRepo, { issueText: task, limit: 3 });

function homepageExample() {
const editCandidate = report.contextFiles[0];
const impactFile = (report.impact?.files ?? []).find((file) => !file.path.includes("test/"));
const testRoute = report.testRoutes[0];
const risk = report.risks[0];

if (!editCandidate || !impactFile || !testRoute || !risk) {
throw new Error("The homepage example no longer contains the file, test, impact, and risk evidence it is designed to explain.");
}
];

const firstExample = examples[0]!;
const genericWords = new Set([
"a", "an", "and", "are", "be", "because", "broken", "bug", "create", "does", "error", "failed", "fails", "failure",
"fix", "for", "from", "in", "is", "issue", "it", "of", "on", "or", "problem", "sometimes", "the", "to", "when", "with", "wrong"
]);

function issueWords(issue: string): string[] {
return [...new Set(issue.toLowerCase().match(/[a-z0-9]+/g) ?? [])]
.filter((word) => word.length > 2 && !genericWords.has(word));
}

function customResult(issue: string): Example {
const words = issueWords(issue);
const area = words[0]?.slice(0, 32) ?? "feature";
const behavior = words[1]?.slice(0, 32) ?? "behavior";
const title = `${area} ${behavior}`;
return {
label: "Custom issue preview",
issue,
keywords: words,
files: [`src/${area}/${behavior}.ts`, `src/${area}/index.ts`, `test/${area}/${behavior}.test.ts`],
checks: [`${title} behavior`, `${area} integration path`, `Regression for the reported issue`],
risks: ["Existing behavior compatibility", "Uncovered input boundary", "Related integration assumptions"]
};
}

function resultForIssue(issue: string): { example: Example; index: number } {
const words = new Set(issueWords(issue));
const ranked = examples
.map((example, index) => ({ example, index, score: example.keywords.filter((keyword) => words.has(keyword)).length }))
.sort((left, right) => right.score - left.score);
const match = ranked[0];
return match && match.score > 0 ? match : { example: customResult(issue), index: -1 };
return { editCandidate, impactFile, testRoute, risk };
}

const stageMeta: Array<{ key: StageKey; label: string; hint: string }> = [
{ key: "files", label: "Files", hint: "Finding relevant code" },
{ key: "checks", label: "Tests", hint: "Finding checks" },
{ key: "risks", label: "Risks", hint: "Reviewing impact" }
];
const { editCandidate, impactFile, testRoute, risk } = homepageExample();

function OutputCard({
stage,
active,
ready,
items,
onSelect
}: {
stage: StageKey;
active: boolean;
ready: boolean;
items: [string, string, string];
onSelect: () => void;
}) {
const config = {
files: { icon: FileText, title: "Files to open first", count: "7 files", metric: "Match", value: "Strong" },
checks: { icon: CheckCircle, title: "Tests and checks", count: "8 checks", metric: "Support", value: "Found" },
risks: { icon: Warning, title: "Risks worth reviewing", count: "6 risks", metric: "Impact", value: "Medium" }
}[stage];
const Icon = config.icon;

return (
<button
className={`stage-output-card ${active ? "active" : ""} ${ready ? "ready" : "pending"}`}
type="button"
onClick={onSelect}
aria-pressed={active}
>
<span className="stage-card-heading">
<Icon size={22} weight={stage === "checks" ? "fill" : "regular"} aria-hidden />
<strong>{config.title}</strong>
<small>{ready ? config.count : "Analyzing"}</small>
<span className="stage-card-metric"><small>{config.metric}</small><b>{ready ? config.value : "—"}</b></span>
</span>
<span className="stage-card-rows">
{items.map((item, index) => (
<span key={item} style={{ "--row-index": index } as CSSProperties}>
<b>{index + 1}</b><code>{ready ? item : "Scanning the repository…"}</code><small>{stage === "files" ? (index === 0 ? "Strong match" : "Connected") : stage === "checks" ? "Suggested" : "Why it matters"}</small>
</span>
))}
</span>
<span className="stage-card-more">+{stage === "files" ? 4 : 5} more <ArrowRight size={14} weight="bold" aria-hidden /></span>
</button>
);
}
const candidateReason =
editCandidate.reasons.find((reason) => reason.startsWith("defines task identifiers")) ??
editCandidate.reasons[0] ?? "ranked repository evidence";
const impactReason = impactFile.evidence[0]?.reason ?? "related repository evidence";

export function InteractiveMapStage() {
const [exampleIndex, setExampleIndex] = useState(0);
const [issue, setIssue] = useState(firstExample.issue);
const [result, setResult] = useState(firstExample);
const [activeStage, setActiveStage] = useState<StageKey>("files");
const [readyCount, setReadyCount] = useState(3);
const [isRunning, setIsRunning] = useState(false);
const timers = useRef<number[]>([]);

const clearTimers = () => {
timers.current.forEach(window.clearTimeout);
timers.current = [];
};

useEffect(() => clearTimers, []);

const stageItems = useMemo(
() => ({ files: result.files, checks: result.checks, risks: result.risks }),
[result]
);

const runMap = () => {
clearTimers();
const next = resultForIssue(issue);
setExampleIndex(next.index);
setResult(next.example);
setIsRunning(true);
setReadyCount(0);
setActiveStage("files");
timers.current = [
window.setTimeout(() => setReadyCount(1), 420),
window.setTimeout(() => { setReadyCount(2); setActiveStage("checks"); }, 980),
window.setTimeout(() => { setReadyCount(3); setActiveStage("risks"); }, 1540),
window.setTimeout(() => setIsRunning(false), 1880)
];
};

const chooseExample = (index: number) => {
const nextExample = examples[index];
if (!nextExample) return;
clearTimers();
setExampleIndex(index);
setIssue(nextExample.issue);
setResult(nextExample);
setReadyCount(3);
setActiveStage("files");
setIsRunning(false);
};

return (
<div className="interactive-map-stage" role="group" aria-label="Interactive example of a FixMap report">
<div className="stage-toolbar">
<span className="stage-mini-brand"><ShieldCheck size={19} weight="duotone" aria-hidden /><b>FixMap</b></span>
<label className="stage-example-select">
<span className="sr-only">Choose an example issue</span>
<select value={exampleIndex} onChange={(event) => chooseExample(Number(event.target.value))}>
{exampleIndex === -1 ? <option value={-1}>Custom issue preview</option> : null}
{examples.map((item, index) => <option value={index} key={item.label}>Example: {item.label}</option>)}
</select>
<CaretDown size={14} weight="bold" aria-hidden />
</label>
<span className="stage-local-status"><i /> Runs here</span>
</div>

<div className="stage-body">
<div className="stage-input-column">
<div className="stage-input-heading"><span>1</span><strong>Describe what needs fixing</strong></div>
<div className="stage-issue-field">
<label className="sr-only" htmlFor="fixmap-hero-issue">Software issue</label>
<textarea id="fixmap-hero-issue" value={issue} maxLength={500} onChange={(event) => setIssue(event.target.value)} />
<span>{issue.length}/500</span>
<button type="button" onClick={runMap} aria-label="Create a FixMap report" disabled={isRunning || issue.trim().length < 8}>
<ArrowRight size={19} weight="bold" aria-hidden />
</button>
</div>
<div className="stage-trust-note"><ShieldCheck size={19} weight="duotone" aria-hidden /><span><strong>A starting point, not a verdict.</strong>Open the files and run the checks before changing code.</span></div>
<figure className="real-report-stage" aria-labelledby="real-report-title">
<figcaption className="real-report-toolbar" id="real-report-title">
<span><ShieldCheck size={18} weight="duotone" aria-hidden /><strong>Real FixMap output</strong></span>
<span>sample-api · local checkout</span>
</figcaption>

<div className="real-report-flow">
<section className="real-report-input" aria-label="Input to FixMap">
<span className="real-report-label">You provide</span>
<strong>Your coding task</strong>
<blockquote>{task}</blockquote>
<div className="real-report-repo"><GitBranch size={17} aria-hidden /><span><b>Repository</b> sample-api</span></div>
<code>{command}</code>
</section>

<div className="real-report-bridge" aria-label="FixMap maps the task to repository evidence">
<span>FixMap maps repository evidence</span>
<ArrowRight size={22} weight="bold" aria-hidden />
</div>

<div className="stage-output-column">
<div className="stage-progress" role="group" aria-label="Analysis progress">
{stageMeta.map((stage, index) => {
const isReady = readyCount > index;
return (
<button key={stage.key} type="button" className={`${activeStage === stage.key ? "active" : ""} ${isReady ? "ready" : ""}`} onClick={() => setActiveStage(stage.key)}>
<span>{index + 1}</span><strong>{stage.label}</strong><small>{isReady ? "Ready" : stage.hint}</small>
</button>
);
})}
</div>
<div className="stage-output-stack" aria-live="polite">
{stageMeta.map((stage, index) => (
<OutputCard
key={stage.key}
stage={stage.key}
active={activeStage === stage.key}
ready={readyCount > index}
items={stageItems[stage.key]}
onSelect={() => setActiveStage(stage.key)}
/>
))}
</div>
</div>
<section className="real-report-output" aria-label="Output from FixMap">
<span className="real-report-label">Your agent gets</span>
<strong>Focused places to inspect first</strong>
<dl>
<div>
<dt><FileText size={19} weight="duotone" aria-hidden /> File candidate</dt>
<dd><code>{editCandidate.path}</code><span>{candidateReason}</span></dd>
</div>
<div>
<dt><CheckCircle size={19} weight="fill" aria-hidden /> Test</dt>
<dd><code>{testRoute.command}</code><span>{testRoute.relatedFiles[0]}</span></dd>
</div>
<div>
<dt><GitBranch size={19} weight="duotone" aria-hidden /> Impact &amp; risk</dt>
<dd>
<code>{impactFile.path}</code>
<span>{impactReason}</span>
<span className="real-report-risk"><Warning size={14} aria-hidden /><b>{risk.area}</b> · {risk.reason}</span>
</dd>
</div>
</dl>
</section>
</div>
</div>

<p className="real-report-footnote">
Generated by the real browser-safe FixMap engine against the sample repository used in the live demo.
</p>
</figure>
);
}
4 changes: 2 additions & 2 deletions apps/web/app/changelog/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const releases: Release[] = [
"fixmap benchmark compares BM25, FixMap, and Impact Graph on identical historical parent snapshots without executing repository code.",
"fixmap watch streams working-tree drift findings and recalculated impact as an agent edits, with Markdown or JSON Lines output.",
"Compact agent output provides a stable edit, inspect, test, risk, avoid, and uncertainty handoff.",
"A 32-second motion-first comparison shows two agents handling the same issue, with an original no-vocals soundtrack and no unsupported savings claim."
"A 32-second motion-first comparison shows two agents handling the same issue, with an original no-vocals soundtrack and no claim of measured agent efficiency."
]
},
{
Expand Down Expand Up @@ -109,7 +109,7 @@ const releases: Release[] = [
label: "Evidence",
items: [
"The release ledger maps 126 GitHub issues plus the complete attached 30-finding review to fixes and verification.",
"External, held-out, adversarial, baseline, savings, package, generated-output, and 1,000-file scan gates are recorded and reproducible."
"External, held-out, adversarial, baseline, context-size proxy, scan-performance, package, generated-output, and 1,000-file scan gates are recorded and reproducible."
]
}
]
Expand Down
12 changes: 12 additions & 0 deletions apps/web/app/evidence/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@ export default function EvidencePage() {
<div className="button-row"><a className="button primary" href={`${repoUrl}/tree/main/benchmarks`}>Open benchmark data <ArrowRight size={18} weight="bold" aria-hidden /></a></div>
</section>

<section className="section page-shell evidence-boundaries" aria-labelledby="evidence-boundaries-title">
<div className="section-heading split-heading">
<div><p className="eyebrow">Claim boundary</p><h2 id="evidence-boundaries-title">Three different questions need three different answers.</h2></div>
<p>Retrieval measurements can show whether FixMap surfaced a known fixing file. They cannot, by themselves, show whether a coding agent used fewer tokens or completed the task better.</p>
</div>
<div className="evidence-boundary-list">
<article><span>Measured</span><h3>Repository retrieval, calibration, adversarial behavior, and scan time</h3><p>The checked-in suites below contain the cases, baselines, misses, and confidence intervals.</p></article>
<article><span>Mechanism</span><h3>Focused context, test routes, impact candidates, and explicit uncertainty</h3><p>These are product behaviors produced from repository evidence. They are not downstream outcome claims.</p></article>
<article><span>Not yet measured</span><h3>Agent tokens, cost, time, tool calls, and task success</h3><p>The controlled protocol and evaluator are ready for real runs; no result is published until a complete run set exists.</p></article>
</div>
</section>

<section className="section page-shell">
<div className="section-heading split-heading">
<div><p className="eyebrow">Confidence calibration</p><h2>Labels are evidence bands, not probabilities.</h2></div>
Expand Down
Loading