Skip to content

fix(ai): widen the groundedness identifier gate past the literal product name - #222

Merged
NathanTarbert merged 2 commits into
mainfrom
jerel/cpk-7928-widen-identifier-guard
Aug 20, 2026
Merged

fix(ai): widen the groundedness identifier gate past the literal product name#222
NathanTarbert merged 2 commits into
mainfrom
jerel/cpk-7928-widen-identifier-guard

Conversation

@jerelvelarde

@jerelvelarde jerelvelarde commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Closes #147.

The identifier signal is the only thing that can withhold a response — #143 made claim wording penalty-only on purpose — so its coverage is the gate. It required the literal substring copilotkit, which exempted every name the model is actually likely to invent.

extractCopilotKitIdentifiers('Call `useCopilotFabricated()` and `<CopilotInvented />`')
  before -> []                                           suppress: false
  after  -> ['useCopilotFabricated', 'CopilotInvented']   suppress: true

useCopilotAction, CopilotChat, CopilotSidebar, CopilotTextarea are all neighbours of real API names, and none of them contain the product name in full. A model inventing a neighbour of a real hook is a far more natural failure than inventing a .copilotKit* CSS class — so the gate was blind to the likelier fabrication while reading as shipped.

Why not just loosen the regex

The issue asks for "a deliberate answer to what is a CopilotKit identifier, not a looser regex", and that framing is right. /copilot/i would have been worse than the bug: a false positive here withholds a correct answer from a real reporter, and copilot on its own is an English word we and our users both use in prose.

So the guard is now a list of named shapes, each one a form a name can only plausibly take when it names our API surface:

# Shape Catches
1 /copilotkit/i CopilotKit, copilotKitInput, CopilotKitProvider
2 /^Copilot[A-Z0-9_]/ CopilotChat, CopilotSidebar, CopilotRuntime
3 /^useCopilot[A-Z0-9_]/ useCopilotAction, useCopilotReadable

The required second capital is the load-bearing detail. It is what keeps English out: bare Copilot, copilots and copiloting are prose about the product, not claims about an API that exists.

CSS_CLASS_PATTERN widens from copilotkit to a copilot prefix on separate reasoning — the leading . marks a selector rather than a word, so .copilot-chat is ours whatever follows.

The early substring pre-filter in identifierSegments is removed rather than ported. The shape rules are anchored, so they cannot be applied to a still-wrapped token (<CopilotChat />), and the per-segment filter at the bottom already decides the same question correctly after unwrapping. IDENTIFIER_PATH still rejects prose, so dropping it costs a little work on non-identifier tokens and buys the gate every name it used to exempt.

Deliberately still outside the gate

The CoAgent / useCoAgent family. Those are ours too, but no shape rule separates them from generic React vocabulary without a hand-maintained name list — and a stale allowlist fails in the direction that withholds correct answers. Documented in the code rather than left as an omission someone rediscovers.

Verification

9 new tests, 81 passing in groundedness.test.ts (up from 72). The corpus gets rows in both directions, since the widening is only half-pinned if it is tested solely where it suppresses:

  • invented neighbours of real names → suppress: true
  • documented useCopilotAction / CopilotChat against sources that contain them → suppress: false, unsourcedIdentifiers: []
  • invented kebab-case classes → suppress: true
  • English copilot prose → nothing extracted

Both mutations that matter were observed RED and restored to GREEN:

Mutation Result
revert shapes to /copilotkit/i (the bug) 6 tests fail
loosen shapes to the naive /copilot/i the English-word guard fails

That second one is the important one — it means the false-positive edge is a real test and not decoration.

Full packages/outpost suite: 61 test files, 1020 tests passing, zero failures — against 1011 on main, so exactly the 9 new tests and nothing disturbed.

Not addressed here

Issue #147 lists two narrower related gaps that I deliberately left alone, because each needs its own decision rather than riding along:

  • TLD allowlist. BARE_HOST_PATTERN strips scheme-less hosts against a 9-TLD list, so a citation on .cloud or .help still leaks a phantom identifier — while widening the list makes it likelier that a genuine identifier ending in a TLD-like segment gets blanked as a host. That is a real two-sided tradeoff, not an oversight.
  • Extraction shape coverage (nested parens, some JSX/prose forms).

Happy to take either as a follow-up.

…uct name

