diff --git a/TRACKER.md b/TRACKER.md index 869ab7e..184436a 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.10 HTML-entity rendered-text noise (self-found on Grafana — ` `→"nbsp", `"`→"34" create spurious matches; decode/strip entities in the parser's text extraction, failure mode A16); then 6F.6 test-coverage hardening (blocked: needs the field's actual failing variant — Grafana produced 1,009 covered-by edges, so detection works on real code; the fixture's custom-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–6F.9 · **released 0.4.0** (tag v0.4.0, PR #50 to main; publish to npm pending). **Self-validated on Grafana frontend** (6,461 files, 15,334 nodes / 18,367 edges in 72 s): 55 RTK-query data sources, 32 routes, 1,009 coverage edges, gibberish declines — all previously 0/1 in the field run. +- **Next step:** 6F.6 test-coverage hardening (blocked: needs the field's actual failing variant — Grafana produced 1,009 covered-by edges, so detection works on real code; the fixture's custom-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–6F.10 · **released 0.4.0** (tag v0.4.0, PR #50 to main; publish to npm pending). **Self-validated on Grafana frontend** (6,461 files, 15,334 nodes / 18,367 edges in 72 s): 55 RTK-query data sources, 32 routes, 1,009 coverage edges, gibberish declines — all previously 0/1 in the field run. - **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 @@ -556,7 +556,7 @@ Grafana graph:** `Find silences by matcher` → `SilencesFilter` top-1 (`OrderBy honest `no-signal`. eval 297/0/0/0, gate OK, all metrics 1.000. A16 (HTML-entity rendered text) spun out to 6F.10 — out of scope for a scoring PR. -### [ ] 6F.10 HTML-entity rendered-text noise +### [x] 6F.10 HTML-entity rendered-text noise **Failure modes:** A16 (new) **Build:** self-found on Grafana — rendered text that is an HTML entity (` `, `"`, `>`) normalizes to a junk token (`nbsp`, `34`, `gt`); numeric entities make gibberish @@ -565,10 +565,25 @@ containing those digits match. Decode/strip HTML entities in the parser's text-e extraction, hence a separate step from 6F.9. **Accept:** fixture with entity-only rendered text: it produces no match target; gibberish that shares digits with a numeric entity declines. eval green. +**Done:** new `entities.ts` in parser-react — `decodeEntities(text)` resolves the HTML entities +React decodes at render time (numeric decimal `"` / hex `"` generically, named entities +from a curated map: markup, whitespace, punctuation, symbols, currency, accented Latin-1; +unknown names left verbatim, matching React). `extractRenderedText` decodes JSX text and quoted +attribute values — the two surfaces React HTML-decodes — while JS string/template literals stay +untouched (React renders `{" "}` literally). Decoded entities become the character React +renders, which the normalizer strips: ` `→space→dropped, `>`/`"`/`·`→ +punctuation that normalizes to empty, so an entity-only component yields **no discriminating +target** (verified: `EntitySpacer.renderedText` = `["\"", ">", "·", "<", "›"]`, zero +alphanumeric tokens). New fixture `a16-html-entities` (entity-only `EntitySpacer` + real +`QuotaNotice`): the named-entity token `nbsp`, the numeric-entity token `34`, and a gibberish +query sharing those digits (`zzqwxnomatch12345`, which pre-fix matched via `"`→"34") all +decline `no-signal`, while the real query still lands on `QuotaNotice` and isn't poisoned by the +digit-sharing gibberish. 7 new tests (4 unit decode + 3 fixture integration), 136 parser-react +total; eval 304/0/0/0, gate OK, all metrics 1.000. **Gate 6F:** field-patterns fixture fully green (skip list empty ✅) · instance resolution ≥ 95% (✅ 100%) · RTK data sources > 0 (✅) · route nodes > 0 (✅) · gibberish queries decline -`no-signal` (✅) · `pnpm eval` green end-to-end (✅ 290/0/0/0). Remaining before the gate is +`no-signal` (✅) · `pnpm eval` green end-to-end (✅ 304/0/0/0). Remaining before the gate is formally stamped: 6F.6 test-coverage hardening (blocked on a real failing sample). --- diff --git a/eval/fixtures/a16-html-entities/app/EntitySpacer.tsx b/eval/fixtures/a16-html-entities/app/EntitySpacer.tsx new file mode 100644 index 0000000..9238de4 --- /dev/null +++ b/eval/fixtures/a16-html-entities/app/EntitySpacer.tsx @@ -0,0 +1,16 @@ +// Its only rendered text is HTML entities — the shape the Grafana frontend hit +// (26 , 4 ", plus "/>/</·/›). Each decodes to +// punctuation/whitespace, so after decoding there is no match target at all. +export function EntitySpacer() { + return ( +
+ ); +} diff --git a/eval/fixtures/a16-html-entities/app/QuotaNotice.tsx b/eval/fixtures/a16-html-entities/app/QuotaNotice.tsx new file mode 100644 index 0000000..c19e603 --- /dev/null +++ b/eval/fixtures/a16-html-entities/app/QuotaNotice.tsx @@ -0,0 +1,6 @@ +// A real component with distinctive text, so the fixture isn't trivially +// all-declining: a real query must still land here, and gibberish that shares +// digits with a numeric entity must not poison it. +export function QuotaNotice() { + returnStorage quota exceeded
; +} diff --git a/eval/fixtures/a16-html-entities/golden.json b/eval/fixtures/a16-html-entities/golden.json new file mode 100644 index 0000000..0ab8827 --- /dev/null +++ b/eval/fixtures/a16-html-entities/golden.json @@ -0,0 +1,17 @@ +{ + "failureMode": "A16", + "note": "HTML-entity rendered text (self-found on Grafana, 0.4.0): a component whose only rendered text is HTML entities ( , ", >, ", ·, <, ›) normalized to junk tokens ('nbsp', '34', 'gt'), creating spurious matches — and a numeric entity like " let a gibberish query sharing its digits ('...12345') match. Fix (6F.10): decode HTML entities in the parser's text-extraction pass so they become the character React renders, which normalizes away as punctuation. The entity-only component then produces no match target: a named-entity token ('nbsp'), a numeric-entity token ('34'), and a gibberish query that shares those digits all decline no-signal, while a real query still lands on the real component and isn't poisoned by the digit-sharing gibberish.", + "expect": { + "components": [ + { "name": "EntitySpacer", "instances": 0 }, + { "name": "QuotaNotice", "instances": 0 } + ], + "queries": [ + { "terms": ["34"], "status": "declined" }, + { "terms": ["nbsp"], "status": "declined" }, + { "terms": ["zzqwxnomatch12345"], "status": "declined" }, + { "terms": ["Storage quota exceeded"], "status": "ok", "top": "QuotaNotice" }, + { "terms": ["zzqwxnomatch12345", "Storage quota exceeded"], "status": "ok", "top": "QuotaNotice" } + ] + } +} diff --git a/packages/parser-react/src/entities.test.ts b/packages/parser-react/src/entities.test.ts new file mode 100644 index 0000000..cded1a0 --- /dev/null +++ b/packages/parser-react/src/entities.test.ts @@ -0,0 +1,73 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { matchComponentsByText } from "@coderadar/core"; +import { describe, expect, it } from "vitest"; + +import { decodeEntities } from "./entities.js"; +import { scanReact } from "./scan.js"; + +const fixtures = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "../../../eval/fixtures", +); + +describe("decodeEntities", () => { + it("decodes named entities React resolves", () => { + expect(decodeEntities(" ")).toBe(" "); + expect(decodeEntities("a & b")).toBe("a & b"); + expect(decodeEntities("<tag>")).toBe("