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
6 changes: 3 additions & 3 deletions TRACKER.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
## Status

- **Current phase:** 4 — Matching engine
- **Next step:** 4.1Term matching v2
- **Done:** 0.1–0.4, 1.1–1.6, 2.1–2.5, 3.1–3.5
- **Next step:** 4.2Structural matching
- **Done:** 0.1–0.4, 1.1–1.6, 2.1–2.5, 3.1–3.5, 4.1
- **Gates passed:** Gate 0 (CI + red-path, #5/#6) · Gate 1 (precision 1.000, recall 0.895, zero poison) · Gate 2 (C1 instance attribution 1.000 · B1 4-level handler chains · C6 store writers↔readers · A9 portals — scorecard 137/0/0, precision & recall 1.000) · Gate 3 (B3 action effects · B4 routers · B6 cyclic journeys terminate · B7/B8 form & non-JSX events · G5 flag/role conditions — precision & recall 1.000)

## What CodeRadar is
Expand Down Expand Up @@ -218,7 +218,7 @@ The heart of the project. C1 and B1 live here.

## Phase 4 — Matching engine

### [ ] 4.1 Term matching v2
### [x] 4.1 Term matching v2
**Failure modes:** A4, A7 (matching half), A10
**Build:** replace `matchComponentsByText` with a scorer over instances:
- Normalization (from 1.5) both sides; fuzzy token match (edit distance ≤ 1 per token, token-set overlap) for OCR noise.
Expand Down
8 changes: 8 additions & 0 deletions eval/fixtures/a10-ocr-noise/app/InvoiceDashboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export function InvoiceDashboard() {
return (
<section>
<h1>Invoice Dashboard</h1>
<p>Outstanding balances by customer</p>
</section>
);
}
8 changes: 8 additions & 0 deletions eval/fixtures/a10-ocr-noise/app/ReconciliationReport.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export function ReconciliationReport() {
return (
<section>
<h1>Account Reconciliation</h1>
<p>Match transactions against statements</p>
</section>
);
}
8 changes: 8 additions & 0 deletions eval/fixtures/a10-ocr-noise/app/SettingsPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export function SettingsPanel() {
return (
<section>
<h1>Settings</h1>
<p>Preferences and account options</p>
</section>
);
}
8 changes: 8 additions & 0 deletions eval/fixtures/a10-ocr-noise/app/ShipmentTracker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export function ShipmentTracker() {
return (
<section>
<h1>Shipment Tracking</h1>
<p>Delivery status and carrier updates</p>
</section>
);
}
18 changes: 18 additions & 0 deletions eval/fixtures/a10-ocr-noise/golden.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"failureMode": "A10",
"note": "OCR noise: screenshot text arrives misspelled ('Acount Reconcilliation', 'Invoise Dashbord'). Fuzzy token matching — a small, length-scaled edit distance on long distinctive tokens — still lands the right component in the top 3 (here, top 1). Short common tokens must still match exactly so noise doesn't collide unrelated components.",
"expect": {
"components": [
{ "name": "ReconciliationReport", "instances": 0 },
{ "name": "InvoiceDashboard", "instances": 0 },
{ "name": "ShipmentTracker", "instances": 0 },
{ "name": "SettingsPanel", "instances": 0 }
],
"queries": [
{ "terms": ["Acount Reconcilliation"], "status": "ok", "top": "ReconciliationReport", "topK": 3 },
{ "terms": ["Invoise Dashbord"], "status": "ok", "top": "InvoiceDashboard", "topK": 3 },
{ "terms": ["Shipmnt Tracking"], "status": "ok", "top": "ShipmentTracker", "topK": 3 },
{ "terms": ["Reconciliation"], "status": "ok", "top": "ReconciliationReport" }
]
}
}
12 changes: 9 additions & 3 deletions eval/src/checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,9 +224,15 @@ export function runChecks(fixture: string, golden: Golden, graph: LineageGraph):
if (!passed) {
detail = `expected status ${query.status}, got ${result.status}`;
} else if (query.status === "ok" && query.top !== undefined) {
const top = result.candidates[0]?.value.component.name;
passed = top === query.top;
if (!passed) detail = `expected top ${query.top}, got ${top ?? "none"}`;
const k = query.topK ?? 1;
const topNames = result.candidates.slice(0, k).map((c) => c.value.component.name);
passed = topNames.includes(query.top);
if (!passed) {
detail =
k > 1
? `expected ${query.top} in top ${k}, got [${topNames.join(", ")}]`
: `expected top ${query.top}, got ${topNames[0] ?? "none"}`;
}
}
finalize("queries", id, passed, query.expectedFail, detail);
}
Expand Down
5 changes: 5 additions & 0 deletions eval/src/golden.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ export interface GoldenQuery {
status: "ok" | "ambiguous" | "declined";
/** Required top-1 component name when status is "ok". */
top?: string;
/**
* When set, `top` need only appear within the first `topK` candidates rather
* than at rank 1 — the honest bar for OCR-noisy input (failure mode A10).
*/
topK?: number;
expectedFail?: string;
}