The identifier signal is the only thing that can withhold a response — #143
made claim wording penalty-only on purpose — so its coverage is the whole
gate. It required the literal substring `copilotkit`, which exempted every
name the model is actually likely to invent: `useCopilotAction`,
`CopilotChat`, `CopilotSidebar`, `CopilotTextarea` are all neighbours of real
API names and none of them contain the product name in full.

    extractCopilotKitIdentifiers('Call `useCopilotFabricated()` and `<CopilotInvented />`')
      before -> []                                           suppress: false
      after  -> ['useCopilotFabricated', 'CopilotInvented']   suppress: true

Answering that with a looser /copilot/i would have been worse than the bug: a
false positive here withholds a CORRECT answer from a real reporter, and
`copilot` on its own is an English word. So the guard is now a list of named
SHAPES rather than a substring test — each one a form a name can only
plausibly take when it names our API surface:

  1. carries the product name outright  (`CopilotKit`, `copilotKitInput`)
  2. PascalCase component               (`Copilot[A-Z0-9_]…`)
  3. hook                               (`useCopilot[A-Z0-9_]…`)

The required second capital is what keeps English out: bare `Copilot`,
`copilots` and `copiloting` are prose about the product, not claims about an
API that exists. `CSS_CLASS_PATTERN` widens to a `copilot` prefix on the same
reasoning — the leading `.` marks a selector, so `.copilot-chat` is ours
whatever follows.

The early substring pre-filter in identifierSegments is gone rather than
ported: the shape rules are anchored and cannot be applied to a still-wrapped
token (`<CopilotChat />`), and the per-segment filter already decides the same
question correctly after unwrapping.

Deliberately still outside the gate: the `CoAgent` / `useCoAgent` family. No
shape rule separates those from generic React vocabulary without a
hand-maintained name list, and a stale allowlist fails in the direction that
withholds correct answers.

Pinned in both directions — 9 new tests, and the two mutations that matter
were each observed RED:

  - reverting to /copilotkit/i          -> 6 tests fail
  - loosening to the naive /copilot/i   -> the English-word guard fails

Closes #147
@linear-code

linear-code Bot commented Aug 19, 2026

Copy link
Copy Markdown
CPK-7928 1. #147 — widen the identifier guard (UNBLOCKED, start here)

outpost#147. No open PR touches packages/outpost/ai/src/groundedness.ts — this is the one piece of response-quality work with no merge in front of it.

The defect

groundedness.ts:278 reduces a backticked token to CopilotKit identifiers with:

if (!/copilotkit/i.test(token)) return [];

and CSS_CLASS_PATTERN is /\.(copilotkit[A-Za-z0-9_-]*)/gi. Both require the literal substring copilotkit. So useCopilotAction, CopilotChat, CopilotSidebar, CopilotTextarea — the names the bot most plausibly invents — are exempt from the only signal that can withhold a response.

Why it is load-bearing

After #143 the identifier signal is the sole withholding mechanism; claim wording is penalty-only by design. A gate that covers a fraction of the surface is closer to no gate than to a working one, and it currently reads as shipped.

Care needed

