From 91486d1bb08b594e096cf90639950d8857bf792e Mon Sep 17 00:00:00 2001 From: Deep Kumar Singh Kushwah Date: Wed, 15 Jul 2026 21:29:36 +0530 Subject: [PATCH] feat(core): identifier-affinity ranking + top-line candidate score (6F.7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Field feedback: matches were "ranked only by the query's own character rarity", and find_component's real score was nested where agents misread it. - matchComponents applies IDENTIFIER_AFFINITY (x1.5) to a matched term that also names the candidate component — its name, props, or file basename, camelCase-split, tokenized, fuzzy-tolerant. "calendar" matching CalendarPanel now outranks the same text rendered incidentally elsewhere; genuinely-tied generic terms stay tied (ambiguity honesty unchanged at 1.000). Evidence lines note "(also names the component)". - Candidate gains optional top-level `score` (raw ranking score; confidence stays the calibrated signal), set by matchComponents, printed by the CLI (score=… confidence=…), inherited by MCP through the envelope JSON. 2 new core tests (211 total). Eval 290 pass / 0 fail / 0 xfail, gate OK, metrics 1.000. Candidate isn't in the generated schemas (drift gate green). Co-Authored-By: Claude Fable 5 --- TRACKER.md | 15 +++++++++--- packages/cli/src/index.ts | 1 + packages/core/src/matching.test.ts | 25 ++++++++++++++++++++ packages/core/src/query.ts | 38 +++++++++++++++++++++++++++--- packages/core/src/result.ts | 6 +++++ 5 files changed, 79 insertions(+), 6 deletions(-) diff --git a/TRACKER.md b/TRACKER.md index bacc931..4c35137 100644 --- a/TRACKER.md +++ b/TRACKER.md @@ -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 @@ -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 @@ -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) diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index da6afb9..7023499 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -313,6 +313,7 @@ function printMatchCandidate(candidate: Candidate): 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(" | ")}`); diff --git a/packages/core/src/matching.test.ts b/packages/core/src/matching.test.ts index f939bab..0551bca 100644 --- a/packages/core/src/matching.test.ts +++ b/packages/core/src/matching.test.ts @@ -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); + }); +}); diff --git a/packages/core/src/query.ts b/packages/core/src/query.ts index addd2af..38c339e 100644 --- a/packages/core/src/query.ts +++ b/packages/core/src/query.ts @@ -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. */ @@ -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>(); + const identifierTokens = (component: ComponentNode): Set => { + 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 ?? {}); @@ -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; @@ -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, }); } @@ -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]; diff --git a/packages/core/src/result.ts b/packages/core/src/result.ts index 6d35296..f8ad898 100644 --- a/packages/core/src/result.ts +++ b/packages/core/src/result.ts @@ -19,6 +19,12 @@ export interface Candidate { 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 {