Skip to content

Commit cc97ed0

Browse files
DeepCodeWorkclaude
andcommitted
feat(match): honest structural fallback + A3/A12 fixtures
Close the A3/A12 coverage gap from Phase 4.2. Structural matching already handled text-free UIs, but nothing proved the "graceful degradation + explicit low confidence" contract: - A structure-only match (no text/alias/correction evidence) is now capped at medium confidence — shape alone is never "high". - Fixtures a3-api-text (labels served by API/CMS → matched by structure, medium confidence; a text query honestly declines) and a12-non-text (chart + icon buttons → text declines, structure descriptor returns a medium-confidence candidate). - GoldenQuery gains a `confidence` assertion. 41 core tests pass; eval green (honesty metrics still 1.0/1.0/0.0). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent e8e12d7 commit cc97ed0

11 files changed

Lines changed: 124 additions & 6 deletions

File tree

TRACKER.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ The heart of the project. C1 and B1 live here.
227227
**Accept:** `a4-generic-text` green ("Save" alone → `ambiguous`; "Save" + "invoice details" → correct top-1); noisy-term fixture `a10-ocr-noise` (misspelled terms) top-3 correct.
228228

229229
### [x] 4.2 Structural matching
230-
**Failure modes:** A1, A3, A12
230+
**Failure modes:** A1, A3, A12 — fixtures: `a1-no-static-text`, `a3-api-text`, `a12-non-text` (structure-only matches capped at medium confidence — honest graceful degradation)
231231
**Build:** structural signature per instance subtree (child element kinds/counts: table with N columns, form with M inputs, card grid). Query side accepts a structure descriptor (from vision output: "a table with columns Name, Email, Actions") and scores against signatures. Text and structure scores combine into one ranking.
232232
**Accept:** fixture `a1-no-static-text` (dashboard, zero literals) top-3 correct via structure alone.
233233

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export function FilterBar() {
2+
return (
3+
<div className="filters">
4+
<input type="text" />
5+
<input type="text" />
6+
</div>
7+
);
8+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// A chart with icon-only controls — a canvas and two unlabeled buttons. There
2+
// is nothing to text-match; only structure gives a (low-confidence) candidate.
3+
export function MetricsChart() {
4+
return (
5+
<div className="chart">
6+
<canvas />
7+
<button />
8+
<button />
9+
</div>
10+
);
11+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"failureMode": "A12",
3+
"note": "Non-text UI (charts, canvases, icon buttons): nothing to text-match. A text query declines rather than guessing; a structure descriptor returns a candidate at medium confidence — graceful degradation, not a confident wrong answer.",
4+
"expect": {
5+
"components": [
6+
{ "name": "MetricsChart", "instances": 0 },
7+
{ "name": "FilterBar", "instances": 0 }
8+
],
9+
"queries": [
10+
{ "terms": ["Revenue over time"], "status": "declined" },
11+
{ "terms": [], "structure": { "buttons": 2 }, "status": "ok", "top": "MetricsChart", "confidence": "medium" }
12+
]
13+
}
14+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export function ApiForm({ onSubmit }: { onSubmit: () => void }) {
2+
// Labels/placeholders are all CMS-driven — no literals in source.
3+
return (
4+
<form onSubmit={onSubmit}>
5+
<input type="text" />
6+
<input type="text" />
7+
<button type="submit" />
8+
</form>
9+
);
10+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
interface Row {
2+
id: string;
3+
}
4+
5+
// All visible text comes from the API/CMS at runtime — the source has zero
6+
// literals, so text matching finds nothing. Only the structure identifies it.
7+
export function ApiTable({ columns, rows }: { columns: string[]; rows: Row[] }) {
8+
return (
9+
<table>
10+
<thead>
11+
<tr>
12+
<th>{columns[0]}</th>
13+
<th>{columns[1]}</th>
14+
<th>{columns[2]}</th>
15+
</tr>
16+
</thead>
17+
<tbody>
18+
{rows.map((r) => (
19+
<tr key={r.id}>
20+
<td>{r.id}</td>
21+
</tr>
22+
))}
23+
</tbody>
24+
</table>
25+
);
26+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"failureMode": "A3",
3+
"note": "Text served by API/CMS: components render only {data.*}, so text matching finds nothing (a text query honestly declines). Structural matching still identifies them — but at medium confidence, never high, because shape alone can't be certain.",
4+
"expect": {
5+
"components": [
6+
{ "name": "ApiTable", "instances": 0 },
7+
{ "name": "ApiForm", "instances": 0 }
8+
],
9+
"queries": [
10+
{ "terms": [], "structure": { "table": true, "columns": 3 }, "status": "ok", "top": "ApiTable", "confidence": "medium" },
11+
{ "terms": [], "structure": { "form": true, "inputs": 2 }, "status": "ok", "top": "ApiForm", "confidence": "medium" },
12+
{ "terms": ["Team members"], "status": "declined" }
13+
]
14+
}
15+
}

eval/src/checks.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,11 @@ export function runChecks(
248248
passed = missing.length === 0;
249249
if (!passed) detail = `expected context [${query.context.join(", ")}], got [${ctx.join(", ")}]`;
250250
}
251+
if (passed && query.confidence !== undefined) {
252+
const level = result.candidates[0]?.confidence.level;
253+
passed = level === query.confidence;
254+
if (!passed) detail = `expected confidence ${query.confidence}, got ${level ?? "none"}`;
255+
}
251256
}
252257
finalize("queries", id, passed, query.expectedFail, detail);
253258

eval/src/golden.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ export interface GoldenQuery {
4242
top?: string;
4343
/** Ancestor names the top match must list as `context` (step 4.3). */
4444
context?: string[];
45+
/** When set, the top candidate's confidence level must equal this (A3/A12 honesty). */
46+
confidence?: "high" | "medium" | "low";
4547
/**
4648
* When set, `top` need only appear within the first `topK` candidates rather
4749
* than at rank 1 — the honest bar for OCR-noisy input (failure mode A10).

packages/core/src/matching.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,28 @@ describe("matchComponentsByText scorer (TRACKER 4.1, A4/A10)", () => {
102102
it("declines when nothing matches", () => {
103103
expect(matchComponentsByText(graph(forms), ["Purchase history"]).status).toBe("declined");
104104
});
105+
106+
it("caps a structure-only match at medium confidence, never high (A3/A12)", () => {
107+
const chart: ComponentNode = {
108+
...component("MetricsChart", []),
109+
structure: {
110+
table: 0,
111+
columns: 0,
112+
form: 0,
113+
input: 0,
114+
button: 2,
115+
link: 0,
116+
image: 0,
117+
heading: 0,
118+
list: 0,
119+
repeated: 0,
120+
},
121+
};
122+
const result = matchComponents(graph([chart]), { structure: { buttons: 2 } });
123+
expect(result.status).toBe("ok");
124+
expect(result.candidates[0]?.value.component.name).toBe("MetricsChart");
125+
expect(result.candidates[0]?.confidence.level).toBe("medium");
126+
});
105127
});
106128

107129
describe("alias glossary & corrections (TRACKER 4.6, E2/G4)", () => {

0 commit comments

Comments
 (0)