The narrow scoping was deliberate — the docstring says so — to keep generic React vocabulary (useRef, useLayoutEffect) from tripping it, and @copilotkit/* package specifiers are excluded on purpose as stable public knowledge. Widening to /copilot/i will pull in more real names and more false positives, and a false positive here withholds a correct answer from a real reporter.

So this wants a deliberate answer to "what is a CopilotKit identifier", not a looser regex. The existing test suite is the guard: reverting the previous widening (5bff9db) fails 6 tests, so the shape is already pinned.

CPK-7980 Review + land PR #222 — widen the groundedness identifier gate (5 findings open)

Reopened 2026-08-20, Nathan owns it. The review is complete; the findings are not addressed and the PR is not merged, so Done was misreading the board. This ticket now owns the review AND landing it. Retitled to match.

Must-fix before merge: route the CSS_CLASS_PATTERN matches at :364-366 through isCopilotKitIdentifier, strip # in the split at :345 so one fabrication cannot count twice, and de-vacuum the two tests. Widening JSX_WRAPPER / CALL_EXPRESSION is optional-but-recommended. Then a cr-loop confirmation round before merge.

CopilotKit/outpost#222 by jerelvelarde, opened 2026-08-19 04:36Z off main. +168/-11 across 2 files. CI green (Lint/Typecheck/Test 5m23s, zizmor, notify). MERGEABLE / BLOCKED on review. Closes outpost#147 — the parent of this ticket.

Branch is jerel/cpk-7928-widen-identifier-guard, i.e. taken straight from this ticket.

102+  0-  packages/outpost/ai/src/groundedness.test.ts
 66+ 11-  packages/outpost/ai/src/groundedness.ts

Test-heavy ratio (102 new test lines to 66 source), which is the right shape for this change.

Why this one deserves careful review rather than a skim

This is the highest-stakes response-quality change open. After #143, the identifier signal is the only thing that withholds an ungrounded answer — claim wording is penalty-only by design, because parsing English to make a publish/withhold decision failed three times in that PR.

So this gate is load-bearing in both directions, and the danger is asymmetric:

  • Too narrow (today) — useCopilotAction, CopilotChat and most of the public API surface are exempt, so fabricated identifiers publish.
  • Too wide (the new risk) — a false positive withholds a correct answer from a real reporter, publicly, and we would not easily notice.

The specific thing to check: does the new matcher pull in generic React vocabulary (useRef, useLayoutEffect, useMemo)? The original narrow scoping was deliberate and documented for exactly that reason, and @copilotkit/* package specifiers were excluded on purpose as stable public knowledge routinely correct even when absent from retrieved docs.

Worth confirming the 102 new test lines cover both directions — invented CopilotChat caught, legitimate useRef not tripped — rather than only the widening.

cr-loop complete 2026-08-19 — 5 mandatory findings, do not merge as-is

Tier 1 (predicates recorded: 179 LOC, single module, no shared mechanism, reversible). Panel of 3 — code-reviewer + silent-failure-hunter + pr-test-analyzer. Round 1 complete, 3/3. Convergence not declared; no fix cycle run (review-only, and this is Jerel's branch). Review comment posted.

The concern I raised above turned out to be real, and all three agents found it independently. CSS_CLASS_PATTERN was widened to /\.(copilot[A-Za-z0-9_-]*)/gi (:192) and at :364-366 the matches are pushed without passing through isCopilotKitIdentifier, so the new shape allowlist does not guard that branch. Ordinary member access — styles.copilotButton, props.copilotEnabled — now counts as a fabricated identifier, and two of them withhold a correct answer from a real reporter. Silently, since withholding raises nothing.

Mandatory set:

  1. CSS branch bypasses the allowlist — withholds correct answers (3/3 agents).
  2. One fabrication trips the two-identifier thresholdIDENTIFIER_PATH (:244) accepts a leading # but the split at :345 strips only ., so #copilotKitPanel is recorded with the # and dedups separately from its . form. Also means ID selectors can never ground.
  3. The two commonest identifier spellings still escapeJSX_WRAPPER has no attribute support and CALL_EXPRESSION uses [^()] internally, so `<CopilotKitGhost prop="x" />` and `useCopilotAction({ handler: () => {} })` extract nothing. Pre-existing patterns, but load-bearing for this PR's claim to cover the API surface.
  4. Two tests cannot fail:802-812 wraps its assertion in an if so it passes vacuously exactly when extraction starts inventing identifiers; :424 names "empty sources" and passes CHAT_DOCS.
  5. Smaller: BARE_HOST_PATTERN's TLD list (app|dev|io|co|sh) erases copilotKitBridge.io before extraction · the "never throws" docstring at :388 is false for null sources · the hedge reason quotes total hedgeCount while only excessHedges was charged.

Open question for Jerel, not a finding: at :441-448 sourceUrl joins the grounding haystack, so any identifier that is a substring of docs.copilotkit.ai auto-grounds. One agent read it as deliberate, another as a laundering hole — both fit the code, so intent decides.

Ledger + full reports: ~/.local/share/copilotkit/cr/pr222-groundedness/.

Review in Linear

@NathanTarbert

Copy link
Copy Markdown
Collaborator

Hey @jerelvelarde,

Replacing my earlier comment on this PR with a consolidated one. Same reason as #224: I re-ran the review with mutation testing enforced, and it moved things around, including proving that the fix I suggested last time doesn't work. Everything below is executed against the real module unless marked otherwise.

Verdict: NEEDS CHANGES. One blocker. Everything else is non-blocking.


Approved

The shape allowlist is the right design, and the three rules at :211-224 do what the comment says they do: requiring either the literal product name or a second capital is what keeps English out. Mutation-confirmed as load-bearing, several ways. Deleting shape 2 fails 3 tests, deleting shape 3 fails 3, dropping A-Z from shape 2's second character class fails 3.

Deleting the old if (!/copilotkit/i.test(token)) return []; pre-filter in identifierSegments is the load-bearing part of the widening, and it's properly covered: restoring it fails 4 tests.

Leaving CoAgent / useCoAgent out of the gate is fine and it's declared in the diff comment. Agreed as out of scope.


Blocker

groundedness.ts:192CSS_CLASS_PATTERN bypasses the shape gate

The pattern was widened from copilotkit to copilot, and its matches are pushed into hits at :364 without passing through isCopilotKitIdentifier. So the invariant the rest of the PR is built on doesn't apply to this branch. The justification in the diff, that the leading . marks a selector rather than the English word, doesn't hold: in TS/JSON/settings text a . marks member access far more often than a class.

Measured at head against the real CopilotChat docs page as sources:

Response text Head Base
Your handler reads `state.copilotOpen` and toggles `ui.copilotWidth`, neither is a CopilotKit API. ids ["copilotOpen","copilotWidth"], suppress = true ids [], suppress = false
Set `github.copilot.enable` to false, and check `settings.copilotInline` too. ids ["copilot","copilotInline"] ids []
a sentence.Copilot starts here ids ["Copilot"] ids []

So two echoed member accesses from a reporter's own code are enough to hit SUPPRESS_AT_UNSOURCED_IDENTIFIERS at :265 on their own, and a correct answer gets withheld from a real person with no error anywhere. That third row is Copilot bare, which groundedness.test.ts:109 asserts must never be an identifier. Right now `Copilot` is correctly ignored while .Copilot is a fabrication claim.

Correcting myself: I previously said to route the CSS capture through isCopilotKitIdentifier. I ran that, and it fails groundedness.test.ts:100, because copilotSidebarPanel and copilotOpen are shape-identical. That's the proof that shape can't rescue this pattern. Position can:

const CSS_CLASS_PATTERN = /(?<![\w$)\]])\.(copilot[A-Za-z0-9_-]*)/gi;

Applied, 81/81 still pass and both false positives above clear. Note the flip side: they pass either way, so the suite can't currently tell the broken pattern from the fixed one. A regression test for state.copilotOpen and github.copilot.enable needs to land with the fix.


Non-blocking

  • :240 — the new PascalCase rule is mostly inert in practice. JSX_WRAPPER only unwraps prop-less JSX, so Wrap it in `<CopilotFabricated debug={true} />` and `<CopilotInvented labels={{}} />`. yields ids = [] and publishes. A model almost always writes a component with props, so the fabricated-component half of Groundedness identifier signal exempts every name that isn't literally CopilotKit* (useCopilotAction, CopilotChat, …) #147 still gets through in its most common spelling. The regex predates the PR, but component coverage is the headline claim and the new tests only use the bare form (test.ts:82, :94). Allowing trailing attribute text (^<\/?\s*([A-Za-z_$][\w$.-]*)(?:\s[^<>]*)?\s*\/?>$) plus a props-bearing test would close it.
  • :211-224 — common spellings still escape. Verified ids = [] for: undotted kebab (copilot-ghost-panel, while the PR's kebab test at test.ts:100 only covers the dotted form), camelCase invented names (copilotFabricate(), copilotInvented), and lowercase-after-prefix typos (useCopilotfabricated(), <Copilotinvented />). There's also an inconsistency worth a doc line: Copilot_Chat is an identifier because _ is in the class, but copilot_chat isn't, and COPILOT_API_KEY isn't while COPILOTKIT_TOKEN is.
  • :220, :223 — the ^ anchors aren't pinned. :328-333 asserts they're load-bearing, but dropping both anchors passes 81/81. Something like getCopilotXValue / myUseCopilotHook returning [] would fix that.
  • test.ts:769 — the "English word 'copilot'" corpus row is non-discriminating. Its response has no backticks and no dots, so it never reaches either extraction path. Confirmed: replacing all shapes with a naive /copilot/i, and separately dropping the required second capital, each fail only the unit test at test.ts:109 and never this row. Either give it a backticked `Copilot` or a .Copilot, or drop it, because as written it reads as protection it isn't providing.
  • :464 — the trade this PR makes, with the number visible. The widened gate can't distinguish a real API name from an invented one, so its false-positive rate is now bounded entirely by retrieval quality. Measured: Register it with `useCopilotAction()` and mount `<CopilotSidebar />`. against a retrieved CopilotChat page that doesn't literally contain those strings gives suppress = true at head, false at base. Which means the five-ish names correct answers cite most are the ones most likely to be withheld. The API_DOCS corpus row at test.ts:751 only pins the case where the docs do contain them, so this direction is unpinned. I'm not asserting this must change, since you argued against an allowlist and that argument has merit. But those names aren't stale-prone, and an allowlist fails toward publishing, so it's worth deciding deliberately rather than by default.
  • Substring grounding at ~:450 is what saved the github.copilot.enable row above from suppressing: bare copilot is grounded by any source mentioning CopilotKit. Deliberate and documented, but the widening leans on it a lot harder now.

Still open from my earlier read, not re-verified this round

These came from the previous pass and the fresh review didn't cover them, so treat them as unconfirmed rather than dropped. Flagging honestly instead of quietly deleting them:

  • :244 / :345IDENTIFIER_PATH accepts a leading # but the split strips only ., so `#copilotKitPanel` may be recorded with the # attached and dedup separately from its dotted form, turning one invented name into two entries and hitting the threshold alone. Same root cause would mean ID selectors can never ground.
  • test.ts:802-812 — the "claim wording never withholds" assertion looks wrapped in an if, which would make it pass vacuously the moment extraction starts inventing identifiers, i.e. exactly the regression it exists to catch.
  • test.ts:424 — name claims throw coverage and empty sources, but the call passes CHAT_DOCS and asserts neither.
  • :159-160BARE_HOST_PATTERN's TLD list includes app|dev|io|co|sh, so `copilotKitBridge.io` would be erased as a hostname before extraction sees it.
  • :388-395 — the "never throws" docblock, where a null sources or null array element would throw a TypeError inside the publish/withhold decision.
  • :477-479 — the hedge reason quoting total hedgeCount while only excessHedges was charged.
  • :193 — backticked content over 80 chars silently skipped, undocumented and untested.

And the open question from last time still stands: at :441-448, s.sourceUrl is in the grounding haystack, so any identifier that's a substring of docs.copilotkit.ai grounds automatically. Deliberate or a laundering hole? If deliberate, a comment there stops the next reader re-flagging it.


Evidence

vitest 4.1.4 from the lockfile, vite 8.0.8, node v20.19.0, typescript 5.9.3, pnpm 10.33.4. pnpm install --frozen-lockfile clean in 6s.

Head 04a32c2 at 81 passed, base 724a53b at 72 passed, so +9 and nothing moved from pass to fail. One test file fails identically at both commits, @prisma/client throwing on import because --frozen-lockfile skips its postinstall, which is what makes that "1 failed" line interpretable as zero regressions. Prettier clean on both changed files. Vitest has to be rooted at packages/outpost; the repo-root config's include is scripts/__tests__/** and finds nothing.

Not verified: real-world frequency of the false-positive inputs above. I've shown they suppress, I have no corpus telling us how often a generated answer contains two copilotX member accesses or two real API names absent from the retrieved page. Also didn't exercise the pipeline end to end, so the severity of the blocker rests on the documented suppress contract at the three call sites rather than on observed behavior.


Happy to take the blocker as a patch, it's a one-line regex plus the regression test.

The widened `.copilot*` capture pushes straight into `hits` without passing
through `isCopilotKitIdentifier`, so a leading `.` was the entire guard. In TS
and in prose a `.` marks member access far more often than a class, which made
a reporter's own `state.copilotOpen` and `ui.copilotWidth` two unsourced
identifiers — the suppression bar on its own. A correct answer withheld from a
real person, with nothing logged.

Requiring the `.` not to follow an identifier character, `)` or `]` separates
the two. Shape cannot: `copilotSidebarPanel` (ours) and `copilotOpen` (someone's
local state) are shape-identical, so routing the capture through the shape gate
fails the legitimate case instead. Position is the only thing that tells them
apart. Cost is `div.copilotPanel`, a tag-qualified selector, now missed — rarer
than member access, and it fails toward publishing.

Also unwrap JSX that carries props. `JSX_WRAPPER` only matched the prop-less
form, so `<CopilotFabricated debug={true} />` yielded nothing — the
fabricated-component half of #147 escaping in its commonest spelling, since a
model almost always gives an invented component props.

Tests: the suite could not previously tell the broken selector rule from the
fixed one. Now dropping the lookbehind fails 2, reverting the JSX change fails
1, dropping the `^` anchors fails 1, and a naive `/copilot/i` fails 5 where it
used to fail 1 — the "English word" corpus row had no backticks and no dot, so
it reached neither extraction path and passed whatever the rules said.

86 tests in the file, 1025 in the package, all passing.
@jerelvelarde

Copy link
Copy Markdown
Collaborator Author

Blocker fixed in aca57a3. Thanks — this was the right catch and I had the justification backwards.

The blocker

You're right that the leading . was doing no work. I wrote "it marks a selector, not the English word" as if that settled it; in TS and in prose a . marks member access far more often than a class, and this branch pushes into hits without passing through isCopilotKitIdentifier, so there was no second line of defence. Two echoed member reads clearing SUPPRESS_AT_UNSOURCED_IDENTIFIERS on their own is the worst failure direction this module has.

Took your pattern as-is:

const CSS_CLASS_PATTERN = /(?<![\w$)\]])\.(copilot[A-Za-z0-9_-]*)/gi;

And your correction is the useful part of the finding, not a footnote — copilotSidebarPanel and copilotOpen being shape-identical is exactly why position has to be the discriminator here. I've put that reasoning in the comment at :187 so the next person doesn't re-attempt the shape route.

Covered in both directions:

  • state.copilotOpen / ui.copilotWidth, github.copilot.enable / settings.copilotInline, a sentence.Copilot starts here, plus getPanel().copilotWidth and rows[0].copilotState for the ) and ] halves of the class → all []
  • a genuine bare selector and a fenced .copilot-ghost-input still extract, so the fix isn't just switching the branch off
  • a corpus row for the end-to-end direction that matters: reporter's own state echoed back, suppress: false

Mutation: dropping the lookbehind now fails 2. It failed 0 before, which was your point.

Also fixed: the inert PascalCase rule

Agreed, and this one bothered me more than the blocker — component coverage is the headline of #147 and <CopilotFabricated debug={true} /> yielded nothing. Took your regex. A nested > (arrow function in a prop) still falls through to IDENTIFIER_PATH and is rejected; noted in the docstring as acceptable since the name has to satisfy the shape gate either way.

Also fixed: the two test-quality findings

  • test.ts:769 non-discriminating. Confirmed and embarrassing — no backticks, no dot, reached neither extraction path. Now carries a backticked `Copilot`, `copilots`, and a sentence-boundary .Copilot. Naive /copilot/i now fails 5 tests including this row; before it failed 1.
  • :220/:223 anchors unpinned. Confirmed. Added getCopilotXValue and myUseCopilotHook[]. Dropping both anchors now fails 1.

86 in the file, 1025 in the package, prettier clean.

Not taking, with reasons

The allowlist question at :464. This is the one I want to leave deliberately rather than by default, and I think your framing settles it against me on one point: you're right that useCopilotAction, CopilotChat, CopilotSidebar, CopilotTextarea, CopilotPopup are not stale-prone, and that an allowlist for those five fails toward publishing. But it doesn't fix the case #147 was filed about — a fabricated neighbour of a real name — because the fabrication isn't on the list either way. It converts "withheld a correct answer citing a real API" into "published an answer citing an invented one", and I'd rather have the first failure. The real fix is retrieval: if CopilotChat's page doesn't contain useCopilotAction, the answer citing both is standing on one source it shouldn't be. Filing that as a retrieval issue rather than papering it here.

Undotted kebab, camelCase invented names, lowercase-after-prefix typos, and the Copilot_Chat/copilot_chat asymmetry. All confirmed, all genuine gaps, all widen the gate rather than fix a false positive — so they carry false-positive risk that wants its own measurement pass. Filing as a follow-up with your exact strings as the starting test matrix.

The carried-forward items (IDENTIFIER_PATH's #, the vacuous if at test.ts:802, test.ts:424's unasserted args, BARE_HOST_PATTERN eating .io identifiers, the "never throws" docblock vs null sources, the hedgeCount/excessHedges mismatch, the undocumented 80-char cap) — all pre-existing, none introduced here. Filing them together so they don't get lost, since I'd rather this PR stay reviewable against #147.

s.sourceUrl in the grounding haystack — deliberate, and you're right it needs a comment. Going in the follow-up with the rest, since it's a base-behaviour question.

Re-review when you have a moment.

@NathanTarbert

Copy link
Copy Markdown
Collaborator

Hey @jerelvelarde,

Verified aca57a3 with a fresh reviewer. Blocker fixed, nothing legitimate regressed, and all four of your mutation claims reproduce exactly. Approving.

Mutation You claimed Actual
Drop the lookbehind fails 2 2 failed / 84 passed
Revert JSX_WRAPPER fails 1 1 failed / 85 passed
Drop the ^ anchors on shapes 2 and 3 fails 1 1 failed / 85 passed
Naive /copilot/i fails 5, was 1 5 failed at head, 1 failed at base

Every member-access input returns [] with suppress: false, including the two you added that I hadn't suggested (getPanel().copilotWidth, rows[0].copilotState) and (x).copilotPanel. The legitimate direction is intact: bare, backticked, kebab, :hover, descendant and child-combinator selectors all still extract, and the pre-existing test at :99 passes unchanged. Head 1024, base 1019, delta +5 matching four new it blocks plus a corpus row. Prettier clean.

Your stated cost is real but narrower than the comment implies: div.copilotPanel is missed, but a backticked `div.copilotKitPanel` still lands via the backtick path, since IDENTIFIER_PATH matches and the split keeps the shape-passing segment. The loss is confined to names only the selector branch can see. Worth adjusting the comment.

Also checked the thing I'd have worried about in the widened JSX regex: over-capture is impossible by construction. [^<>]* does match a newline and there's no m flag, but the only feed is BACKTICKED_PATTERN, which can't contain one. Multi-line JSX and fenced blocks both yield [], and `<div className="copilotKitFoo">` yields [] at head and base, so the widening can't manufacture a name.

One thing worth folding in, your call

The #-prefix item I'd carried forward as unconfirmed is confirmed, and it's the same failure class this PR exists to close.

IDENTIFIER_PATH accepts a leading #, identifierSegments splits on . only, so the # survives into the reported name. Grounding is haystack.includes(id.toLowerCase()), so #copilotkitpanel can never match a source that writes the name without the #, which is how the docs write it. Measured:

  • `#copilotKitPanel`["#copilotKitPanel"], reported unsourced even when the source contains copilotKitPanel.
  • One name fills two threshold slots: Give the node `#copilotKitPanel` and style `.copilotKitPanel`. yields both forms as distinct case-folded keys.
  • Suppression reachable on its own: Target `#copilotKitPanel` and `#copilotKitSidebar`. against a source containing both names → suppress: true, penalty 0.30. A fully grounded answer withheld.

Both lines are byte-identical at 04a32c28, so this is not a regression from your change — it's pre-existing, and that's why I'm approving rather than blocking. But it's one line (token.split(/[.#]/)) and it's the same bug shape as the blocker, so I'd rather it went in here than in a separate PR where the connection is lost. Filed as outpost#234 either way so it doesn't evaporate.

And I was wrong about two tests

I'd flagged :424 and :802-812 as vacuous. Neither is.

:424 asserts on both branches and the second genuinely extracts ["copilotKitInput"], so the name matches what it exercises. :802-812 does wrap its assertions in an if, but the 'This is a known bug.' corpus row has zero identifiers and takes the branch, so it isn't vacuous.

There is a real weakness there though, worth a line when you're next in the file (:871 in the new numbering): rows with two or more identifiers are silently skipped, and the only structural guard is expect(withClaimsOnly.length).toBeGreaterThan(0). If every claim-charged row grew identifiers, the loop would assert nothing and still pass. Counting the rows that actually reach the assertion would close it.

@NathanTarbert NathanTarbert left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified aca57a3 — blocker fixed, all four mutation claims reproduce exactly, nothing legitimate regressed. Details in the comment above. The #-prefix path is pre-existing rather than a regression here, so it's filed as #234 rather than held against this PR.

@NathanTarbert
NathanTarbert merged commit 23a763e into main Aug 20, 2026
2 checks passed
@NathanTarbert
NathanTarbert deleted the jerel/cpk-7928-widen-identifier-guard branch August 21, 2026 13:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Groundedness identifier signal exempts every name that isn't literally CopilotKit* (useCopilotAction, CopilotChat, …)

2 participants