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

- **Current phase:** 6F — Field hardening, feedback round 1 (runs before 6.1–6.5)
- **Next step:** 6F.6 test-coverage detection hardening (needs the field's actual failing variant) · 6F.7 scoring polish · 6F.8 visualizer
- **Done:** 0.1–0.4, 1.1–1.6, 2.1–2.5, 3.1–3.6, 4.1–4.6, 5.1–5.7, 6F.1–6F.5
- **Next step:** 6F.8 galaxy visualizer · 6F.6 test-coverage hardening (blocked: needs the field's actual failing variant — fixture's wrapper shape already passes)
- **Done:** 0.1–0.4, 1.1–1.6, 2.1–2.5, 3.1–3.6, 4.1–4.6, 5.1–5.7, 6F.1–6F.5, 6F.7
- **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) · Gate 4 (A4 rarity · A10 fuzzy/OCR · A1 structural · A6 subtree · E3 vision annotations · E2 aliases · G4 corrections — high-conf correct 1.000, ambiguity honesty 1.000, poison rate 0.000) · Gate 5 (F1 context bundle · F2 blast radius · F3 test coverage · F4 response schema · F5 git history · MCP server over stdio — scorecard 265/0/0, all honesty metrics 1.000; **M5 reached** — ticket in → budgeted context bundle out, over MCP)

## What CodeRadar is
Expand Down Expand Up @@ -489,7 +489,7 @@ through to the JSX argument), test files importing through the same alias/barrel
coverage graph emits `coverage-unmapped` instead of per-component `untested`; enable the 6F.2
checks.