Expand Down
91 changes: 91 additions & 0 deletions packages/core/src/matching.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { describe, expect, it } from "vitest";

import { matchComponentsByText } from "./query.js";
import { editDistance, fuzzyTokenMatch } from "./text.js";
import { type ComponentNode, type LineageGraph, nodeId } from "./types.js";

const loc = (file: string) => ({ file, line: 1, endLine: 1 });

function component(name: string, texts: string[]): ComponentNode {
return {
id: nodeId("component", `${name}.tsx`, name),
kind: "component",
name,
loc: loc(`${name}.tsx`),
exportName: name,
props: [],
renderedText: texts.map((text) => ({ text, source: "jsx" as const })),
rendersComponents: [],
};
}

function graph(components: ComponentNode[]): LineageGraph {
return {
version: 2,
root: "/app",
generatedAt: "2026-01-01T00:00:00.000Z",
generator: "test",
nodes: components,
edges: [],
};
}

describe("editDistance / fuzzyTokenMatch (TRACKER 4.1)", () => {
it("bounds the edit distance and abandons past the budget", () => {
expect(editDistance("kitten", "sitting", 3)).toBe(3);
expect(editDistance("abcdef", "uvwxyz", 2)).toBe(3); // max + 1 sentinel
});

it("tolerates OCR slips on long tokens but keeps short tokens strict", () => {
expect(fuzzyTokenMatch("reconciliation", "reconcilliation")).toBe(true);
expect(fuzzyTokenMatch("dashboard", "dashbord")).toBe(true);
expect(fuzzyTokenMatch("save", "safe")).toBe(false); // short → exact only
});
});

describe("matchComponentsByText scorer (TRACKER 4.1, A4/A10)", () => {
const forms = [
component("SettingsForm", ["Save", "Notifications"]),
component("ProfileForm", ["Save", "Display name"]),
component("BillingForm", ["Save", "Card number"]),
];

it("a lone generic term is honestly ambiguous, not a coin flip", () => {
const result = matchComponentsByText(graph(forms), ["Save"]);
expect(result.status).toBe("ambiguous");
expect(result.disambiguation).toBeDefined();
});

it("a distinctive term breaks the tie via rarity weighting", () => {
const result = matchComponentsByText(graph(forms), ["Save", "Card number"]);
expect(result.status).toBe("ok");
expect(result.candidates[0]?.value.component.name).toBe("BillingForm");
});

it("respects word order — 'Order deleted' ≠ 'Delete order'", () => {
const g = graph([
component("Toast", ["Order deleted"]),
component("DeleteButton", ["Delete order"]),
]);
expect(matchComponentsByText(g, ["Order deleted"]).candidates[0]?.value.component.name).toBe(
"Toast",
);
expect(matchComponentsByText(g, ["Delete order"]).candidates[0]?.value.component.name).toBe(
"DeleteButton",
);
});

it("matches OCR-noisy distinctive text", () => {
const g = graph([
component("ReconciliationReport", ["Account Reconciliation"]),
component("InvoiceDashboard", ["Invoice Dashboard"]),
]);
const result = matchComponentsByText(g, ["Acount Reconcilliation"]);
expect(result.status).toBe("ok");
expect(result.candidates[0]?.value.component.name).toBe("ReconciliationReport");
});

it("declines when nothing matches", () => {
expect(matchComponentsByText(graph(forms), ["Purchase history"]).status).toBe("declined");
});
});
Loading
Loading