fix(search): find the property when a reader pastes a code expression - #439
Draft
eugenia-scandit wants to merge 6 commits into
Draft
eugenia-scandit wants to merge 6 commits into
eugenia-scandit wants to merge 6 commits into
Conversation
`this.state.settings.codeduplicatefilter` returns nothing - 5 searches in the last analytics window - while the property it names is documented on 106 pages. Algolia keeps `word.word` as a single token, so that whole string appears on no page. Two dotted shapes fail for opposite reasons, and the shape says which fix applies, so there is ONE retry rather than a chain: TWO segments is `Class.Member`. An enum member has no page of its own, so drop it and search the parent: rectangularviewfinderstyle.legacy 9 -> 505. THREE OR MORE is a code expression pasted from the reader's own source, whose meaning is in the last segment: this.state.settings.codeDuplicateFilter 0 -> 106. Trying the member-drop first, then the last segment, would have cost an extra round trip on every pasted expression and returned the worse answer where both found something. Measured on the live index for every 3+ segment query that returns zero, the member-drop is never better: this.state.settings.codeduplicatefilter drop -> 0 keep -> 106 this.barcodeCapture.settings.symbologies drop -> 0 keep -> 840 sdc.core.ui.viewfinder.rectangular drop -> 0 keep -> 340 com.scandit...barcode.spark...SparkScanView drop -> 6 keep -> 106 settings.barcodeCaptureSettings.codeDup... drop -> 55 keep -> 106 And the namespace-qualified forms the member-drop might have been expected to protect - scandit.datacapture.core.Anchor.TopLeft, Scandit.DataCapture.Core.MeasureUnit.Fraction - return hits AS WRITTEN, so no retry runs for them at all. The retry fires only on zero results and is adopted only when it finds something, so this.state.settings.nonexistentthing still finds nothing rather than inventing a match. dottedFallback: 11/11 unit cases; the facet suite passes; tsc clean; build succeeds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The retry shipped with its measurements in a comment and no committed test -
the "11/11 unit cases" in the parent commit's message was an ad-hoc probe. Six
teen cases now, extracted from the module the way the three siblings in this
suite already are, so changing the real function cannot leave them passing.
The rows that expect `null` carry as much weight as the positive ones: the
retry must decline a query the reader wrote deliberately. Verified by reverting
each decision in turn - returning the whole query instead of the last segment,
dropping the letter requirement, disabling the two-segment branch, and removing
the whitespace guard each fail a case.
One disagreement between the code and its own comment, fixed in the code's
favour. The comment said a trailing number is not a symbol name worth
searching for; `/^[A-Za-z0-9_]{4,}$/` accepted one, so `array.items.1234`
would have become a search for `1234` and returned whatever page mentions that
number. The tail now has to contain a letter.
`readme.md` is recorded as returning `readme` rather than nothing: two
segments take the Class.Member branch, and searching the stem of a file name
is a reasonable answer. My first version of the table asserted `null` there -
the test was wrong, not the function.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
eugenia-scandit
marked this pull request as draft
September 8, 2026 17:06
|
…eiver The docs-search events carry `RectangularViewfinderStyle.LEGACY.` beside the same query without the dot - a reader partway through typing the next member. Its last segment is empty, so no retry ran. Trailing dots are dropped now, and that query retries as `RectangularViewfinderStyle`, which is what it means. Trimming alone would have widened an exposure that was already there, so this closes it in the same commit. The two-segment branch searches for the part BEFORE the dot, and for a pasted expression that part is the receiver: `this.state` retried as `this`, which matches pages containing the word and is worse than an honest no-result. `this.state` alone already reached that branch; trimming would have added `this.state.` to it. EXPRESSION_ROOTS declines the receivers a pasted expression actually starts with, and the constant is read out of the module by the test rather than retyped in it, so the two cannot drift. Six cases added. Both decisions are pinned: removing the trimming and removing the guard each fail one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ts count came from
Three findings from review, none of which the tests caught.
The tail test was loose enough to rewrite common property names. `[A-Za-z0-9_]{4,}`
plus one letter admitted `width`, `count`, `state`, `value`, `enabled`, `text`,
`size` and `color` - each a plausible property at the end of a pasted
expression, each returning zero as written, and each retrying into a thousand
or more unrelated pages that the reader never asked for:
this.overlay.viewfinder.width -> "width" 2848 hits, topped by release notes
session.newlyRecognizedBarcodes.count -> "count" 2512
this.state.settings.enabled -> "enabled" 1264
The last is reachable partway through typing the flagship example itself:
`this.state.settings.code` retries as `code` and flashes 2913 hits. That is the
outcome the design set out to avoid - a confident wrong answer is worse than an
honest no-result. The tail now has to look like a symbol: a case boundary or an
underscore at any length, or eight characters without one. Every intended case
survives (`codeDuplicateFilter`, `symbologies`, `rectangular`, `SparkScanView`,
`arMode`, `max_codes`); the words above do not.
The docstring's justification for the two-segment branch described a transition
this retry cannot make. "9 hits become 505" - but it runs only when the primary
returned ZERO, so a query with nine hits never reaches it. The two numbers were
also whole-index counts while every figure in the table twelve lines below is
contextual. On the basis the rest of the table uses it is 0 -> 203, and the
comment now states the basis once for both.
And the footer promised results its own link could not deliver. `nbHits` is the
count of the response DocSearch received, which after a retry is the retry's,
while the link was built from the query the reader typed - the one that found
nothing. So "See all 107 results" landed on a search page measuring zero. The
link follows the query the count came from, and a line above it says which
query the results are for, since otherwise nothing on screen mentions what was
pasted.
Also from the same review:
- A numeric BASE now declines, matching the tail rule. `2024.11` and `1024.5`
only failed to bite because they return hits as written.
- The retry and its counted ping are tagged `dotted-fallback`. Without it the
feature was invisible in both directions: Algolia's popular-searches and
no-result reports would show `codeduplicatefilter`, a query nobody typed, and
the PostHog ping would stop flagging the paste as zero-result - so the
phenomenon this exists to fix became unmeasurable the moment it shipped.
- Three comments still called the whole mechanism the "enum-member fallback",
which is now half of it and the half a reader is least likely to hit.
- `dottedFallback` joins the extraction guard in the test, with a note that it
is the one function whose brace-counted extraction depends on a regex
quantifier.
Twelve rows added. Six decisions that survived the review's mutation run are now
pinned: the segment threshold, the tail length floor in both directions, the
anchoring of the tail regex, the two-segment member regex, and the numeric base.
One thing is NOT pinned, and stating it rather than implying otherwise: the
footer's link is React markup, and this suite has no renderer. The change is
verified by reading, not by a test.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The retry sent the identical query. DocSearch builds `{ query, indexName,
params }` - the query is at the TOP LEVEL and `params` has no `query` key at
all - and the override was guarded by `typeof params.query === "string"`, which
is therefore never true. So `params.query = queryOverride` never executed:
call 1 query="this.state.settings.codeDuplicateFilter" tags=["as-you-type"]
call 2 query="this.state.settings.codeDuplicateFilter" tags=[…,"dotted-fallback"]
Byte-identical apart from the tag, so the retry returned zero too and nothing
was ever adopted. What this branch shipped was one extra Algolia operation per
zero-result dotted query and no behaviour change. Every measurement in the
docstring described an Algolia the widget never talked to.
The guard is not new - it is in main, and `strippedQuery` goes through the same
function, so the routed framework/version strip has been inert since it landed.
Fixing it here fixes that too.
applyQueryOverride is a module-scope function now, with a test that feeds it
the request shape @docsearch/react actually sends. Nothing in the suite covered
the request; it covered the predicate that picks the string, which is exactly
how this reached review.
Two more, both mine from the previous commit:
The footer note fired on any query naming a framework or a version. I wired it
to `primaryQuery`, which is the STRIPPED query, so it differed from the typed
one whenever the strip did anything - and told the reader "nothing matched
`barcode capture ios` as written" about a query with 443 hits, linking to one
with 475. That is the defect the previous commit set out to fix, reintroduced
in the other direction. It is driven by retry ADOPTION now, through a ref that
holds `{ typed, used }` and is compared against `state.query` before anything
is shown - the relevance strip is not a fallback and must never be surfaced.
And a slow retry from an abandoned query overwrote that ref after a later query
had rendered, leaving the footer showing the old retry against the new count.
The write is guarded on the query still being the live one.
The symbol-shape rule was defeated by capitalisation, because Algolia matches
case-insensitively: the words it was meant to exclude came straight back with a
capital letter, and PascalCase is the norm in .NET, which this file treats as a
framework. Measured on the basis the docstring now states:
Text 408 Size 126 Color 158 Value 154 no internal boundary
codeD 355 five characters
available 325 configured 327 selection 245 bare lowercase
codeDu 59 arMode 11 SparkScanView 29 accepted
`codeD` is one keystroke past the example the previous commit singled out, so
that fix had moved the bad answer by one character rather than removing it. The
rule is now an INTERNAL lower-to-upper boundary or an underscore, and at least
six characters.
The bare-lowercase branch is gone, and that costs two of the docstring's own
examples: `symbologies` (271) and `rectangular` (55) lose their retry. They are
documented terms and the retry would have helped - but `selection` (245) and
`configured` (327) are prose, the counts overlap, and nothing in the string
separates them. The conservative reading wins; both remain reachable as a
two-segment query.
Every count in that comment is now on one stated basis - the live index on
2026-09-08 under a current-version reader's facet filters. Mixing bases is how
an earlier version came to cite a 9-hit query, which this retry can never see.
Two claims did not survive re-measurement on it and are corrected rather than
left: the namespace-qualified constants do NOT return hits as written
(`scandit.datacapture.core.Anchor.TopLeft` is 0), so the retry does reach them -
their tail `TopLeft` is also 0, so the adoption check, not the shape check, is
what keeps the honest no-result.
Test rows: three were mislabelled and pinned nothing - an "eight lowercase
characters" row using a nine-character tail, a "short tail with an underscore"
using a nine-character one, and a numeric-tail row that the shape rule had
taken over. Eleven decisions are pinned now, verified by reverting each: the
override in both places it lands, the character set, the letter guard, the
six-character floor, the internal boundary, the underscore clause, the base
floor from both sides, the numeric base, and the case-folding of the expression
roots. The `{4,}` floor is deleted - it became unreachable when the length
check moved to six and read as if it were still doing something.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ht facet
Three rounds went into reading "symbol or ordinary word?" out of the query
string, and the string does not carry it. Each rule that excluded prose also
excluded something documented, and each was bypassed within a keystroke:
{4,} + a letter admitted width, enabled
contains an uppercase admitted them capitalised (Algolia is case-insensitive)
eight characters or more admitted available, configured
internal case boundary admitted codeD, one keystroke past the example
And on the real index `barcodecapture` is a class name returning 2633 while
`options` is a word returning 883, so no lexical rule could have worked.
What separates them is how specific the answer is. adoptRetry believes a retry
only when it returns few enough hits to be a symbol match, and the measured
groups do not overlap:
believed max_codes 2 arMode 12 legacy 37 SparkScanView 106
codeDuplicateFilter 107 TopLeft 170 codeDu 183
rectangularviewfinderstyle 203 barcodecapturesettings 212
---- 250 ----
declined rectangular 340 options 883 symbologies 842 config 1259
enabled 1264 codeD 1505 settings 1678 barcodecapture 2633
width 2848 core 3488
So the shape heuristics are gone. This also closes the two-segment harm review
reproduced - `config.enabled` retried into 1259 pages and `options.timeout`
into 883, with a footer note asserting it - without needing a shape rule there
either, and it accepts two cases the old rules wrongly declined (`max_c` 2,
`LEGACY` 37).
The candidate now comes from the TYPED query. Stripping a trailing framework
token turned `settings.viewfinder.web` into `settings.viewfinder.` - three
segments became two and the retry became a search for `settings`. The strip is
for relevance and must not decide which rewrite applies.
Every count in this file and the PR was measured under `api-reference-8.6`, a
facet tag that appears nowhere as a value. Readers search under
`api-reference-latest`, which the suite itself asserts. Everything was 3-6x too
small, and two conclusions drawn from those figures reverse on the real ones -
including the reason given for dropping `symbologies` and `rectangular`, which
was "the counts overlap with prose". They do not; the exclusion is right but the
stated reason was wrong, and it is now the ceiling that declines them. The
two-segment example is 0 -> 203 rather than 0 -> 30, the three-segment one
0 -> 107 rather than 0 -> 36, and the member-drop column reads 0/55/22 rather
than 0/1/0 - which argues for the ordering more strongly than the zeroes did.
Also restored: `scandit.datacapture.core.Anchor.TopLeft` returns 14 as written,
all `anchor.html` pages, so no retry runs for it. Round 1 said so, I "corrected"
it to 0 on the wrong basis, and this puts the true reading back.
runDottedRetry is at module scope with its collaborators injected. Both defects
this branch shipped lived in the closure it replaces and neither was reachable
from a test: an override that never applied to the request, and a candidate
taken from the stripped query. The suite drives it with a fake search and
asserts the requests that come out.
Two harness bugs found while doing that, both of the "test that cannot fail"
kind: check() called fn() and dropped the result, so an async body that threw
printed "ok" and the rejection went unhandled - verified by breaking an
assertion and watching it pass. And extract() lost a leading `async` and was
defeated by a destructured parameter list, since it counts braces from
`function`.
One row claimed to pin the whitespace guard and did not - both cited queries
are declined by other rules. A question carrying a pasted symbol does need it,
and that is the row now.
Sixteen decisions verified by reverting each: the zero gate, the adoption gate,
the ceiling and its value, that adoptRetry is called, that the typed query
feeds the fallback, the override in both places it lands, the identifier floor,
the letter guard, the base floor, the numeric base, the expression roots and
their case-folding, the whitespace guard, and trailing-dot trimming.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was broken
this.state.settings.codeDuplicateFilterreturns nothing, while the property it names is documented. Algolia keepsword.wordas a single token, so that whole string appears on no page at all.Two dotted shapes fail for opposite reasons, and the segment count says which, so there is one retry rather than a chain:
Class.Member. An enum member has no page of its own, so drop it and search the parent:rectangularviewfinderstyle.legacy→ 0 hits become 30.this.state.settings.codeDuplicateFilter→ 0 hits become 36.Every count in this PR and in the code comments is on one stated basis: the live index on 2026-09-08 under the facet filters a current-version reader searches with (
language:enANDdocusaurus_tag:default | docs-default-current | api-reference-8.6). Unfiltered counts run about an order of magnitude higher and are not what a reader sees.Why one retry and not two
For a 3+ segment query that returned zero, dropping the member is useless where keeping the tail works:
So trying the member-drop first would cost an extra request on every pasted expression and return the worse answer where both found something.
What it will not do
The retry fires only on zero results and is adopted only if it finds something, so
this.state.settings.nonexistentthingstill finds nothing rather than inventing a match.The tail also has to look like a symbol rather than a word: an internal lower-to-upper boundary or an underscore, and at least six characters. "Contains an uppercase letter" was a bypass — Algolia matches case-insensitively, so words came back capitalised, and PascalCase is the norm in .NET:
A bare lowercase tail gets no retry at any length. That costs two useful cases —
symbologies(271) andrectangular(55) — butselection(245) andconfigured(327) are prose, the counts overlap, and nothing in the string separates them. Both remain reachable as a two-segment query.Also declined: a phrase, a bare word, a numeric tail or base, a version string, a trailing-dot-only query, and the receiver of a pasted expression (
this.statemust not retry asthis).The bug this PR also fixes in
mainThe query override never reached Algolia. DocSearch builds
{ query, indexName, params }— the query is at the top level, andparamshas noquerykey — while the override was guarded bytypeof params.query === "string", which is never true.strippedQuerygoes through the same function, so the routed framework/version strip has been inert in production since it landed.applyQueryOverrideis a module-scope function now, tested against the request shape@docsearch/reactactually sends.Footer
nbHitsis the count of the response DocSearch received, which after a retry is the retry's. The "See all N results" link now follows the query that count came from, with a line saying which query the results are for — driven by retry adoption, not by "the text differs", and guarded so a superseded retry cannot show a note against a later query.Tests
scripts/test-search-facets.cjsgains two checks and a 46-row table (11 positives, 35 declines), extracted from the module the way the three existing readers in that suite are — so changing the real function cannot leave them passing. The rows that expect no retry carry as much weight as the positive ones.Eleven decisions are pinned, each verified by reverting it and confirming a row fails: the override in both places it lands, the identifier character set, the letter guard, the six-character floor, the internal case boundary, the underscore clause, the base floor from both sides, the numeric base, and the case-folding of the expression roots.
Not pinned, stated rather than implied: the footer's link is React markup and this suite has no renderer, so that change is verified by reading.
Scope
One behavioural file (
src/theme/SearchBar/index.js) and its test. No content, config or build changes.🤖 Generated with Claude Code