### [ ] 6F.7 Scoring & result-surface polish
### [x] 6F.7 Scoring & result-surface polish
**Failure modes:** A4, D2, D6
**Build:** weight matches by term specificity against the candidate's own identifiers (name,
props, file path), not global character rarity alone — gibberish must never outscore a real
Expand All @@ -498,6 +498,15 @@ term (field: 6.50 for gibberish vs 6.09 for "calendar"). Expose a top-line `scor
to misread) — core envelope, CLI output, MCP result schema.
**Accept:** ranking fixture: identifier-specific terms beat rarity-only scores; `score` present
at candidate top level across CLI + MCP (schemas regenerated, drift gate green).
**Done:** `matchComponents` applies `IDENTIFIER_AFFINITY` (×1.5) to a matched term that also
names the candidate — its name, props, or file basename, camelCase-split and tokenized
(memoized per component, fuzzy-tolerant) — with the evidence line noting "(also names the
component)". Genuinely-tied generic terms stay tied, so ambiguity honesty is unchanged
(1.000). `Candidate` gains an optional top-level `score` (raw ranking score, larger = stronger
within one result; `confidence` stays the calibrated signal) set by `matchComponents` and
printed by the CLI (`score=… confidence=…`); MCP inherits it through the envelope JSON.
Candidate isn't part of the generated schemas, so no schema change (drift gate confirms).
2 new core tests (211 total); eval 290/0/0/0, gate OK, metrics 1.000.

### [ ] 6F.8 Galaxy visualizer (`coderadar visualize`)
**Failure modes:** — (user-requested feature, 2026-07-15)
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ function printMatchCandidate(candidate: Candidate<ComponentMatch>): void {
const match = candidate.value;
console.log(
`${match.component.name} (${match.component.loc.file}:${match.component.loc.line}) ` +
`score=${candidate.score?.toFixed(2) ?? "—"} ` +
`confidence=${candidate.confidence.level} (${candidate.confidence.score.toFixed(2)})`,
);
console.log(` matched: ${match.matchedText.join(" | ")}`);
Expand Down
25 changes: 25 additions & 0 deletions packages/core/src/matching.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,3 +202,28 @@ describe("empty-normalizing rendered text is never a wildcard (TRACKER 6F.1, A14
expect(result.candidates[0]?.value.component.name).toBe("CalendarPanel");
});
});

describe("identifier affinity & top-line score (TRACKER 6F.7)", () => {
// "Calendar" is rendered by both, but only CalendarPanel is NAMED for it —
// global character rarity alone can't tell these apart (field complaint).
const g = graph([
component("CalendarPanel", ["Calendar", "Upcoming meetings"]),
component("Dashboard", ["Calendar", "Revenue overview"]),
]);

it("a term that also names the component beats the same text elsewhere", () => {
const result = matchComponentsByText(g, ["Calendar"]);
expect(result.status).toBe("ok");
expect(result.candidates[0]?.value.component.name).toBe("CalendarPanel");
expect(
result.candidates[0]?.evidence.some((e) => e.detail.includes("also names the component")),
).toBe(true);
});

it("exposes the raw ranking score at the candidate top level, best-first", () => {
const result = matchComponentsByText(g, ["Calendar"]);
const scores = result.candidates.map((c) => c.score ?? 0);
expect(scores[0]).toBeGreaterThan(0);
expect([...scores].sort((a, b) => b - a)).toEqual(scores);
});
});
38 changes: 35 additions & 3 deletions packages/core/src/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,13 @@ export interface MatchQuery {

/** Weight of a full structural match relative to a rare matched term. */
const STRUCTURE_WEIGHT = 3;
/**
* Multiplier when a matched term also names the component itself — its name,
* props, or file (6F.7). "calendar" matching CalendarPanel outranks the same
* text rendered incidentally elsewhere; global character rarity alone can't
* tell them apart.
*/
const IDENTIFIER_AFFINITY = 1.5;
/** A glossary alias hit outweighs any text/structure evidence (Phase 4.6). */
const ALIAS_WEIGHT = 10;
/** A recorded human correction is the strongest signal of all. */
Expand Down Expand Up @@ -170,6 +177,28 @@ export function matchComponents(
return base * (boosts.get(term) ?? 1);
};

// A component's own identifiers — name, props, file basename — split on
// camelCase and separators. Terms that also NAME the component score higher
// than the same text rendered incidentally elsewhere (6F.7).
const identifierMemo = new Map<string, Set<string>>();
const identifierTokens = (component: ComponentNode): Set<string> => {
const cached = identifierMemo.get(component.id);
if (cached) return cached;
const camelSplit = (s: string): string => s.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
const basename = component.loc.file.split("/").pop()?.replace(/\.[a-z]+$/i, "") ?? "";
const out = new Set(
tokenize(
[component.name, ...component.props, basename].map(camelSplit).join(" "),
),
);
identifierMemo.set(component.id, out);
return out;
};
const namesComponent = (term: string, component: ComponentNode): boolean => {
const ids = identifierTokens(component);
return tokenize(term).some((t) => ids.has(t) || [...ids].some((id) => fuzzyTokenMatch(id, t)));
};

// Glossary aliases + recorded corrections (Phase 4.6). Both are authority
// signals — a phrase resolves even when it appears nowhere in the code.
const aliasEntries = Object.entries(query.aliases ?? {});
Expand Down Expand Up @@ -233,7 +262,8 @@ export function matchComponents(
}
}
if (hit === null) continue;
const w = termWeight(term);
const affine = namesComponent(term, component);
const w = termWeight(term) * (affine ? IDENTIFIER_AFFINITY : 1);
covered.add(term);
if (where.id === component.id) matched.push(term);
weight += w;
Expand All @@ -247,7 +277,9 @@ export function matchComponents(
: "";
evidence.push({
kind: "text-match",
detail: `"${term}" matched rendered text "${hit.text}"${provenance} — rarity weight ${w.toFixed(2)}`,
detail:
`"${term}" matched rendered text "${hit.text}"${provenance} — rarity weight ${w.toFixed(2)}` +
(affine ? " (also names the component)" : ""),
loc: where.loc,
});
}
Expand Down Expand Up @@ -347,7 +379,7 @@ export function matchComponents(
s.covered.size === 0 && conf.level === "high"
? { score: conf.score, level: "medium" as const }
: conf;
return { value: s.match, confidence, evidence: s.evidence };
return { value: s.match, confidence, evidence: s.evidence, score: s.score };
});

const top = winners[0];
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ export interface Candidate<T> {
value: T;
confidence: Confidence;
evidence: Evidence[];
/**
* Raw ranking score, exposed top-level so agents don't misread the nested
* calibration score as match strength (6F.7). Larger is stronger; only
* comparable within one result. `confidence` stays the calibrated signal.
*/
score?: number;
}

export interface QueryResult<T> {
Expand Down
Loading